Flash keeps one sandboxed child per resident plugin. The process boundary is intentional: it gives each integration an explicit capability profile, independent crash recovery, bounded requests, and deterministic lifecycle. Official plugins are all native Rust binaries so that isolation no longer requires six language runtimes or host-injected SDK bridges.
The pre-migration installed-app snapshot had nine resident non-Rust children using roughly 84 MiB RSS / 117 MiB physical footprint; Flash plus all plugin children was roughly 188–190 MiB physical. The steady-state target after the Rust migration is at most 130 MiB physical for the same enabled plugin set.
On the maintainer’s Apple Silicon machine, a post-migration plugin-dev
direct-process run (2026-09-02, five cold samples per plugin) measured all 32
executables at 111,424 KiB aggregate median RSS. Per-plugin cold-initialize
p95 values were 1.8–7.6 ms and settled ping p95 values were 0.12–0.35 ms. This is a
deliberately conservative aggregate—on-demand plugins do not all coexist in a
normal session—and excludes the resident host and seatbelt wrapper.
The matching installed dev app measured 126.3 MiB physical footprint for Flash plus its 28 fully warmed resident plugin children, down from roughly 188–190 MiB before the migration. All 28 initialized concurrently in 219 ms, the slowest handshake took 26 ms, and none timed out. Treat these figures as a machine-specific reference, not a portable guarantee.
The five system-monitor plugins added on 2026-09-04 (cpu, memory, disks,
network, and power) measured 15,336 KiB aggregate median RSS across 20
samples each. Their cold-initialize p95 values were 2.09–2.30 ms and settled
ping p95 values were 0.19–0.22 ms. The same build’s 36 direct plugin binaries
measured 121,504 KiB aggregate median RSS across five samples. The installed
dev app settled at a 126.54 MiB median physical footprint for Flash plus its
33 resident children across five measurements (127.01 MiB maximum), remaining
inside the existing 130 MiB budget.
Those figures predate status-bound activation. The status-only monitors
(cpu, memory, disks, power, caffeinate, feed) now
spawn only while the enabled status bar shows one of their segments, so with
the bar off (the default) none of them runs until its command does.
Use these regression budgets when changing the runtime or an official plugin:
| Metric | Budget |
|---|---|
| direct cold initialize, p95 | 100 ms |
| idle ping round trip, p95 | 5 ms |
| synchronous query evaluator round trip | 50 ms protocol deadline |
| catalog decode/store | warn at 50 ms |
| host queued outbound transport | 256 frames / 20 MiB |
| host queued raw input / decoded frames | 256 entries / 20 MiB each |
| host outstanding requests / host RPCs | 64 / 64 |
| plugin outbound transport | 64 frames / 16 MiB |
| full installed steady-state footprint | 130 MiB physical |
The transport and outstanding-call limits are enforced admission caps, mirrored in the shared protocol specification. The timing and footprint budgets are regression tripwires. Hardware, signing, seatbelt compilation, TCC state, and debug versus release builds all affect absolute startup numbers.
Build the native-architecture binaries, then run the report-only benchmark:
./Scripts/benchmark-plugins.py --build --samples 5
./Scripts/benchmark-plugins.py --samples 10 --json > plugin-benchmark.json
./Scripts/benchmark-plugins.py --plugin browsers --samples 20
Scripts/measure-footprint.sh [sample-seconds] [log-window-minutes] is the
whole-system counterpart: it samples the running resident and every child
(CPU, idle wakeups, memory, descriptors, threads) and summarises watchdog
stalls, tap re-enables, and log lines per minute over the window. It is
read-only and expects one installed Flash to be running.
The plugin benchmark launches each official executable from a clean data directory, measures the protocol initialize round trip, lets post-initialize startup settle, measures an idle ping, samples RSS/thread count, then shuts the child down through stdin EOF. It does not enforce thresholds in CI; compare like-for-like builds and machines. The installed-app measurement is the final authority for total footprint because it includes real seatbelt profiles, resident/on-demand policy, catalogs, and host state.
Host logs provide phase data for real launches: install_ms,
resolution_ms, sandbox_ms, spawn_ms, initialize elapsed_ms, and
startup_total_ms. Catalog publication logs only when decode/store takes at
least 50 ms.
core:ax.changed never exhausts a briefly stalled child’s
outbound budget (see event coalescing). Deadlines include time spent awaiting
queue admission, and teardown invalidates work from the old child generation.
Stderr drains on its pipe callback without scheduling lifecycle work.core:ax.changed carries the bundle id the observer
recorded at install, never a LaunchServices lookup per notification. A
status that restates every segment unchanged schedules no re-evaluation.
The Rust runtime moves params out of the envelope instead of cloning it,
encodes each outbound frame once, and flushes frames queued together with
one write.spawn_blocking retain concurrency without multiplying idle
worker threads.browsers, windows
and kitty refresh on app lifecycle and focus events and on the
core:ax.changed notifications that can change their rows, ignoring the
rest — every keystroke’s AXValueChanged among them: windows on a window
retitled, created or destroyed; kitty on those plus its window focus
moving; browsers on a window retitled or created (web content posts
element creation and destruction on every DOM update). Relevant bursts —
a shell retitling around each command, a closing window’s subtree — refresh
once quiet for 300 ms, and at most ten seconds after they began. The host
observes an app’s AX only once it has been focused; changes it cannot see
catch up when focus or the flashlight next touches the app.tmux never re-reads its local servers on a timer. Each server a user’s
client is attached to is followed by one control-mode client (no-output,
ignore-size; it never starts a server or applies update-environment),
and one inventory read follows each burst of the notifications that can
change it — sessions and windows created, closed, renamed (automatic-rename
included) or selected, panes selected, added or closed, clients attaching,
switching or detaching — settled for 100 ms, at most a second after the
burst began. What tmux never notifies — a window’s working directory, its
command once automatic-rename is off, which client was used last — is
re-read on focus changes, app termination and flashlight opens. A client
attaching to a server nobody is attached to reaches no observer, so while
such a server runs, settled window retitles and creations
(core:ax.changed) refresh too; a kqueue watch on the tmux-$UID socket
directory catches servers starting or exiting. A socket whose probe fails
transiently is retried once per backoff step (5, 15, then 60 s). Remote
hosts in ssh_hosts, which no local notification reaches, keep a
five-second refresh.Do not trade the child-process boundary for an in-process ABI or shared daemon merely to reduce memory. That would collapse the sandbox and crash containment guarantees and create a second plugin architecture.