Board session 2026-10-12 (b): memory, motion curves, warm tap-apply
Tasks 11.3, 18.3 and 6.6 of change the-shell-swaps-themes-without-a-python-stall.
The operator asked for this session, without being present: “just test stuff that doesn’t need me as far as you can then lmk what you need”. The work follows the first board session of the same day (../board-session-2026-10-12/README.md) and its tools, workloads and gates.
The source is branch perf/theme-row-memory-trace, in worktree
/home/jadams/tmp/k230-row-memtrace. Its base is master 6faf4b9b, and it merges
perf/theme-tap-apply. Times are UTC unless marked local (board local time is CDT, UTC-5).
Evidence classes:
- software-injected touch through the virtual
K230 injected touchscreen; - the shell’s own runtime trace and journal;
- coherent-installer serial checks.
No finger or camera observation is claimed. Raw captures stay private in
~/tmp/k230-session-b-run/ and are not committed.
Builds
Four candidates were built, each with
flock /tmp/k230-nix-build.lock nix build .#kernelMainlineDrmShellBootFiles --out-link ~/.local/state/tdisplay-k230/retained-builds/row-memtrace-2026-10-12/<name> --print-out-paths --max-jobs 1 --cores 4.
Each was swapped in through a /run drop-in (wrapper copy with the final exec replaced) for its board runs.
All four candidate ELFs have the same runtime reference set as baseline 8cik (md5 adb9f783…).
| Candidate | Commit | Rust ELF | What it added |
|---|---|---|---|
| 1 | 0cfbb9e6 | x6hhs64w… | Rest-size sprite cache only, per-frame carousel trace record, tap without a preview, pressed-wash fingerprint |
| 2 | 3e9df8d5 | zfck2cyv… | LRU card cache (4 MiB), reused scratch surfaces for in-between cards, release and malloc_trim when a scene has no picker |
| 3 | b01b580d | a7aid2j6… | Diagnostic: logs the pre-render’s content parts beside the current ones on a mismatch |
| 4 | e93b5a23 | 3m65ag9z… | Fingerprint recomputed on every view update, only collapsed slices cached (2 MiB), trim and picker-memory log at gesture rest |
Candidate 4 is the measured and installed build. The tables below are all candidate 4 unless named otherwise.
Task 11.3: matched pairs against the repaired baseline
Each run used tools/go.sh and tools/seq.sh, the same run.py / 13.1 workload as the
first session, recorder off.
The order was:
p1cand, base (16:56);p2base, cand;p3cand, base.
Every run passed the profile harness gates, and every compare reports comparable: true
(11.3/compare-of-p1.json and the next two). The reduced reports are in
11.3/reports/, and the pair table is 11.3/pairs-summary.json.
The baseline is 8cik (repaired baseline, same as the first session). The candidate is 3m65ag9z.
| Metric (median of three pairs) | Baseline | Candidate | Per-pair delta |
|---|---|---|---|
| Warm theme commit gap, median ms | 140 | 63 | -81, -79, -65.5 |
| Warm theme commit gap, p95 ms | 173 | 89 | -84, -99, -65 |
| Cold theme commit gap, median ms | 144 | 67 | -78, -63, -67 |
| Background commit gap, median ms | 84 | 38 | -50, -46, -32.5 |
| Warm theme commits | 59 | 97 | +39, +40, +29 |
| Rust CPU %, warm theme swipes | 79.0 | 57.0 | -21.1, -21.7, -22.1 |
| Rust CPU %, background swipes | 46.0 | 9.2 | -36.8, -37.5, -15.8 |
| Rust CPU %, idle | 0.48 | 0.51 | -0.48, +0.01, +0.06 |
| Rust RSS at end of warm idle, MiB | 82.5 | 84.5 | +2.0, -2.3, +11.0 |
| Rust RSS peak, MiB | 82.5 | 87.4 | +4.9, +3.9, +13.2 |
| Helper RSS, MiB | 24.0 | 23.4 | -0.7, +0.2, -0.7 |
| Theme preview requests | 5 | 15 | +9, +7, +12 |
| Overlay pre-renders completed (none with a touch held) | 4 | 8 | +3, +1, +11 |
Memory attribution. Per-phase Rust RSS shows the following
(11.3/reports/, units.shell-ui.service.rss_last_bytes):
- The candidate ends every run at 80–87 MiB.
- The baseline ends at 82.5, 82.5 and 75.5 MiB. The +11 MiB pair is the baseline’s low
p3run, not a high candidate run. - When the picker closes (phase
back), the candidate falls to 79.2–79.3 MiB in all three runs. That is below the baseline’s 80.5–81.9 inp1/p2.
While the picker is open, the shell’s own picker-memory log at gesture rest reports
(6.6/journal-trial-1.txt):
| Item | Size |
|---|---|
| Card sprites | 1.96–2.03 MiB |
| Outline masks | 52 KiB |
| Scratch surfaces | 1.5 MiB |
| Thumbnails (same bounded cache as the baseline) | 7.2–8.1 MiB |
| glibc heap in use | 22–25 MiB |
| Free after trim | 4–6.8 MiB |
The candidate’s +2 MiB median is therefore the picker’s deliberate 3.5 MiB sprite and scratch working set, released when the picker closes.
How the build got there:
- Candidate 1 held +16.0 MiB at the end. Its unbounded rest-size cache kept every visited card (11.3/first-candidate/).
- Candidate 2 held +15.5 MiB at the end (11.3/second-candidate/), but fell by about
12 MiB at
back, which showed the remainder was picker-scoped. - Candidate 4 caches only collapsed slices and trims freed heap at each gesture rest.
The candidate makes three times as many speculative preview requests as the baseline. Helper RSS is unchanged.
Task 18.3: per-frame carousel position and velocity
The shell now writes one carousel trace event per committed frame on the Settings route:
- frame number;
- each row’s position (items) and velocity (reference px/s, the coast or spring velocity; 0 under a finger and at rest), stored as f64 bits;
- each row’s motion phase: rest, finger, coast or settle.
Source: Carousel::trace_state in nix/rust-shell-client/src/theme_carousel.rs and the commit site in main.rs.
Runs g-theme-1, g-bg-1, g-theme-2, g-bg-2 (16:43 to 16:56) used tools/gog.sh. That is the first
session’s run.py --workload gestures with the five injected programs (flick, slow, hold-release, reversal,
interrupt), two repetitions each, a fresh Settings to Themes opening per gesture, and trace mode. Generation and
process identities were checked after every gesture and never changed. No trace dropped an event.
tools/curves.py plots every committed frame. Nothing is replayed through a host model. It anchors “release” on the record’s own last finger-phase frame, because the shell handles an injected release 6–130 ms after it is written.
Outputs:
- 18.3/position-curves.png;
- 18.3/velocity-curves.png;
- 18.3/curves-summary.json, with all 40 gestures and their medians.
| Gesture (median of 4) | Row | Final item | Items after release | Overshoot past final | Release to rest, ms | Frames after release |
|---|---|---|---|---|---|---|
| flick | theme | 10 (4 of 4) | 8.08 | 0 | 837 | 12 |
| slow | theme | 3 | 1.16 | 0 | 482 | 10.5 |
| hold-release | theme | 1 | 0.06 | 0 | 370 | 8 |
| reversal | theme | 0 | -0.29 | 0 | 418 | 12.5 |
| interrupt | theme | 7 | -0.69 | 0 | 632 | 11 |
| flick | background | 3 (the last item) | 1.62 | 0 | 439 | 14 |
| slow | background | 3 | 0.26 | 0 | 305 | 10 |
| hold-release | background | 1 | -0.39 | 0 | 342 | 10 |
| reversal | background | 0 | -0.29 | 0 | 419 | 13 |
| interrupt | background | 2 | -0.90 | 0 | 617 | 17.5 |
Observations only:
- Every gesture approaches its final item monotonically, with no overshoot.
- The four repetitions of each gesture land on the same item.
- A 300 px / 80 ms theme flick leaves at the -3000 px/s velocity ceiling and coasts about 8 items.
- One theme flick (
g-theme-1, rep 0) holds near item 2 for about 150 ms early in the coast before catching up. That is a slow frame, visible as a gap in the position curve.
Whether the travel, settling and interruption feel right is task 18.4’s finger question. These curves do not answer it.
Task 6.6: warm tap-apply
tools/apply-swap.sh ran the first session’s apply trial (tools/apply_trial.sh)
twice on candidate 4 through the /run drop-in. Each trial does the following:
- Cold restart of
theme-helperandshell-ui. - Settings to Themes, then wait 15 s.
- Hold-release to the light neighbour, then an injected centre tap.
- Swipe back to the dark item, then an injected centre tap.
Trial 1 started 16:41:06 and trial 2 at 16:42 (local 11:41 / 11:42). Sanitized journal excerpts: 6.6/journal-trial-1.txt and 6.6/journal-trial-2.txt, via tools/sanitize_journal.py. The markers are in 6.6/marks.txt.
| Light (t1 / t2) | Dark (t1 / t2) | |
|---|---|---|
optimistic-apply shown ms | 181.7 / 189.4 | 153.4 / 150.5 |
prerendered | true / true | true / true |
stage adopt / wallpaper / overlay ms | 16.0 / 4.6 / 54.8; 12.4 / 7.5 / 56.5 | 14.9 / 21.4 / 14.3; 15.3 / 19.3 / 11.0 |
| Tap-up to shown frame (journal Rust clock), ms | 185 / 190 | 154 / 151 |
activate_generation total, ms; prepare_phase ms; warm | 173.8 / 174.2; 0.2 / 2.8; 2 | 140.1 / 140.8; 2.7 / 2.6; 2 |
theme-command activate round trip, ms | 323 / 326 | 288 / 280 |
The first session’s figures were:
- tap-up to shown 510–570 ms;
- every pre-render discarded as
content-changed; - overlay stage 55–86 ms;
- a
previewrequest and a secondappearance-prepare-acceptedafter every tap.
In this session:
-
No second prepare. No
previewrequest and no secondappearance-prepare-acceptedfollows a tap. The tap goes straight toactivatewith the generation the browse-time warm learned. -
The pre-render is reused (
prerendered=true, 4 of 4).Candidate 3’s diagnostic line showed identical content parts on both sides of the mismatch. That narrowed the cause to the chooser fingerprint: it was only recomputed when the old cache-key check fired. That check compares each update with the previous one at a quarter-item tolerance, so a settle moving in small steps left the fingerprint on a mid-settle position. Candidate 4 recomputes it on every update.
Candidate 1 and 2 trials (
content-changed, 203–245 ms shown) are in the private run directory only. -
Not yet under 150 ms. Tap-up to the shown frame is 151–190 ms, and the helper’s
activate_generationis 140–174 ms.commit_phase(72–109 ms) waits for the shell’s commit acknowledgement.swap_pointer(26–33 ms) is the durable directory fsync.
The cold-open neighbour check passed in the first session and was not repeated.
Follow-up: what the tap waited on
Same trial, same recipe (tools/apply-swap.sh with the candidate path changed), two cold-process
trials per candidate, injected taps. Candidates 5–8 add the entry_ms/checks_ms stage fields; source is the
perf/theme-row-memory-trace branch after 89f8b25e.
- Candidate 5 (
7rfw6yc2, timing only).entry_ms, from the Activate submit to the optimistic check, was 106–193 ms of the 140–235 ms shown time. The touch-up handler ends with a synchronous pickerdraw(the pending and pressed state), which ran before the loop reached the optimistic check. Journals are in the private run directory only. - Candidate 6 (
2jgydicg). The touch-up handler and the overlay frame callback skip that draw while a fresh Activate awaits the optimistic check; the loop draws anything still dirty afterwards. Tap-up to shown frame: 27.8–31.1 ms,entry_ms3.4–6.1,prerendered=true4 of 4 (6.6/candidate-6/). The helper’s commit then waited longer: the shell spent 336 ms repainting the hidden Home layer (K230_HOME_FRAME ms=336) before acknowledging. Shell commit exchange 165–232 ms,commit_phase215–238 ms, activate round trip 459–476 ms. - Candidate 7 (
bgdk0j87). Defers the Home repaint while an optimistic frame awaits its commit (cap 2 s). The commit event clears that state on arrival, and the loop still repainted Home before reaching the deferred ack: shell commit exchange 214–232 ms,commit_phase281–301 ms. Private run directory only. - Candidate 8 (
hl4bca4g). Also acknowledges a reused optimistic commit as soon as the event arrives, before that tick’s Home repaint (6.6/candidate-8/):
| Candidate 8 | Light (t1 / t2) | Dark (t1 / t2) |
|---|---|---|
optimistic-apply shown ms | 27.5 / 28.8 | 29.2 / 34.2 |
prerendered | true / true | true / true |
| shell commit exchange, ms | 48.1 / 52.6 | 37.2 / 42.9 |
activate_generation total; commit_phase, ms | 179.1 / 174.9; 110.5 / 115.9 | 166.5 / 170.6; 105.1 / 105.7 |
theme-command activate round trip, ms | 338 / 339 | 333 / 342 |
Home repaints 350 ms after the ack (K230_HOME_FRAME ms=347–355), off the commit path. Every apply changed the active
theme link (ACTIVE_* in 6.6/candidate-8/marks.txt). Native grim captures of the picker
after each apply (trial 2 of candidates 6 and 8, viewed, not committed) show the applied theme’s chrome and
“Current theme” on the applied item.
The visible apply is now well under 150 ms. The durable activation, measured by the helper, is not:
activate_generation is 167–179 ms, and prepare_entry (82–101 ms) plus commit_phase put the round trip at
333–342 ms. The commit exchange with the card process (32–35 ms) and swap_pointer (23–39 ms) remain inside
commit_phase. Neither the injected taps nor these journals are a finger-on-glass measurement.
Install
| Identity | Value |
|---|---|
| Bundle | /nix/store/1dkw87zfhgfygnmv9qxdrbcqy2xqzxfg-k230-coherent-shell-boot-files |
| System | /nix/store/fm8gk6xica4h7ni3fp6wgya3lmfxfzjr-nixos-system-nixos-26.11.20260919.20b1ddd |
| Rust shell ELF | /nix/store/3m65ag9zlzffdd86ajjfq7si96cj46y1-k230-shell-rust-riscv64-unknown-linux-gnu-0.1.0/bin/k230-shell-rust |
Kernel, Image, initrd.uimg | unchanged from the baseline |
| Baseline | sprite install rav09qp6… (../row-sprites-2026-10-12/) |
The coherent installer ran with the row-sprites recipes. Only the paths, the baseline and the saved catppuccin
appearance differ (install/expected-runtime.json; see install/*-used.py). Every step
passed:
- Preflight: boot
49fb3850-…, 17:07:32 (install/preflight.json). - Stage: (install/stage.log, install/transfer.json, install/staged-state.json, install/host-inspection.json).
- Candidate boot: (install/candidate-boot.json). The runtime check ran on boot
dd17a5e9-…at 17:12:19 (install/candidate-runtime.json). - Qualification: (install/deployment-authorization.json).
- Install and ordinary autoboot: (install/ordinary-boot.json). The runtime check ran on
boot
34d77814-…at 17:15:31 (install/ordinary-runtime.json). The current system and profile are bothfm8gk6xi…, the four shell services are active, and the saved appearance is unchanged.
install/installed-picker-native.png is a native grim -s 0.5 capture taken on
that ordinary boot at about 17:16. Settings was opened and Themes was tapped by injection. It shows catppuccin as the
current theme, its neighbours as side slices, and the Backgrounds row loaded. No finger observation was made on the
installed boot.
Install of candidate 8
| Identity | Value |
|---|---|
| Bundle | /nix/store/amvl5qymc5vgq3b9y1r659c9lf96gyzx-k230-coherent-shell-boot-files |
| System | /nix/store/298a2bf6sqpc82ngdryxgg3xmr9fg9f7-nixos-system-nixos-26.11.20260919.20b1ddd |
| Rust shell ELF | /nix/store/hl4bca4gi4m0wmhwas1i1zl8n360m744-k230-shell-rust-riscv64-unknown-linux-gnu-0.1.0/bin/k230-shell-rust |
| Theme helper | /nix/store/2m37rx16riv3mjxvajfvjakncs9gszkb-handheld-theme-command-0.1 (changed only because it names the Rust ELF) |
| Source | 56a7b94e |
Kernel, Image, initrd.uimg | unchanged |
| Baseline | candidate 4 install fm8gk6xi… (above) |
The same installer recipes ran from a new run directory with the paths, baseline, bundle already on the board, and saved
appearance changed (install-2/, *-used.py, install-2/expected-runtime.json).
Every step passed:
- Preflight: boot
34d77814-…, 18:35:00 UTC (install-2/preflight.json). - Stage: (install-2/stage.log, install-2/transfer.json, install-2/staged-state.json, install-2/host-inspection.json).
- Candidate boot: (install-2/candidate-boot.json); runtime check on boot
c9d1793e-…at 18:40:10 UTC (install-2/candidate-runtime.json). - Qualification: (install-2/deployment-authorization.json).
- Install and ordinary autoboot: (install-2/ordinary-boot.json); runtime check on
boot
94fd02a1-…at 18:43:21 UTC (install-2/ordinary-runtime.json).
On that ordinary boot the apply trial (tools/apply_trial.sh, no drop-in, injected taps) ran once (install-2/journal-installed-apply.txt, install-2/marks-installed-apply.txt):
| Installed | Light | Dark |
|---|---|---|
optimistic-apply shown ms | 28.0 | 42.2 |
prerendered | true | true |
activate_generation total; commit_phase, ms | 152.0; 91.3 | 148.7; 85.7 |
theme-command activate round trip, ms | 313 | 309 |
A floating terminal covered Home during the trial, so Home drew no frame while covered. Moving the terminal to the sway
scratchpad drew one (K230_HOME_FRAME ms=203), and
install-2/installed-home-after-apply-native.png (native
grim -s 0.5, about 18:51 UTC) shows Home in the applied dark catppuccin theme. The terminal was then shown again.
No finger observation was made on the installed boot.
Board state afterwards
- Installed system
298a2bf6…with Rusthl4bca4g…. - The virtual touch device unit
k230-theme-session-inputstops by its ownRuntimeMaxSec. - The gcroots
k230-session-b-candidate{,2,3,4,5,6,7,8}pin the swapped ELFs.k230-theme-baseline-8cikandk230-theme-motion-si9vdremain from the first session. - The saved theme is the dark
catppuccingeneration applied by the last apply trial. The operator said any theme is fine.