92afdc3bf6
Updates the ADR log, architecture, limitations, perf table and README to match what now exists, rather than what was expected. **The perf number was wrong and is now honest.** The M0 figure of 0.279 MB excluded SQLite entirely — the placeholder binary never referenced the crate, so dead-code elimination dropped it. With the daemon actually opening a database it is **1.364 MB**, of which bundled SQLite is ~1.1 MB. That is 1.1 MB of the 5 MB budget spoken for, leaving 3.8 MB for D-Bus, X11 and a UI toolkit. This is the first number that makes the section 5 budget feel like a budget rather than a formality, and it turns ADR-4's bundled-vs-system-linked choice into a real trade-off with a measured price on both sides. - ADR-4 marked accepted and implemented, with that trade-off left open. - ARCHITECTURE.md documents why `blocker-storage` is a sibling crate and not a submodule: the dependency direction is what keeps the zero-dependency purity proof automatic rather than a grep someone can route around. - LIMITATIONS.md drops the stale "storage has no I/O" shortfall and records the bundled-vs-system question instead, plus the fact that `LockState` still always evaluates to Normal. - README status table and layout updated. Co-Authored-By: Claude Code <noreply@anthropic.com>
124 lines
5.2 KiB
Markdown
124 lines
5.2 KiB
Markdown
# 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 | **1.364 MB** | 2026-10-01, M1 |
|
|
| 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 | — |
|
|
|
|
**What the 1.364 MB contains, and why it matters.** That figure is with bundled
|
|
SQLite linked in and the daemon actually opening a database — not the 0.279 MB
|
|
placeholder from M0, which was two `println!` lines and excluded SQLite entirely
|
|
through dead-code elimination.
|
|
|
|
So the honest read is: **bundled SQLite costs roughly 1.1 MB of a 5 MB budget.**
|
|
That leaves 3.8 MB for D-Bus, X11, and the UI toolkit. It is a real constraint
|
|
rather than a comfortable margin, and it is the first number that makes the
|
|
section 5 budget feel like a budget instead of a formality.
|
|
|
|
It also makes ADR-4 (rusqlite bundled vs system-linked) a genuine trade-off rather
|
|
than a settled one: system-linking would recover most of that 1.1 MB at the cost
|
|
of a runtime dependency on the host's libsqlite3. Recorded as an open question.
|
|
|
|
## 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). |