evidence · 16.2 KiB

README.md

Committed at docs/evidence/omarchy-themes/instant-theme-swap/README.md. This is what grounds the requirements that cite it.

Open raw source

Instant theme swap: buffer-swap commit, chooser prepare-ahead, deferred wvkbd

Implements OpenSpec tasks 3.1b, 3.2, and 3.3a from openspec/changes/archive/2026-10-12-the-shell-swaps-themes-without-a-python-stall/tasks.md, against the user’s stated target: the new theme visible within about 100 ms of the Apply tap, touch and animation never stalling. This is a host + headless-QEMU-under-qemu-riscv64-static result: no board, glass, or real-finger observation. Worktree perf/instant-theme-swap, base master 64c38998.

What changed (see the OpenSpec change’s tasks.md/proposal.md for the full design)

Test-suite proof

Headless-QEMU runtime proof (real Sway + real Rust shell, not synthetic)

Store paths (perf/instant-theme-swap, this worktree):

1. Task 3.2 end to end, real touch injection

python3 tests/rust_theme_chooser_qemu.py \
  --sway /nix/store/ajdxjvqp303gnbdqbm67jnfk9pw8nbn4-sway-unwrapped-riscv64-unknown-linux-gnu-1.12/bin/sway \
  --rust /nix/store/qdh605prj11a2kbwy3kq3gayk1x4fyjv-k230-shell-rust-riscv64-unknown-linux-gnu-0.1.0/bin/k230-shell-rust \
  --output /tmp/k230-is-2

PASS. The captured synthetic-command log (/tmp/k230-is-2/theme-commands.jsonl) shows exactly the intended shape:

["list"]
["preview", "...06"]                                    <- warm-up while dragging settled on 6
["preview", "...07"]                                    <- warm-up while a side-tap settled on 7
["preview", "...07"]                                    <- the explicit confirm tap's own request
["preview", "...07", "--background", "dddd..."]         <- explicit background selection
["list"]                                                 <- after Cancel
["preview", "...00"]                                     <- re-confirm (post-cancel, centred on active)
["activate", "...00", "--expected-generation", "bbbb...", "--background", "cccc..."]

Two background warm-up preview calls landed during ordinary browsing (drag, then a side-slice tap), each fired only after its own theme sat centred and at rest past the 220 ms debounce, neither one visible to the person (no navigation, no repaint beyond the carousel’s own animation — proven by this test’s own screenshot diffs at each step) and neither one able to activate or select a background (browse_calls_are_safe’s assertion, updated in this change since it is the exact old “browsing must never call preview” invariant this task deliberately changes). The subsequent explicit confirm, Apply, Cancel, and re-confirm flows are byte-for-byte the same as before this change.

2. Task 3.1b on the compositor: gradient prepare/adopt pixel proof

python3 tests/test_card_shell_appearance_runtime.py \
  --sway /nix/store/ajdxjvqp303gnbdqbm67jnfk9pw8nbn4-sway-unwrapped-riscv64-unknown-linux-gnu-1.12/bin/sway \
  --client /nix/store/qpjim9wgpwcwjnyvg6zn0qgyf5v9g746-card-composition-probe-client-riscv64-unknown-linux-gnu-0.1/bin/card-composition-probe-client \
  --output /tmp/k230-is-appr1

result.json: "result": "PASS", with the canvas gradient’s left edge (250,0,5) and right edge (4,0,251) matching the authored #ffff0000 -> #ff0000ff, 90-degree stops exactly, and rollback restoring the pre-theme backdrop (30,30,46) exactly. sway.log shows:

K230_CARD_SHELL appearance-canvas-gradient-adopted generation=222222222222222222222222

— exactly once, never -built, proving the candidate’s gradient was built during prepare and adopted (not repainted) at commit, with correct pixels.

3. Task 3.1b+3.2 on the real two-phase protocol under occlusion + throttle

Reused tests/test_theme_commit_under_occlusion_runtime.py’s own harness shape (real theme_catalog.py preview/activate CLI calls, real sway+k230-shell-rust under qemu-riscv64-static, the Rust process throttled via cpulimit, an ordinary maximized opaque app already mapped):

python3 tests/test_theme_commit_under_occlusion_runtime.py \
  --sway /nix/store/ajdxjvqp303gnbdqbm67jnfk9pw8nbn4-sway-unwrapped-riscv64-unknown-linux-gnu-1.12/bin/sway \
  --rust /nix/store/qdh605prj11a2kbwy3kq3gayk1x4fyjv-k230-shell-rust-riscv64-unknown-linux-gnu-0.1.0/bin/k230-shell-rust \
  --client /nix/store/qpjim9wgpwcwjnyvg6zn0qgyf5v9g746-card-composition-probe-client-riscv64-unknown-linux-gnu-0.1/bin/card-composition-probe-client \
  --theme-bundle /nix/store/hklqzizmpawxdp044zm5rqa19xbq6pz7-handheld-theme-default-28ceaae7 \
  --icons /nix/store/in3nc21zx02xiz2rdp5bpq36719x3axd-handheld-theme-icons-25.10.3 \
  --tools nix/omarchy-theme-tools/upstream --throttle 60 --output /tmp/k230-is-occ3

Scenario B (rollback to packaged default with previous=None) PASS / Scenario A (commit under occlusion) PASS. The default --throttle 20 timed out during Scenario B’s own prepare exchange on this dev host — identically against an unmodified master build of the same test, so this is this host’s known QEMU+cpulimit decode throughput limit at -l 20 (already documented in docs/evidence/omarchy-themes/background-decode-cache-qemu/README.md), not a regression; --throttle 60 clears it for both. rust.log shows the two new stage markers landing:

rust-shell 2576ms appearance-prepare-accepted
rust-shell 2774ms wallpaper-commit
rust-shell 2774ms appearance-commit-accepted
rust-shell 3051ms appearance-prepare-accepted
rust-shell 3270ms appearance-prepare-accepted
rust-shell 3280ms wallpaper-commit
rust-shell 3280ms appearance-commit-accepted

4. Tap-to-visible, cold vs. warmed-ahead (activate’s own wall time only)

A small script (not committed — see reproduction command below) reused the same harness shape to isolate exactly the activate call’s own wall time in two cases: (A) cold, no browse-ahead (preview then activate immediately — what any theme not dwelled on still costs, even after this change), and (B) preview (task 3.2’s own warm-up call shape), a 400 ms simulated dwell (past the 220 ms debounce), then only activate timed:

wall time
A: cold activate (gruvbox, never prepared)223 ms
B: activate after a warm-up + dwell (nord, never prepared)236 ms

Both already fast and nearly equal here: rust.log shows only a 13-15 ms gap between appearance-prepare-accepted and appearance-commit-accepted in both cases, meaning these particular bundled themes’ backgrounds decode fast enough on this host under QEMU that decode was never this measurement’s bottleneck — the dominant, already-fixed cost this whole change addresses is the ~1.2 s Python-interpreter import tax theme-helper.service already removes (see docs/evidence/omarchy-themes/theme-swap-jank/README.md’s own numbers), not wallpaper decode for typical theme assets. What 3.1b’s LRU specifically prevents is a regression that would otherwise reappear once 3.2 lands: without it, browsing a second/third candidate before Apply would evict a single-slot cache’s warm entry, turning a would-be buffer swap back into a background.cache file read or a full decode; multiple_recent_candidates_ stay_warm_without_evicting_each_other (background_decode_module.rs) is the correctness proof for that, in the absence of a wall-clock difference large enough to be visible with these particular fixture assets.

Reproduce with the new tools/theme-swap-jank-warm-ahead-demo.py (same store paths as above; --theme-a/--theme-b default to gruvbox/nord):

python3 tools/theme-swap-jank-warm-ahead-demo.py \
  --root "$(pwd)" \
  --sway /nix/store/ajdxjvqp303gnbdqbm67jnfk9pw8nbn4-sway-unwrapped-riscv64-unknown-linux-gnu-1.12/bin/sway \
  --rust /nix/store/qdh605prj11a2kbwy3kq3gayk1x4fyjv-k230-shell-rust-riscv64-unknown-linux-gnu-0.1.0/bin/k230-shell-rust \
  --client /nix/store/qpjim9wgpwcwjnyvg6zn0qgyf5v9g746-card-composition-probe-client-riscv64-unknown-linux-gnu-0.1/bin/card-composition-probe-client \
  --theme-bundle /nix/store/hklqzizmpawxdp044zm5rqa19xbq6pz7-handheld-theme-default-28ceaae7 \
  --icons /nix/store/in3nc21zx02xiz2rdp5bpq36719x3axd-handheld-theme-icons-25.10.3 \
  --tools nix/omarchy-theme-tools/upstream --throttle 60 --output /tmp/k230-is-warm-N

It is a thin wrapper around test_theme_commit_under_occlusion_runtime.py’s own setup, timing only the theme_catalog.py activate subprocess call in each scenario (not a proof tool with its own --self-test; it produces a number, tools/analyze-theme-swap-jank.py and the two runtime tests above are what assert pass/fail).

5. A test-suite invariant this change intentionally breaks (and how it was fixed forward)

tests/rust_theme_chooser_qemu.py previously asserted calls() == [["list"]] after ordinary browsing (a drag, then a side-tap) — literally “browsing must never call preview”. Task 3.2 makes that no longer true by design: a theme dwelled on during browsing now does trigger a background preview call. Confirmed by running the test unmodified first (it failed exactly there: AssertionError: a side-slice tap must only browse, never confirm, log showing ["preview","...06"]/["preview","...07"] from the warm-up), then updating the assertions to the real invariant that still must hold (“browsing must never select a background or activate”, not “browsing must never call preview at all”) — see browse_calls_are_safe in that file. The updated test additionally proves 3.2 fires (assert any(row[0] == "preview" ... for row in calls())) rather than merely not-breaking.

6. A pre-existing, unrelated flake noted, not fixed

tests/test_paired_theme_endpoints_runtime.py timed out on this dev host at wait(expanded_app, 5) (a card “back”-navigation animation settle, nothing to do with appearance/theme code) under sustained high host load (uptime showed load average: 23-28 on a 32-core host, from concurrent Nix builds run by other agents in this shared repository per tools/work-status.py). Reproduced identically against an unmodified master build of the same test (.#card-shell/.#handheld-shell-rust built from origin/master 64c38998), confirming this is host contention on this dev machine, not a regression from this change. Not fixed here; worth a retry on a quieter host or with a longer timeout if it recurs.

Board commands for the coordinator (tasks 2.3 and 3.4, unchanged gate)

Unchanged from docs/evidence/omarchy-themes/theme-swap-jank/README.md’s own “Board commands for the coordinator” section — this change adds no new board-only step, only more to look for once run:

./tools/console.py /dev/ttyACM0 --wait=3 \
  "python3 /path/to/tools/theme-swap-jank.py --theme-id catppuccin-latte --duration-after-s 3"
./tools/console.py /dev/ttyACM0 --wait=3 "cat /run/theme-swap-jank-<epoch>.json" > /tmp/after-3.1b-3.2.json
python3 tools/analyze-theme-swap-jank.py --input /tmp/after-3.1b-3.2.json --output /tmp/after-3.1b-3.2-report.json

In addition to the existing tap_to_visible_ms/gap-histogram checks, the coordinator should now also see, in the board’s own journalctl -u shell-ui -u shell -o json output that tools/theme-swap-jank.py already merges:

Neither this change’s task 2.3 nor 3.4 board run has been executed by this worktree; /dev/ttyACM0 was not opened here.