guide/07-footprint.md
Footprint
Standing: the app's table and Phase 0's table are measured (Phase 0: ../review-0.md). Phase 3 read Live Activity under a stream as first built (../review-3.md); the readings for its 4-a-second redraw are owed.
Some customers will measure what this app costs them. The aim is to be able to answer in numbers, for the app and for the sensor, with developer mode absent, off, and in use.
The app today
Release build, Apple M2 Max, macOS 15.6, synthetic data, idle with no pointer. Measured with strobe 0.26.0 on 2026-11-10 (README, Footprint; cycles/c2.expressive-range/review-3.md).
| Reading | |
|---|---|
| Binary | 23.2 MB |
| Memory at rest, window on the first page | 62 MB |
| Memory after every Inventory page was shown once | 116 MB |
| Memory, tray only | 21 MB |
| CPU, idle | 0.005% of one core |
| Wake-ups per second | 0.9 |
Two things these readings already say about developer mode:
- Showing a page costs more than carrying its code. The 54 MB between the second and third rows is almost all GPU memory for pages that were drawn, and it levels off after the first visit.
- Idle really is idle. Code that is not running costs no CPU.
What Phase 0 read (2026-11-11)
The same recipe, four states, two interleaved rounds on an idle Mac. A reading starts only after a minute without input and is taken again if anyone touches the machine.
| State | What it tells us | Read |
|---|---|---|
| lean build | the floor: no developer code in the binary | 23.2 MB binary; 61.6 and 63.0 MB at rest; 0.004–0.006% of a core |
| full build, developer mode off | what carrying the code costs | 24.3 MB binary (+1.07 MB); 61.0 MB at rest (quiet); 0.004% |
| full build, on, nothing opened | what the Dev group itself costs | 61.5 MB (quiet); 0.005% |
| full build, a developer page shown | what looking costs | 65.3 and 65.0 MB with Components shown; 0.005% |
| full build, allowed, turned off in the tray | what the toggle buys | 62.8 and 61.8 MB; 0.004–0.006% |
The difference between the first two rows is what the lean build buys: 1.07 MB of binary, and nothing at rest that two readings can resolve (one build's repeated readings differ by up to 2.7 MB). The estimate held, so the plan stands.
What Phase 3 read (2026-11-11)
The setup:
- Live Activity open, with a synthetic
coding-sessionstream at 10, 100 and 1,000 events a second. - The full release build, the window on the Studio Display.
- The same idle rule, and the screen unlocked. A locked screen draws nothing, so it reads low.
As first built, the page redrew once for every batch that arrived:
| State | What it tells us | Read |
|---|---|---|
| Live Activity, 10 a second | the cost of an open stream page | 107.5 MB, 5.67% of a core (taken while a build ran), 269 wakes a second |
| 100 a second | 103.4 MB, 27.9%, 1,061 wakes a second | |
| 1,000 a second | 112.2 MB, 33.9%, 1,047 wakes a second | |
| left, then read for 60 s | whether closing the page ends the feed | 95.1 MB, 0.003%, 0.8 wakes a second, 6 threads (8 while open) |
What this says:
- Closing the page ends the feed. The lifecycle rule holds from outside: no CPU, no wake-ups, two threads fewer. The memory the page used stays with the allocator and GPUI's atlases, as after c2's page walk.
- The cost is the drawing, not the stream.
- The main thread accounts for nearly all of the CPU: 26–32% of a core at 100 and 1,000 a second.
- One redraw of the page costs about 10 ms. A build that gathered arrivals for 50 ms read 9.6% main-thread busy at 10 a second, which is ten redraws a second.
- The stream core alone, with the window covered so nothing draws, costs 0.6%, 1.7% and 7.05% of a core at the three rates. That covers ingest, presence, the tally and the index, for every record.
- Memory: 92–112 MB while the page is open, against 62–74 MB at rest. The buffer's bound is 20,000 events or 32 MiB.
- The fix, not yet read: the page now redraws at most 4 times a second, the monitor TUI's cadence.
scripts/footprint/live-activity.shtakes those readings. It needs about ten minutes of an idle, unlocked Mac.
The recipe
- Build the release binary. Copy it under a distinct file name.
- Launch it with
QPOINT_DATA=synthetic. Wait 5 s. strobe record pid:<pid> --for 60s.- Cross-check with
footprint -p <pid>andps -o rss=,pcpu=,cputime= -p <pid>. - Memory is strobe's
memory.app.private. Record the display the window was on; GPU-backed memory scales with it.
QPOINT_SPIKE_WALK_AFTER=<s> shows every Inventory page once, for the "after every page" row.
The recipe is now a script:
scripts/footprint/measure.sh <label> <binary>takes one reading. It waits for an idle, unlocked Mac, records with strobe, checks that the process survived the minute, and retakes if anything touched the machine.scripts/footprint/live-activity.shreads Live Activity at each rate, and again after leaving it.
The sensor's side
The app is not the only one that pays. From qcontrol main (2026-11-10):
- Every subscriber to the event stream makes the monitor re-scan installations and publish two snapshots to all subscribers (throttled to two in 500 ms).
- Each subscriber has a 1,024-record buffer in the hub. A slow reader is cut off; it does not slow the sensor.
- Reading the dashboard's API means the dashboard subsystem is on, and it projects the stream into SQLite as it goes.
- The TUI's own work happens only with
--tui: a snapshot of the monitor's state up to four times a second, an index of every tracked process, and a resource sample once a second.
The rule we would keep for anything new the sensor serves to this app: no subscriber, no work.
What we cannot measure from here, and ask for: the monitor's CPU and memory with no subscriber, with one subscriber idle, and with one subscriber during a busy session. The same recipe works against the monitor's pid.
What we will say
| If the readings show | The sentence |
|---|---|
| the lean build, as expected | "The production build contains no developer tooling." Read: scripts/check-lean.sh (graph and strings), Phase 0. |
| developer mode off costs nothing measurable | "Where it is allowed, it does no work until it is switched on." Read: Phase 0, rows 2, 3 and 5. |
| an open stream page has a bounded cost | "A page that reads the stream costs at most N MB and M% of a core while it is open, and nothing once it is closed." Read: the second half, Phase 3 ("left": 0.003%, the feed's threads gone). N and M wait for the 4-a-second readings. |
No sentence is used before its reading exists.