git.lucas.co / cce-status-interface
status bar
git clone https://git.lucas.co/cce-status-interface.git

commit6c96a7d5cb4d8da7d31f482dd09a7ec5fa5127f4
parenta66a5445a6
authorLucas Galante <lsgalante12@gmail.com>
date2026-10-05 22:15
perf: the bar's pollers read what they paint, and wait on events

Measured in a scale-2 shadow, the bar idle for 10 s (installed build ->
this one): pactl spawns 36 -> 0; context switches clock 168 -> 10,
launcher 449 -> 79, stats 754 -> ~330-410.

- spawn_system_stats ran whole in every stats segment -- the clock
  included -- each second, two `pactl` spawns per pass (each walking
  ~10 PATH entries first). It now reads only what its segment paints
  (module_reads, the stats_signature split) and never the backlight or
  the sink where the fast path owns them (has_fast_levels); update()
  keeps those two across a stats push. The clock alone wakes once a
  minute, on the minute (until_next_minute).
- The volume subscription re-reads the sink each time it (re)starts,
  replacing the poll as the catch-up after a gap.
- watch_brightness blocks in poll(POLLPRI) on actual_brightness, which
  the kernel sysfs_notify's on every brightness write (checked on this
  machine: ~20 ms after a same-value `brightnessctl set`, nothing at
  rest), instead of re-reading sysfs every 100 ms in two processes.
  The 100 ms poll remains the fallback.
- A CPU reading reaches the bar only when it moves CPU_STEP (2) points
  or after CPU_HOLD_S (5 s), so idle jitter no longer redraws the stats
  bubble -- and re-bakes the compositor's blur behind it -- each second.
- The launcher's supervisor waits on SIGCHLD (and a timer only while a
  restart is pending) instead of try_wait-ing every 500 ms; killing the
  clock in a shadow still restarts it after its backoff.
- The launcher and module runtimes run 2 tokio workers, not one per core
  (20 here, in each of six processes).

CLAUDE.md: the stats and fast-path paragraphs, and the test count.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

 CLAUDE.md    |  26 ++++---
 src/main.rs  | 145 +++++++++++++++++++++++++--------------
 src/osd.rs   |   4 +-
 src/stats.rs | 221 ++++++++++++++++++++++++++++++++++++++++++++++++++---------
 4 files changed, 302 insertions(+), 94 deletions(-)

diff --git a/CLAUDE.md b/CLAUDE.md
index 0aa210c..163fbe3 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -20,7 +20,7 @@ cce-icons glyph textures), `src/listeners.rs` (status/switcher socket tasks),
 
 ```sh
 cargo build --release                 # standalone build (or `-p cce-status-interface` from the workspace root)
-cargo test                            # 48 tests: main.rs (parsers, droplet geometry, menu glyphs), config.rs, tray.rs, osd.rs, the tray bridge's x11.rs and title.rs
+cargo test                            # 50 tests: main.rs (parsers, droplet geometry, menu glyphs), stats.rs, config.rs, tray.rs, osd.rs, the tray bridge's x11.rs and title.rs
 make install                          # release build, then `ccebuild install --no-build cce-status-interface`
 ```
 
@@ -292,15 +292,23 @@ listen to tray D-Bus, etc.:
   `SystemStats` carries numbers (`cpu_pct`, `memory`, `battery: (capacity,
   charging)`, `volume: (level, muted)`, `brightness`, `wifi: (signal,
   connected)`), each `Option` where
-  the source can be absent; only the clock arrives pre-formatted. The loop is
+  the source can be absent; only the clock arrives pre-formatted. The loop
+  reads only what its segment paints (`module_reads`, the same split as
+  `stats_signature`) — the `clock` segment reads nothing but the time, and
+  wakes once a minute, on the minute. Until 2026-10-05 every stats segment
+  ran the whole poll, two `pactl` spawns included, every second. A CPU
+  reading reaches the bar only when it moves by `CPU_STEP` points or has been
+  held back `CPU_HOLD_S`, so idle jitter does not redraw the segment (and
+  re-bake the compositor's blur behind it) each second. The loop is
   once a second, which is fine for a clock or a load average and far too slow
   for the two values a KEYPRESS moves — so the backlight and the sink have a
   fast path beside it (`spawn_level_watchers`), each pushing its own
   one-field event (`BrightnessUpdated` / `VolumeUpdated`) that patches
-  `stats` in place. `watch_brightness` polls `/sys/class/backlight` every
-  100ms — `brightnessctl` writes the attribute directly, so there is nothing
-  to subscribe to, and two small sysfs reads are cheap enough that the
-  interval is not worth tuning; `watch_volume` follows `pactl subscribe` and
+  `stats` in place. `watch_brightness` blocks in `poll(POLLPRI)` on
+  `actual_brightness`, which the kernel's `backlight_generate_event`
+  `sysfs_notify`s on every write to `brightness` (measured ~20 ms after a
+  `brightnessctl set`, nothing at rest); it falls back to the old 100 ms poll
+  only when that cannot be opened. `watch_volume` follows `pactl subscribe` and
   re-reads only on a `sink`/`server` event (NOT `sink-input`, which fires
   throughout playback, and NOT `client`, which the bar's own `pactl` runs
   generate — matching either would put the reader in a loop with itself).
@@ -312,8 +320,10 @@ listen to tray D-Bus, etc.:
   `pactl` spawn per event would fall behind); the child carries
   `PR_SET_PDEATHSIG` as well as `kill_on_drop`, because a subscription whose
   reader was killed outright is reparented to init and sits there rather than
-  noticing. The one-second loop still reads both values, so it remains the
-  safety net when `pactl subscribe` cannot run at all. Measured in a shadow:
+  noticing. Where the fast path runs, the one-second loop does NOT read
+  either value (`has_fast_levels`) and `update()` keeps them across a stats
+  push; each subscription re-reads the sink when it (re)starts, so a gap
+  while `pactl subscribe` was down is caught up. Measured in a shadow:
   ~45ms for the backlight, ~55ms for the sink, against a second before.
 - **Tray** (`spawn_status_tray`): a full StatusNotifierItem/Watcher host over `zbus`,
   including DBusMenu fetching. Icons arrive as pixmaps or theme names (rendered via
diff --git a/src/main.rs b/src/main.rs
index 3ebbda4..f10f963 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -117,6 +117,13 @@ fn stats_signature(module: Option<&str>, s: &SystemStats) -> Option<String> {
     }
 }
 
+/// Does this module run the brightness/volume fast path
+/// ([`spawn_level_watchers`])? Then it owns those two values, and the
+/// one-second poll leaves them alone.
+fn has_fast_levels(module: Option<&str>) -> bool {
+    paints_stat(module, "brightness") || paints_stat(module, "volume")
+}
+
 /// Does this module paint `field` (`"brightness"` or `"volume"`)? The
 /// fast-path pushes carry one value each, so they ask this where a full stats
 /// push compares a [`stats_signature`]; the two must agree about which
@@ -1402,15 +1409,15 @@ impl cce_ui::engine::Application for StatusApp {
         let has_stats = selected_module.as_ref().map_or(true, |(name, _)| {
             name == "stats" || name == "cpu" || name == "memory" || name == "brightness" || name == "volume" || name == "battery" || name == "wifi" || name == "clock"
         });
-        if has_stats {
-            tokio::spawn(spawn_system_stats(sender.clone()));
-        }
         // The backlight and the sink are what a keypress moves, so they get a
-        // fast path alongside the one-second poll — only where a module
+        // fast path instead of the one-second poll — only where a module
         // actually paints one of them.
-        if paints_stat(selected_module.as_ref().map(|(n, _)| n.as_str()), "brightness")
-            || paints_stat(selected_module.as_ref().map(|(n, _)| n.as_str()), "volume")
-        {
+        let module_name = selected_module.as_ref().map(|(n, _)| n.clone());
+        let fast_levels = has_fast_levels(module_name.as_deref());
+        if has_stats {
+            tokio::spawn(spawn_system_stats(sender.clone(), module_name, fast_levels));
+        }
+        if fast_levels {
             let sender = sender.clone();
             tokio::spawn(spawn_level_watchers(move |change| {
                 let _ = sender.send(change.into());
@@ -1505,7 +1512,16 @@ impl cce_ui::engine::Application for StatusApp {
                 changed = self.title != t;
                 self.title = t;
             }
-            CustomEvent::SystemStatsUpdated(s) => {
+            CustomEvent::SystemStatsUpdated(mut s) => {
+                // Where the fast path runs, the poll does not read the
+                // backlight or the sink (`spawn_system_stats`): those two
+                // stay as the fast path last set them.
+                if has_fast_levels(self.selected_module_name.as_deref()) {
+                    if let Some(old) = self.stats.as_ref() {
+                        s.brightness = old.brightness;
+                        s.volume = old.volume;
+                    }
+                }
                 log::debug!("[module-{}] stats updated, current width={}", self.selected_module_name.as_deref().unwrap_or("none"), self.width);
                 let module = self.selected_module_name.as_deref();
                 match stats_signature(module, &s) {
@@ -2198,11 +2214,13 @@ fn main() {
 
     if !has_module {
         log::info!("Starting cce-status-interface launcher daemon...");
-        let rt = tokio::runtime::Runtime::new().expect("tokio runtime");
+        let rt = small_runtime();
         rt.block_on(async {
             use tokio::signal::unix::{signal, SignalKind};
             let mut sigint = signal(SignalKind::interrupt()).expect("SIGINT");
             let mut sigterm = signal(SignalKind::terminate()).expect("SIGTERM");
+            // Before any module is spawned, so an early exit is not missed.
+            let mut sigchld = signal(SignalKind::child()).expect("SIGCHLD");
             
             // `stats` is the combined readout segment (cpu, memory,
             // brightness, volume, wifi, battery in one bubble); the six single
@@ -2262,6 +2280,11 @@ fn main() {
             }
 
             loop {
+                let next_restart = supervised
+                    .values()
+                    .filter(|e| e.child.is_none())
+                    .map(|e| e.restart_at)
+                    .min();
                 tokio::select! {
                     _ = sigint.recv() => {
                         log::info!("Received SIGINT, shutting down...");
@@ -2271,50 +2294,58 @@ fn main() {
                         log::info!("Received SIGTERM, shutting down...");
                         break;
                     }
-                    _ = tokio::time::sleep(std::time::Duration::from_millis(500)) => {
-                        let now = std::time::Instant::now();
-                        for module in &modules {
-                            let Some(entry) = supervised.get_mut(*module) else { continue };
-
-                            if let Some(child) = entry.child.as_mut() {
-                                match child.try_wait() {
-                                    Ok(None) => continue,
-                                    Ok(Some(status)) => {
-                                        entry.child = None;
-                                        if entry.spawned_at.elapsed() >= HEALTHY_UPTIME {
-                                            entry.backoff = RESTART_BASE;
-                                        } else {
-                                            entry.backoff = (entry.backoff * 2).min(RESTART_MAX);
-                                        }
-                                        entry.restart_at = now + entry.backoff;
-                                        log::warn!(
-                                            "Module process '{}' exited with status: {:?}. Restarting in {:?}...",
-                                            module, status, entry.backoff
-                                        );
-                                    }
-                                    Err(e) => {
-                                        log::error!("Error checking status for module '{}': {:?}", module, e);
-                                        continue;
-                                    }
+                    // A module exited — or any child: the slider, a `pactl` —
+                    // or a held-back restart is due. Until 2026-10-05 this
+                    // woke every 500 ms to `try_wait` each module.
+                    _ = sigchld.recv() => {}
+                    _ = async {
+                        match next_restart {
+                            Some(t) => tokio::time::sleep_until(tokio::time::Instant::from_std(t)).await,
+                            None => std::future::pending().await,
+                        }
+                    } => {}
+                }
+                let now = std::time::Instant::now();
+                for module in &modules {
+                    let Some(entry) = supervised.get_mut(*module) else { continue };
+
+                    if let Some(child) = entry.child.as_mut() {
+                        match child.try_wait() {
+                            Ok(None) => continue,
+                            Ok(Some(status)) => {
+                                entry.child = None;
+                                if entry.spawned_at.elapsed() >= HEALTHY_UPTIME {
+                                    entry.backoff = RESTART_BASE;
+                                } else {
+                                    entry.backoff = (entry.backoff * 2).min(RESTART_MAX);
                                 }
+                                entry.restart_at = now + entry.backoff;
+                                log::warn!(
+                                    "Module process '{}' exited with status: {:?}. Restarting in {:?}...",
+                                    module, status, entry.backoff
+                                );
                             }
+                            Err(e) => {
+                                log::error!("Error checking status for module '{}': {:?}", module, e);
+                                continue;
+                            }
+                        }
+                    }
 
-                            if now >= entry.restart_at {
-                                match spawn_module(module) {
-                                    Ok(c) => {
-                                        log::info!("Restarted module process for: {}", module);
-                                        entry.child = Some(c);
-                                        entry.spawned_at = now;
-                                    }
-                                    Err(e) => {
-                                        entry.backoff = (entry.backoff * 2).min(RESTART_MAX);
-                                        entry.restart_at = now + entry.backoff;
-                                        log::error!(
-                                            "Failed to restart module process for {}: {:?}. Retrying in {:?}...",
-                                            module, e, entry.backoff
-                                        );
-                                    }
-                                }
+                    if now >= entry.restart_at {
+                        match spawn_module(module) {
+                            Ok(c) => {
+                                log::info!("Restarted module process for: {}", module);
+                                entry.child = Some(c);
+                                entry.spawned_at = now;
+                            }
+                            Err(e) => {
+                                entry.backoff = (entry.backoff * 2).min(RESTART_MAX);
+                                entry.restart_at = now + entry.backoff;
+                                log::error!(
+                                    "Failed to restart module process for {}: {:?}. Retrying in {:?}...",
+                                    module, e, entry.backoff
+                                );
                             }
                         }
                     }
@@ -2331,12 +2362,24 @@ fn main() {
         return;
     }
 
-    let rt = tokio::runtime::Runtime::new().expect("tokio runtime");
+    let rt = small_runtime();
     let _guard = rt.enter();
 
     cce_ui::engine::run::<StatusApp>();
 }
 
+/// The runtime for the launcher and every module process: a few sockets, a
+/// timer and a `pactl` pipe, so two workers. `Runtime::new()` starts one per
+/// core — twenty here, in each of the bar's six processes — and a timer tick
+/// on a runtime that wide wakes idle workers to look for work to steal.
+fn small_runtime() -> tokio::runtime::Runtime {
+    tokio::runtime::Builder::new_multi_thread()
+        .worker_threads(2)
+        .enable_all()
+        .build()
+        .expect("tokio runtime")
+}
+
 
 #[cfg(test)]
 mod tests {
diff --git a/src/osd.rs b/src/osd.rs
index 906a19e..10b7ef1 100644
--- a/src/osd.rs
+++ b/src/osd.rs
@@ -12,8 +12,8 @@
 //!
 //! - **The trigger** ([`spawn_trigger`]) runs in the launcher daemon. It
 //!   drives the same fast-path watchers the bar's readouts use
-//!   ([`crate::spawn_level_watchers`] — the backlight polled every 100ms, the
-//!   sink followed through `pactl subscribe`), so the slider moves for a
+//!   ([`crate::spawn_level_watchers`] — the backlight's sysfs change
+//!   notification, the sink followed through `pactl subscribe`), so the slider moves for a
 //!   keypress, `brightnessctl` in a terminal or a mixer app alike. Each change
 //!   is forwarded as one line to the running slider, or starts one.
 //! - **The slider** (`--osd <line>`, [`main`]) is a single-instance `cce-ui`
diff --git a/src/stats.rs b/src/stats.rs
index 150fd65..487fd07 100644
--- a/src/stats.rs
+++ b/src/stats.rs
@@ -196,47 +196,108 @@ pub(crate) fn get_initial_stats() -> SystemStats {
     }
 }
 
-pub(crate) async fn spawn_system_stats(sender: calloop::channel::Sender<CustomEvent>) {
+/// Does `module` paint `field` — one of the [`SystemStats`] field names?
+/// The same split as `stats_signature`: a single-stat module reads its own
+/// field, the combined `stats` bar (or an unknown name) reads them all.
+pub(crate) fn module_reads(module: Option<&str>, field: &str) -> bool {
+    match module {
+        Some("window") | Some("tray") | Some("light_source") => false,
+        Some(m @ ("clock" | "cpu" | "memory" | "battery" | "volume" | "brightness" | "wifi")) => m == field,
+        _ => true,
+    }
+}
+
+/// A CPU reading moves the bar only by at least this many points, or once
+/// it has been held back for [`CPU_HOLD_S`]. The busy figure jitters by a
+/// point or two every second at rest, and each change redraws the segment
+/// (and the compositor re-bakes the blur behind it).
+const CPU_STEP: u8 = 2;
+const CPU_HOLD_S: u64 = 5;
+
+/// The once-a-second poll. It reads only what `module` paints, and never the
+/// two values the fast path owns when `fast_levels` is set (the backlight and
+/// the sink, [`spawn_level_watchers`]): the bar keeps those from the fast
+/// path, so polling them too only spawned `pactl` twice a second for nothing.
+/// A clock alone wakes once a minute, on the minute — it shows no seconds.
+pub(crate) async fn spawn_system_stats(
+    sender: calloop::channel::Sender<CustomEvent>,
+    module: Option<String>,
+    fast_levels: bool,
+) {
     log::info!("[spawn_system_stats] Starting system stats loop!");
+    let module = module.as_deref();
+    let reads = |field: &str| module_reads(module, field);
+    let read_volume_here = reads("volume") && !fast_levels;
+    let read_brightness_here = reads("brightness") && !fast_levels;
+    let clock_only = ["memory", "cpu", "battery", "volume", "brightness", "wifi"]
+        .iter()
+        .all(|f| !reads(f));
+
     let mut last_cpu = read_cpu_ticks().unwrap_or((0, 0));
+    let mut shown_cpu: Option<u8> = Some(0);
+    let mut shown_cpu_at = std::time::Instant::now();
     loop {
         log::debug!("[spawn_system_stats] loop iteration start");
         let clock = chrono::Local::now().format("%A, %B %d, %Y %I:%M %p").to_string();
-        let memory = read_memory_usage();
-        
-        let cpu_pct = if let Some(current_cpu) = read_cpu_ticks() {
-            let total_diff = current_cpu.0 - last_cpu.0;
-            let idle_diff = current_cpu.1 - last_cpu.1;
-            last_cpu = current_cpu;
-            if total_diff > 0 {
-                let usage = 100.0 - (idle_diff as f32 * 100.0 / total_diff as f32);
-                Some(usage.round().clamp(0.0, 100.0) as u8)
-            } else {
-                Some(0)
+
+        let cpu_pct = if reads("cpu") {
+            let sample = read_cpu_ticks().map(|current_cpu| {
+                let total_diff = current_cpu.0 - last_cpu.0;
+                let idle_diff = current_cpu.1 - last_cpu.1;
+                last_cpu = current_cpu;
+                if total_diff > 0 {
+                    let usage = 100.0 - (idle_diff as f32 * 100.0 / total_diff as f32);
+                    usage.round().clamp(0.0, 100.0) as u8
+                } else {
+                    0
+                }
+            });
+            let moved = match (sample, shown_cpu) {
+                (Some(new), Some(old)) => {
+                    new.abs_diff(old) >= CPU_STEP
+                        || (new != old && shown_cpu_at.elapsed().as_secs() >= CPU_HOLD_S)
+                }
+                (a, b) => a != b,
+            };
+            if moved {
+                shown_cpu = sample;
+                shown_cpu_at = std::time::Instant::now();
             }
+            shown_cpu
         } else {
             None
         };
 
         let stats = SystemStats {
             clock,
-            memory,
+            memory: if reads("memory") { read_memory_usage() } else { None },
             cpu_pct,
-            battery: read_battery_details(),
-            volume: read_volume().await,
-            brightness: read_brightness(),
-            wifi: read_wifi(),
+            battery: if reads("battery") { read_battery_details() } else { None },
+            volume: if read_volume_here { read_volume().await } else { None },
+            brightness: if read_brightness_here { read_brightness() } else { None },
+            wifi: if reads("wifi") { read_wifi() } else { None },
         };
         log::debug!("[spawn_system_stats] stats: {:?}", stats);
         let _ = sender.send(CustomEvent::SystemStatsUpdated(stats));
-        tokio::time::sleep(std::time::Duration::from_secs(1)).await;
+        let wait = if clock_only {
+            until_next_minute(chrono::Local::now())
+        } else {
+            std::time::Duration::from_secs(1)
+        };
+        tokio::time::sleep(wait).await;
     }
 }
 
-/// How often the backlight is re-read on the fast path. Two small sysfs
-/// reads, so the cost is a rounding error next to the once-a-second poll's
-/// two `pactl` processes — and nothing is sent unless the value moved, so an
-/// unchanged backlight never wakes the bar's event loop.
+/// How long until just past the next minute boundary of `now`.
+pub(crate) fn until_next_minute(now: chrono::DateTime<chrono::Local>) -> std::time::Duration {
+    use chrono::Timelike;
+    let into = std::time::Duration::from_secs(now.second() as u64)
+        + std::time::Duration::from_nanos(now.nanosecond().min(999_999_999) as u64);
+    std::time::Duration::from_secs(60).saturating_sub(into) + std::time::Duration::from_millis(50)
+}
+
+/// How often the backlight is re-read when its change notification cannot be
+/// had ([`watch_brightness`]'s fallback).
 const BRIGHTNESS_POLL_MS: u64 = 100;
 
 /// How long a sink event is held before the volume is read, swallowing the
@@ -248,8 +309,10 @@ const VOLUME_COALESCE_MS: u64 = 30;
 /// The fast path for the two values a keypress moves: the backlight and the
 /// default sink. Both reach the bar the moment they change instead of at the
 /// next [`spawn_system_stats`] tick, which is a full second at worst — long
-/// enough that the number visibly lags the key. The one-second poll still
-/// reads both, so it remains the safety net if either watcher cannot run.
+/// enough that the number visibly lags the key. Where these run, the poll no
+/// longer reads either value: each watcher is its own safety net (the volume
+/// one re-reads on every resubscribe, the backlight one falls back to
+/// polling).
 ///
 /// Each change goes to `emit`: the bar turns it into a one-field
 /// `CustomEvent`, and the launcher daemon into the volume/brightness slider
@@ -280,19 +343,75 @@ impl From<LevelChange> for CustomEvent {
     }
 }
 
-/// Poll `/sys/class/backlight` and push every change. `brightnessctl` writes
-/// the sysfs attribute directly (see the compositor's media-key bindings), so
-/// there is nothing to subscribe to — but the read is two small files, and
-/// only a moved value is sent.
-async fn watch_brightness(emit: impl Fn(LevelChange)) {
-    let mut last = read_brightness();
+/// Push every backlight change. The kernel signals `actual_brightness`
+/// (`sysfs_notify`, from `backlight_generate_event`) on every write to
+/// `brightness` — `brightnessctl`, the compositor's media keys, firmware
+/// hotkeys — so a thread blocks in `poll` on it and wakes only then.
+/// Measured on this panel: the notification lands ~20 ms after a write, and
+/// nothing fires at rest. Until 2026-10-05 this re-read the files every
+/// 100 ms, in two processes (the bar's `stats` segment and the launcher's
+/// slider trigger), forever. Polling remains the fallback when the
+/// notification cannot be had.
+async fn watch_brightness(emit: impl Fn(LevelChange) + Send + 'static) {
+    std::thread::Builder::new()
+        .name("backlight-watch".into())
+        .spawn(move || {
+            let mut last = read_brightness();
+            if let Err(e) = brightness_notifications(&emit, &mut last) {
+                log::info!("[watch_brightness] no change notification ({e}); polling");
+            }
+            loop {
+                std::thread::sleep(std::time::Duration::from_millis(BRIGHTNESS_POLL_MS));
+                let cur = read_brightness();
+                if cur != last {
+                    last = cur;
+                    emit(LevelChange::Brightness(cur));
+                }
+            }
+        })
+        .ok();
+}
+
+/// Block on the first backlight's `actual_brightness` notification and push
+/// each moved value. Returns only on an error, for the polling fallback.
+fn brightness_notifications(
+    emit: &impl Fn(LevelChange),
+    last: &mut Option<i32>,
+) -> std::io::Result<()> {
+    use std::io::{Read, Seek};
+    use std::os::fd::AsRawFd;
+
+    let dir = std::fs::read_dir("/sys/class/backlight")?
+        .flatten()
+        .map(|e| e.path())
+        .find(|p| p.join("brightness").exists() && p.join("max_brightness").exists())
+        .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::NotFound, "no backlight"))?;
+    let mut file = std::fs::File::open(dir.join("actual_brightness"))?;
+    let mut buf = [0u8; 32];
     loop {
-        tokio::time::sleep(std::time::Duration::from_millis(BRIGHTNESS_POLL_MS)).await;
+        // sysfs re-arms the notification on a read from the start.
+        file.seek(std::io::SeekFrom::Start(0))?;
+        let _ = file.read(&mut buf)?;
+        let mut pfd = libc::pollfd { fd: file.as_raw_fd(), events: libc::POLLPRI | libc::POLLERR, revents: 0 };
+        let rc = unsafe { libc::poll(&mut pfd, 1, -1) };
+        if rc < 0 {
+            let err = std::io::Error::last_os_error();
+            if err.kind() == std::io::ErrorKind::Interrupted {
+                continue;
+            }
+            return Err(err);
+        }
+        if pfd.revents & libc::POLLNVAL != 0 {
+            return Err(std::io::Error::other("backlight fd invalid"));
+        }
         let cur = read_brightness();
-        if cur != last {
-            last = cur;
+        if cur != *last {
+            *last = cur;
             emit(LevelChange::Brightness(cur));
         }
+        // A held key writes in a burst; one read per 10 ms is plenty, and it
+        // bounds the loop should the attribute ever stay readable.
+        std::thread::sleep(std::time::Duration::from_millis(10));
     }
 }
 
@@ -369,6 +488,14 @@ async fn volume_subscription(
         }
     });
 
+    // A resubscribe follows a gap in which changes went unseen; nothing else
+    // reads the sink where this runs, so catch up now.
+    let cur = read_volume().await;
+    if cur != *last {
+        *last = cur;
+        emit(LevelChange::Volume(cur));
+    }
+
     let mut delivered = false;
     while rx.recv().await.is_some() {
         // Swallow the rest of the burst, then read once.
@@ -395,3 +522,31 @@ async fn volume_subscription(
 pub(crate) fn is_sink_event(line: &str) -> bool {
     line.contains(" on sink #") || line.contains(" on server")
 }
+
+#[cfg(test)]
+mod poll_tests {
+    use super::{module_reads, until_next_minute};
+    use chrono::TimeZone;
+
+    #[test]
+    fn a_single_stat_module_reads_only_its_own_field() {
+        assert!(module_reads(Some("clock"), "clock"));
+        assert!(!module_reads(Some("clock"), "volume"));
+        assert!(!module_reads(Some("clock"), "cpu"));
+        assert!(module_reads(Some("cpu"), "cpu"));
+        assert!(!module_reads(Some("cpu"), "memory"));
+        // The combined bar, and a name this build does not know, read all.
+        assert!(module_reads(Some("stats"), "wifi"));
+        assert!(module_reads(None, "battery"));
+        assert!(!module_reads(Some("tray"), "clock"));
+    }
+
+    #[test]
+    fn the_clock_wakes_just_past_the_minute() {
+        let at = |s, ms| chrono::Local.with_ymd_and_hms(2026, 10, 5, 21, 30, s).unwrap()
+            + chrono::Duration::milliseconds(ms);
+        assert_eq!(until_next_minute(at(0, 0)).as_millis(), 60_050);
+        assert_eq!(until_next_minute(at(59, 900)).as_millis(), 150);
+        assert_eq!(until_next_minute(at(30, 0)).as_millis(), 30_050);
+    }
+}