Board session 2026-10-12: picker overhead, matched pairs, motion trial, tap-apply
One SSH-driven session on the reserved board (single operator, flock /tmp/k230-board.lock held for the whole
session). Evidence class: software-injected touch events through the verified virtual device named
K230 injected touchscreen (the physical Goodix node was never written), journal and Rust trace timing, and native
grim captures taken outside every timing pass. No finger, no camera, no glass observation. Tasks 14.4 and 18.4
(real finger) were not attempted.
Worktree /home/jadams/tmp/k230-theme-board-2026-10-12, branch theme/board-session-2026-10-12, base e7a47f2f.
Session window 2026-10-12 05:44 to 07:20 UTC.
What is installed and what was compared
| Item | Identity |
|---|---|
| System (booted = current, never changed) | /nix/store/zvxmd93gnn6n3vpk1mdxgajqnb8h1gkm-nixos-system-nixos-26.11.20260919.20b1ddd, kernel 7.3.0-rc5 |
Installed Rust shell (candidate; source faae7298: 18.2 motion, 13.2 counters, 13.3 admission gate, 17.2 row repaint) | /nix/store/ybpnyqmcn672n7x45nx9ybzprljgdhfg-k230-shell-rust-riscv64-unknown-linux-gnu-0.1.0 |
Repaired baseline for 13.4/11.3/17.3 (8cik; 12.1 fix, no 13.3 gate, no 17.2 row repaint) | /nix/store/8cikjd811d7b7hsdhn7nnb3lnipf3vwp-k230-shell-rust-riscv64-unknown-linux-gnu-0.1.0 |
Motion baseline for 18.3 (si9vd; pre-18.2, has 13.3 and 17.2) | /nix/store/si9vd9hhdcb8kzppfsfq9id8kaqhnmwk-k230-shell-rust-riscv64-unknown-linux-gnu-0.1.0 |
| Compositor / helper interpreter | /nix/store/q96sk40q7j5v9f0ygxf2wx3yk7nzixqs-sway-unwrapped-riscv64-unknown-linux-gnu-1.12/bin/sway, /nix/store/v189xydz6qkcd4cbkixcmv91w8hbc560-python3-riscv64-unknown-linux-gnu-3.14.7/bin/python3.14 |
| Catalog | 22 themes, 4 backgrounds (still images), 568x1232 |
8cik was garbage-collected on the board. It was shipped with nix-store --export | xz | ssh ... nix-store --import
(its closure was otherwise present; only the path itself was missing) and pinned with gcroots
/nix/var/nix/gcroots/k230-theme-baseline-8cik and .../k230-theme-motion-si9vd (left in place).
Runtime references: nix-store -q --references of 8cik, si9vd and the installed ybpn ELF produce the same
seven-path set (md5 adb9f78378c5b1747e6d1f7ce94699dc for all three): glibc, glib, cairo, pango, gcc-lib,
librsvg, libxkbcommon. The ELF swap is therefore a like-for-like comparison of the binary.
The two builds do not draw the same picker. The 8cik captures show perspective-skewed cards with a blue
outline; the installed build draws flat cards with page bars (see 13.4/vis/). Frame-cost differences below
therefore include the visual-polish change as well as the row-repaint mechanism.
Starting state: no durable theme pointer (departs from the brief)
The board had been reflashed or reset: /home/shell/.local/state/omarchy/current held only .activation.lock; the
shell ran the bundled default generation and k230-theme list reported active: {id: null, generation: null}.
In that state the picker’s background row stays at “Loading backgrounds” indefinitely (reproduced 12 s and 22 s
after opening; theme_ui.rs only loads the background detail when list.active.id is set). This is the same symptom
docs/evidence/theme-picker/working-set/README.md recorded, and it makes the 13.1 gates (both rows resolved) unmeetable.
To get a comparable board I applied the bundled catppuccin theme once through the helper as the shell user
(k230-theme preview then activate --expected-generation; durable pointer to generation 1af520f2..., selected
background 1-totoro). That is the fixed generation for every run below. At the end I removed that state and
restarted shell.service to put the board back to its original pointer-less default (see Final state). Applying as
root fails with appearance peer has wrong UID / fanout prepare failed and rollback was not acknowledged; this
was the expected UID check, not a regression.
Method (all runs)
tools/run.py (board, root, board Python) swaps only the final ELF through a runtime drop-in
/run/systemd/system/shell-ui.service.d/92-theme-session.conf (wrapper copy with the last exec line replaced),
arms an independent systemd-run --on-active restoration timer, restarts theme-helper and shell-ui
(so every run starts from fresh processes and the same active theme index 0 and background index 0), opens
Settings, and then runs tools/theme-picker-profile.py capture (the 13.1 workload: 2 s baseline, first open 10 s,
6 + 4 swipes of 240 px / 200 ms with 1.2 s rest, 15 s idle, back, warm open 5 s, 6 warm theme swipes, 15 s idle;
no activation). The finally-block removes the drop-in, reloads and restarts the service; the timer is stopped.
The profile harness gates every run: verified virtual device, exact executable identities, PID/start-tick
identity, active generation unchanged for every 0.5 s sample, rows resolved and reviewed (I reviewed native captures
of both rows before the first run), process-cold first opening (journal shows no earlier touch for that PID),
no activation. All 31 runs passed every gate; each reduced report is in 13.4/reports/.
tools/ holds the executed drivers (run.py, gestures.py) and the host analysers; host/ holds the shell
wrappers that invoked them. Raw private captures stay in ~/tmp/k230-theme-board-run-2026-10-12/ (46 MB, not
committed); their SHA-256 values are in raw-sha256.json and per-run identities in runs-meta.json.
Typical invocation (UTC start times in runs-meta.json):
BOARD_PYTHON=/nix/store/v189xydz6qkcd4cbkixcmv91w8hbc560-python3-riscv64-unknown-linux-gnu-3.14.7/bin/python3
systemd-run --pipe --wait --collect --unit=k230-theme-sess-NAME --property=RuntimeMaxSec=400s \
--setenv=PATH=/run/current-system/sw/bin:/run/current-system/sw/sbin \
$BOARD_PYTHON $B/run.py run --label NAME --kind baseline|candidate --exe ELF --mode off|counters|trace --out $B/runs/NAME
python3 tools/theme-picker-profile.py reduce RAW --rust-trace TRACE --output REPORT # host
python3 tools/theme-picker-profile.py compare BASE_REPORT CAND_REPORT # host, gates then deltas
One harness change was needed and is committed with a test: reduce called any journal line whose Rust elapsed
clock was lower than the previous line “a process restart”. Lines from different threads reach the journal a few
milliseconds out of order, and this fired on about half the runs (a 7 ms frame-done reordering). It now tolerates
250 ms (CLOCK_JITTER_MS); a real restart changes the PID and is still caught by the PID gate.
python3 -m unittest discover -s tests -p test_theme_picker_profile.py: 51 tests pass.
Task 13.2 / 13.4: observer cost first
Same installed ELF, same workload, fresh restart before each; three OFF, three counters-only, one trace run
(ov-*, 05:48 to 06:05 UTC). Pooled medians (13.4/overhead-pooled.json; harness pairwise output in
13.4/harness-overhead-*.json):
| Pooled median | OFF (3) | counters (3) | trace (1) |
|---|---|---|---|
| warm theme commit-gap median, ms (each run) | 112.5 (111-141) | 129 (115.5-143) | 142 |
| warm theme Rust CPU % | 69.1 (54-71) | 57.4 (56-66) | 59.6 |
| background-swipe Rust CPU % | 11.1 (9.9-11.4) | 14.4 (10.6-16.1) | 13.8 |
| background commit-gap median, ms | 39 | 39 | 38 |
| idle Rust CPU % (15 s windows) | 0.51 | 0.59 | 0.94 |
| RSS at end, MiB | 86.0 | 85.1 | 87.9 |
Run-to-run spread with the recorder off is as large as any recorder effect on the theme row (the helper’s speculative preview work lands in different swipes each run: 6 to 14 preview requests). The measurable recorder cost is about +0.08 percentage points of Rust CPU when idle and about +3 points during the cheapest (background) swipes; trace mode adds about +0.4 points idle. No difference in background commit-gap median and no memory increase. Judgement: small enough to interpret counters, with that caveat; they are indicative, and absolute CPU figures from instrumented runs are slightly high. The pairs below were run twice per build: trace mode (event ring on both builds, so spans compare) and recorder off (clean CPU and cadence).
Task 13.4 / 11.3 / 17.3: three matched pairs, alternating order
Order of execution (UTC from runs-meta.json): trace mode p1 base, cand (06:07); p2 cand, base; p3 base,
cand; then recorder off p1 cand, base (06:21); p2 base, cand; p3 cand, base. Fresh processes each run; the
first opening of every run is journal-verified process-cold (disk and wallpaper caches were not cleared, so
this is not disk-cold) and the second is warm. Gates passed for all six pairs (13.4/compare-*.json:
comparable: true).
Recorder off (CPU, cadence, memory) — base / candidate per pair, then median
| Metric | pair 1 | pair 2 | pair 3 | median |
|---|---|---|---|---|
| Warm theme commit gap, median ms | 137.5 / 108 | 135 / 117 | 141 / 114 | 137.5 / 114 |
| Warm theme commit gap, p95 ms | 183 / 160 | 173 / 177 | 165 / 141 | 173 / 160 |
| Cold theme commit gap, median ms | 143 / 118 | 143 / 123.5 | 140.5 / 121 | 143 / 121 |
| Cold theme commit gap, p95 ms | 177 / 168 | 175 / 184 | 181 / 182 | 177 / 182 |
| Background commit gap, median ms | 80.5 / 39 | 93 / 39 | 87 / 38.5 | 87 / 39 |
| Background commit gap, p95 ms | 142 / 95 | 154 / 90 | 155 / 89 | 154 / 90 |
| Background commits in 4 swipes | 30 / 59 | 31 / 58 | 31 / 60 | 31 / 59 |
| Rust CPU %, warm theme swipes | 77.8 / 63.0 | 79.4 / 68.9 | 80.5 / 75.2 | 79.4 / 68.9 |
| Rust CPU %, cold theme swipes | 81.0 / 60.8 | 81.0 / 72.8 | 80.2 / 63.7 | 81.0 / 63.7 |
| Rust CPU %, background swipes | 35.1 / 9.8 | 44.4 / 18.1 | 43.9 / 11.0 | 43.9 / 11.0 |
| Rust CPU %, idle windows | 0.49 / 0.49 | 0.56 / 0.52 | 0.59 / 0.55 | 0.56 / 0.52 |
| Helper cgroup CPU %, warm theme swipes | 0.06 / 1.01 | 0.06 / 1.67 | 0.06 / 0.71 | 0.06 / 1.01 |
| Rust RSS at end of warm idle, MiB | 78.8 / 86.0 | 80.7 / 88.1 | 82.5 / 86.7 | 80.7 / 86.7 |
| Rust RSS growth over the run, MiB | 27.6 / 34.9 | 29.8 / 36.8 | 31.6 / 35.3 | 29.8 / 35.3 |
| Helper RSS, MiB | 23.9 / 24.0 | 24.1 / 24.0 | 24.0 / 24.0 | 24.0 / 24.0 |
| Commits in the two 15 s idle windows | 0 / 0 | 0 / 0 | 3 / 0 | 0 / 0 |
| Theme preview requests to the helper | 4 / 14 | 5 / 15 | 5 / 14 | 5 / 14 |
| Overlay pre-renders completed (all while no touch held) | 4 / 6 | 4 / 9 | 4 / 7 | 4 / 7 |
All Rust CPU in the sampled windows is on the main k230-shell-rust thread (per-thread sampling with start-tick
identity; short-lived worker threads fall between 0.5 s samples, so worker CPU is only visible through the trace
spans below). Opening: first open 6 commits and about 4.4% Rust CPU for both builds; warm open 3 commits for
both.
Trace mode (spans, presentation) — base / candidate per pair, then median
| Metric | pair 1 | pair 2 | pair 3 | median |
|---|---|---|---|---|
Warm theme presented gap, median ms | 140.2 / 105.2 | 127.0 / 105.3 | 133.6 / 122.6 | 133.6 / 105.3 |
Warm theme presented gap, p95 ms | 186 / 166 | 160 / 178 | 163 / 165 | 163 / 166 |
Background presented gap, median ms | 84.0 / 38.3 | 65.6 / 38.3 | 65.8 / 38.3 | 65.8 / 38.3 |
render_total per frame, warm theme, median ms | 133.3 / 98.6 | 124.8 / 96.9 | 128.6 / 104.3 | 128.6 / 98.6 |
render_total per frame, background, median ms | 76.9 / 3.2 | 61.8 / 3.1 | 60.4 / 3.2 | 61.8 / 3.2 |
| Full scene rebuilds, warm theme swipes | 59 / 8 | 65 / 16 | 65 / 17 | 65 / 16 |
| Full scene rebuilds, cold theme swipes | 61 / 19 | 60 / 20 | 61 / 19 | 61 / 19 |
| Full scene rebuilds, background swipes | 28 / 0 | 30 / 0 | 30 / 0 | 30 / 0 |
| Scene-rebuild wall in warm theme swipes, ms | 7185 / 1040 | 7877 / 2002 | 7780 / 2215 | 7780 / 2002 |
| Row repaint median per frame (candidate only), theme / background, ms | - / 92.8 / 40.3 | - / 93.2 / 39.6 | - / 88.1 / 38.1 | 92.8 / 39.6 |
| Thumbnail hash CPU in warm theme swipes (all off the main thread), ms | 360 / 105 | 37 / 264 | 112 / 219 | 112 / 219 |
| Rust RSS at end, MiB | 89.3 / 84.1 | 75.6 / 88.9 | 76.4 / 87.0 | 76.4 / 87.0 |
The first baseline run (tr-p1-base, 89.3 MiB) is an outlier; the other two baseline trace runs end at 75.6 and
76.4 MiB, so the trace pairs are noisier on memory than the recorder-off pairs, but the candidate is higher in
five of six comparisons (+13 and +11 MiB in two trace pairs).
Speculative admission and in-flight work (trace pairs)
| pair 1 | pair 2 | pair 3 | |
|---|---|---|---|
| Baseline admissions in total / inside a carousel drag | 3 / 1 | 2 / 0 | 2 / 0 |
Candidate admissions in total / by shell gesture phase held + moving | 13 / 0 | 10 / 0 | 13 / 0 |
| Candidate admissions at rest, inside a swipe window (touch plus the 1.2 s after) | 11 | 8 | 11 |
Candidate completions (in-flight work finishing) by shell phase held / moving | 1 / 0 | 1 / 1 | 0 / 0 |
| Candidate completions that land inside a carousel touch window | 7 | 2 | 7 |
No new speculative admission starts during either row’s motion in the candidate (0 of 36 in held/moving).
The gate defers them to the 1.2 s rest after each swipe, and the work admitted there (a helper preview of about
230 to 400 ms, a neighbour warm) is still in flight when the next swipe starts: most completions fall at the next
touch-down. The candidate admits four to six times as many speculative requests as the baseline (13 / 10 / 13 vs 3 / 2 / 2
admissions in trace mode; helper preview requests 12 to 15 vs 4 to 5 in trace mode and 14 to 15 vs 4 to 5 in off mode) because of the neighbour queue, and the helper
cgroup sits at 1 to 2% (24% in one cold-swipe window) where the baseline’s is 0.06%.
Candidate counters by shell gesture phase (trace runs; count, wall ms, thread CPU ms)
| Kind | moving pair 1 / 2 / 3 | held pair 1 / 2 / 3 |
|---|---|---|
| scene_rebuild | 23, 2999, 2203 / 33, 4836, 3013 / 33, 4475, 3325 | 4 / 4 / 3 |
| picker_row_repaint | 53, 4846, 3909 / 32, 3707, 2254 / 40, 3472, 2795 | 36, 2957, 2483 / 30, 3065, 2056 / 34, 2815, 2360 |
| render_total | 121 / 111 / 116 frames | 42 / 36 / 39 frames |
| thumbnail_request (hash and resolve each equally many) | 28 / 48 / 40 | 8 / 12 / 10 |
Full per-phase span tables are in 13.4/trace-phase-summary.json and 13.4/pairs-summary.json.
Native stills and sampled frames (not a timing pass)
13.4/vis/baseline-open-theme-background.png and .../candidate-...png: open, after one theme swipe, after one
background swipe (injected 240 px / 200 ms), native grim -s 0.5. Both rows respond in both builds (theme row
catppuccin to flexoki-light, background row Totoro to Omarchy). 13.4/browse-base.mp4 and browse-cand.mp4 encode
the sampled frames (about 17 frames in 16 s, tools/sample-grim-frames.sh with half-scale grim, timestamps in the
JSON). They are sampled screencopy, not smooth video: the baseline clip catches the background row in motion
(Blue eye frame); the candidate clip shows theme-row motion but its background swipes fall between samples (that row’s
response is shown by the still). I viewed the stills and sampled frames.
Outcome for 13.4, 11.3 and 17.3
- The candidate improves cadence and CPU: warm theme commit-gap median 137.5 to 114 ms (-17%), background 87 to 39 ms (-55%), background frames double, render per frame 129 to 99 ms (theme) and 62 to 3 ms (background), full scene rebuilds fall by 69 to 100%, Rust CPU in swipes falls by 10 to 33 points, idle CPU is unchanged and idle windows stay quiet (candidate: 0 commits in every pair run; baseline: 0 except 2 and 3 stray commits in two runs).
- It regresses memory: +6.0 MiB (about +8%) at the end of the warm idle window in all three recorder-off pairs (+4.2, +7.2, +7.5); +10.7 MiB median in the noisier trace pairs. Helper RSS is unchanged. The cause is UNVERIFIED: the candidate does about three times as many speculative prepares and about 1.5x the overlay pre-renders, but no retained-byte measurement was taken.
- Repaint still dominates. The theme row still costs about 93 ms per frame to repaint (candidate), so warm swipes
run at about 9 to 10 frames per second (p95 gap 160 to 177 ms, not improved), and the candidate still does 23 to 33 full
scene rebuilds (3.0 to 4.8 s of main-thread wall at about 130 ms each) while
moving, coinciding with 28 to 48 thumbnail requests in that phase (17.2 keeps full invalidation for thumbnail completion). - Next measured correction (named, not done here): (1) turn thumbnail completion into row-local damage rather than a full scene rebuild, and (2) cut the theme-row repaint cost (about 93 ms for one row; cache the scaled card layer and translate it) before further scheduling work; (3) take an RSS attribution (retained pre-render and thumbnail bytes versus the speculative-request count) before changing the neighbour queue.
Because warm-swipe p95 does not improve and memory does not hold, 11.3 stays open. The comparison tasks 13.4
and 17.3 were performed with the evidence above, so they are ticked in tasks.md; the push and site-deployment
check in 17.3 and 14.4 / wider 13 and 15 gates are not covered.
Task 18.3: guarded motion trial (open)
18.3/: eight runs (4 per build, 06:34 to 07:00 UTC, trace mode, alternating order mo theme, ca theme, ca bg, mo bg, ca theme, mo theme, mo bg, ca bg), each with two repetitions of five injected programs (tools/gestures.py) on one
row, a fresh Settings to Themes opening before every gesture, 2.5 s settle, then a native grim -s 0.4
capture (outside the gesture window). 80 gestures; the generation and process identities were checked after every
one and never changed.
- flick: 300 px in 80 ms, release at speed.
- slow: 300 px in 1.5 s, release at the last sample.
- hold-release: 240 px in 300 ms, hold 700 ms, release.
- reversal: 200 px left in 200 ms, then 150 px back in 150 ms, release.
- interrupt: flick, then a new touch 300 ms after release drags 30 px against the coast.
The shell records no per-frame carousel position or velocity, so no position or velocity curve is published.
input_motion carries counters, commit and presented carry no carousel state, and the journal logs only finger
positions. Deriving a curve by replaying the touches through the host model would be a model, not a board result.
What exists is the injected finger path, the frame commits and the settled item:
18.3/gesture-timelines.png: injected finger x against time with frame-commit ticks for both builds.18.3/gesture-release-to-rest.png: milliseconds from the final release to the last commit.18.3/gesture-summary.json: per-gesture counts and gaps (80 rows, medians over four repetitions).18.3/labels-{mo,ca}-{theme,bg}.png: the label strip of each native capture. I read the final item from them.
| Gesture | Row | Frames after release (base / cand) | Last commit after release, ms | Final item (base / cand), ordinal |
|---|---|---|---|---|
| flick | theme | 14 / 9.5 | 1684 / 1120 | matte-black 11 / lupine 10 (4 of 4 each) |
| slow | theme | 7 / 7 | 626 / 704 | ethereal 2 / everforest 3 |
| hold-release | theme | 4 / 5 | 321 / 419 | catppuccin-latte 1 / 1 |
| reversal | theme | 8 / 12.5 | 298 / 547 | catppuccin 0 / 0 |
| interrupt | theme | 8 / 8 | 805 / 781 | kanagawa 7 (3 of 4; gruvbox 5 once) / hackerman 6 (4 of 4) |
| flick | background | 7.5 / 13 | 237 / 506 | Omarchy 3 / Omarchy 3 |
| slow | background | 7.5 / 10.5 | 245 / 383 | Omarchy 3 / Omarchy 3 |
| hold-release | background | 4 / 7 | 174 / 360 | Waves 1 / 1 |
| reversal | background | 7.5 / 13 | 263 / 487 | Totoro 0 / 0 |
| interrupt | background | 8.5 / 13.5 | 440 / 745 | Omarchy 3 / Blue eye 2 |
Observations only: both builds are repeatable for identical injected input; commit-gap medians are 38 to 65 ms on the background row in both builds (reversal 41 versus 57 ms in the candidate’s favour) and 100 to 125 ms on the theme row in both (reversal 39 versus 47 ms), so the theme row is drawn at 8 to 10 frames per second in both builds; the candidate commits more frames after release on the background row (13 versus 7.5 after a flick); the candidate’s slow drag settles one item farther (3 versus 2) and its interrupt absorbs one item more; the settle tail after release is longer on the background row in the candidate. Whether that is better direct-tracking, flick distance or settling is exactly 18.4’s finger question, and no curve here supports a claim.
18.3 stays open. To publish position and velocity curves the shell needs a bounded per-frame carousel
position/velocity record in the Rust trace (one value per row per commit). I did not add instrumentation in this
session.
Task 10.7 and 6.6: warm tap-apply, dark and light (two trials)
10.7-6.6/ holds the sanitized journal excerpts (journal-trial-{1,2}.txt; board local time CDT = UTC-5, identifiers
replaced by labels), the marker logs, picker-dark-light-dark.png (five native captures: open dark, light
selected, light applied, dark selected, dark applied) and panel-bottom-full-resolution.png.
Each trial: cold systemctl restart theme-helper shell-ui, Settings to Themes, 15 s for the neighbour warm,
hold-release to the neighbouring light theme (catppuccin-latte), injected centre tap to apply, a fast swipe back to
catppuccin, injected centre tap to apply. Trial 1 started 07:11:11 UTC, trial 2 07:12:45 UTC. The active generation
path hash was identical before and after both trials (1d6e6815e6151009), i.e. the dark generation was restored.
| Light apply (t1 / t2) | Dark apply (t1 / t2) | |
|---|---|---|
optimistic-apply shown ms | 201.2 / 204.4 | 156.1 / 163.2 |
prerendered, reason | false, content-changed (both) | false, content-changed (both) |
stage adopt / wallpaper / overlay / flush ms | 0.5 / 2.6 / 85.7 / 0.0; 0.4 / 2.8 / 86.2 / 0.9 | 0.4 / 7.5 / 55.1 / 0.8; 0.4 / 11.5 / 58.8 / 0.8 |
| tap-up to shown frame (journal Rust clock), ms | 566 / 568 | 511 / 510 |
activate_generation total, ms; prepare_phase; warm | 230.6 / 242.0; 3.1 / 0.1 ms; 2 | 192.9 / 195.0; 0.2 / 0.1 ms; 2 |
theme-command activate round trip, ms | 300 / 314 | 253 / 265 |
10.7. The shown ms figure did not move toward the ~30 ms target and is no better than the 161 ms baseline (dark
156 to 163, light 201 to 204). The reported reason is that the pre-render made while browsing (optimistic-apply prerendered generation=... appears before each tap) was not reused: prerendered=false reason=content-changed; the
overlay stage (55 to 86 ms) is the dominant part. Measured from touch-up, the frame appears about 510 to 570 ms later
because a preview request (255 to 288 ms, with a second appearance-prepare-accepted) runs after the tap, before the
optimistic frame. Both themes read correctly in the native captures and the picker fits the 568x1232 panel:
the theme card, background card, labels and the “Theme applied” status line are all inside the frame, the status line
sits about 27 px above the bottom edge at full resolution (panel-bottom-full-resolution.png), tight but not clipped.
This is compositor output (grim), not a camera view of the glass.
6.6 (not met). (a) Apply of an already-prepared generation is not under 150 ms: activate_generation is
193 to 242 ms and the activate round trip 253 to 314 ms. (b) activate_generation did skip the redundant prepare for
both receivers (prepare_phase 0.1 to 3.1 ms, warm=2 targets=2), and the helper’s activate request showed parse 5 to 7 ms
(trial 1) and prepare_entry 28 to 38 ms. (c) A second appearance-prepare-accepted does occur between the neighbour warm and the
Apply, because the tap sends a preview first. (d) The cold-open neighbour check passes: after a fresh restart,
the active theme previews in 380 to 383 ms and the neighbouring theme in 231 to 241 ms about 0.3 s later, not the earlier ~4 s
cold figure. The disk caches were warm, so this is a process-cold, not disk-cold, result. 6.6 stays open.
Next measured correction: skip the tap-time preview when the Rust side already holds the prepared generation
(or make preview idempotent for a generation the receivers already hold), and reuse the browse-time pre-render
instead of discarding it as content-changed.
Observer cost and limits
- Everything here is injected input over SSH, with Rust-clock and
CLOCK_MONOTONICjournal timing. Commit gaps are intervals betweencommitevents, not display FPS;presentedgaps use the compositor’s presentation feedback, not photons. - CPU samples are about 0.5 s apart, so a 1.4 s swipe window is only partly covered; percentages are CPU seconds over covered wall seconds.
- The recorder was on for the trace pairs and the 18.3 runs (cost above); recorder-off pairs are the clean CPU
and cadence numbers. No
grimran inside a profile timing pass. In the 18.3 runs the capture follows each settle inside the bounded trace window but outside the analysed gesture window. - Baseline and candidate differ visually (card drawing), so render and CPU differences are not purely the row repaint mechanism. Three pairs per mode is a small sample; medians over three, not a distribution.
- Disk-cold behaviour was never tested; user caches were not cleared.
process-coldis journal- and age-verified only. - Run order and the fixed workload were identical for both builds, but the helper’s dwell-triggered previews land in different swipes from run to run, which is the dominant noise.
Final state of the board (verified 07:17 to 07:20 UTC)
- System unchanged (
zvxmd93g..., booted = current); no boot selector, profile or flash was written. - Virtual input unit stopped; only
event0(power key) andevent1(Goodix) remain;/run/systemd/systemempty, no drop-ins, no restoration timers, nok230-theme-sess-*units. shell,shell-ui,theme-helper,k230-touch-trackpadandshell-keyboardallactive;shell-uiruns the installedybpnELF; compositor and helper executables as listed above.- Original theme state restored: the durable state I created (pointer, 18 staged generations, selection file,
app/keyboard appearance) was removed, leaving only
.activation.lock, andshell.servicewas restarted; the shell user seesactive: {id: null, generation: null}as at the start and the screen shows the same boot-time terminal as the first capture. - Left on the board: gcroots for
8cikandsi9vd, the small session scripts under/root/tmp/k230-theme-board-session-20261012/(raw runs deleted).