5073c7321d
The documentation set CLAUDE.md sections 2, 6, 8 and 10.1 call for, written as records of decisions actually made rather than as a description of intent. **DECISIONS.md** — 17 ADRs. Two are deliberately left open and say so: ADR-6 (the desktop UI toolkit, which section 3 requires be decided on measurement and which needs the owner's call, partly because Slint is GPL-3.0) and ADR-17 (the licence, which is a legal commitment nobody should make on the owner's behalf). Where CLAUDE.md gives a reason for a decision, that reason is recorded; where it does not, the file says the reason is unrecorded rather than inventing one. **LIMITATIONS.md** — all eight section 6 limits plus the section 1.5.4 safety guarantees, stated as facts about the operating systems. The words "unbypassable", "tamper-proof", "guaranteed" and "secure" appear nowhere, per sections 6.1 and 6.7. Ends with a separate list of shortfalls in the *current code* — storage has no I/O, the golden tables cannot yet be parsed, LockState always returns Normal — so a known gap in this implementation is never mistaken for a platform limit. **ARCHITECTURE.md** — the layering, the purity contract and its mechanical enforcement, why `next_wakeup` must be armed over inactive schedules too, the overnight weekday-of-start rule, and the end-to-end flow for one block event. **PERF.md** — targets and results as separate tables, so an aspiration is never read as a measurement. The one real number recorded (0.279 MB stripped daemon) is labelled as nearly meaningless, since the daemon is two print statements. Lists the common causes of idle-wakeup regressions. **PERMISSIONS.md** — per platform, what each permission is for and what degrades without it. Flags the two invasive ones (Android accessibility, extension host permissions) rather than burying them, and records that unprivileged eBPF is *assumed* unavailable here because the sysctls were never read. **TESTING.md** — the golden-table contract, the add-the-case-first rule, and why clock tampering needs code fixtures instead of data. Co-Authored-By: Claude Code <noreply@anthropic.com>
115 lines
4.7 KiB
Markdown
115 lines
4.7 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 | **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). |