# Performance Two tables. **Targets** are from CLAUDE.md section 5 and are fixed. **Results** is what was actually measured, and it is currently almost empty because almost nothing is built yet. The separation is deliberate. A single table mixing aspiration with measurement invites reading the aspiration as the result. ## Targets (CLAUDE.md section 5) | Metric | Android | Windows / Linux daemon | |---|---|---| | Idle CPU | ~0% (no wakeups without events) | ~0%, ≤ 1 wakeup/min when idle | | Idle RAM | ≤ 30 MB PSS | ≤ 15 MB RSS (daemon), UI process exits when its window closes | | Block latency (foreground change → block shown) | ≤ 150 ms | ≤ 150 ms | | Daemon binary | — | ≤ 5 MB stripped | | APK size | ≤ 10 MB (release, R8) | — | | Battery | no wakelocks outside active enforcement | — | The release profile in the workspace `Cargo.toml` carries `opt-level = "z"`, LTO, `codegen-units = 1`, `panic = "abort"`, and `strip = true`, which is what makes the size budget reachable. `scripts/size-check.sh` enforces it in CI from the first commit. ## Results Measured on: Ubuntu 26.04 LTS, KDE Plasma 6.6.6, 6 CPUs. Toolchain: rustc 1.98.1. | Metric | Target | Measured | When | |---|---|---|---| | Daemon binary (stripped, release) | ≤ 5 MB | **0.279 MB** | 2026-10-01, M0 | | Block latency | ≤ 150 ms | not measured | — | | Idle CPU | ~0% | not measured | — | | Idle RAM (daemon) | ≤ 15 MB RSS | not measured | — | | Idle wakeups | ≤ 1/min | not measured | — | | APK size | ≤ 10 MB | not measured | — | The binary figure is real but nearly meaningless right now: the daemon is two `println!` lines. The meaningful measurement comes in M2-Linux once enforcement exists, and the real risk is at that point — D-Bus, X11, and a UI toolkit are where 5 MB would actually be threatened. ## How to measure ### Linux daemon ```sh # Binary size — the gate CI runs scripts/size-check.sh # Idle RSS ./systemctl --user start blocker ps -o rss= -C blocker-daemon | awk '{printf "%.1f MB\n", $1/1024}' # Idle CPU — sample over a minute, not instantaneously pidstat -p "$(pidof blocker-daemon)" 10 6 # Block latency — timestamp on the compositor event, timestamp on the window map # TODO: script this in M2-Linux once the KWin backend emits both. ``` `perf stat -e context-switches,cpu-migrations` is useful for the wakeup count. If idle wakeups exceed one per minute, a timer is firing that `next_wakeup` did not ask for — that is the first thing to look for. ### Windows Process Explorer, or ETW. The relevant counters are working set (RAM) and the process's CPU over a sampled window. ### Android ```sh adb shell dumpsys meminfo vu.valiq.blocker # PSS, must be <= 30 MB adb shell dumpsys batterystats vu.valiq.blocker adb shell am profile start --sampling 1 vu.valiq.blocker /data/local/tmp/p ``` Perfetto for the wakeup analysis. A wakeup from the blocked package when nothing is happening is a bug in `next_wakeup` accounting, and battery stats will show it long before a user complains. ## Why idle is hard, and what breaks it The budget is zero idle wakeups, so the common causes are worth naming. 1. **A timer that fires to check something.** The engine's job is to make these unnecessary. Every condition that contributes a `next_wakeup` boundary is a deliberate, named commitment. A new timer that does not correspond to a boundary is a bug. 2. **Conditions that should contribute no boundary but do.** Wi-Fi and location schedules are changed by OS events — NetworkManager's callback, the geofencing API. Arming a timer for them would be a wakeup for nothing. `wifi_schedule_declares_no_time_wakeup` pins this. 3. **The UI holding a GL context.** A resident graphics context costs both RAM and compositor wakeups. This is why the daemon and the block window are separate processes: the window exits when the block lifts, and nothing holds a context while idle. 4. **Unbounded `Vec` growth in the engine.** `blocked_apps` is rebuilt per evaluation. With a handful of schedules it is small, but a schedule set in the thousands would allocate per focus event. `next_wakeup` returning `None` means the daemon is asleep and this does not matter, which is exactly why the budget and the design reinforce each other. 5. **Storage polling.** Nothing in the design polls the database. If a future change introduces it, it will show up here first. ## Recording a result Add a row to the table above with the date and the tool used. Then note in the commit message whether the change touches the hot path — section 9 requires a re-measurement when it does. A milestone is not done until its acceptance checks pass **and** its numbers are recorded here (section 7).