A profile of the benchmark worlds showed where a sync spends its time.
- The token sync of shapes that apply no tokens took 38% of a sync in
the 1000-copy world: for every synced shape, generate-update-tokens
walked every sync attribute, resolved its group and its token
attributes, and only then found that neither the main shape nor the
copy applies a token. It now returns at once, and the token
attributes of each sync attribute are computed once, at load.
- A sync looks through every object of each page and component to
find the copies it updates, so in a file with many shapes and few
copies the search costs more than the sync itself. The new benchmark
world main-edit-among-many-shapes syncs a main edit into 10 copies in
a page with 20,000 plain rects; its baseline, measured before the
change below, joins the recorded one with a note, and its result
joins the golden master. generate-sync-container called the
uses-assets? multimethod on every object through a lazy filter; the
predicate is now chosen once per container (asset-user?) and the
objects walked with reduce-kv into a vector, in the same order.
- compare-children searched the remaining children (d/seek) for every
child out of place, and dropped a child taken out of order with a
lazy remove, so a reorder took quadratic time and nested one lazy
sequence per child taken. It now looks children up by their
reference or id (index-positions) and skips those taken out of order
when the walk reaches them, with the same callbacks in the same
order. The sync of a reorder stays slow: when a main with 1,000
children moves its first one to the end, each copy still gets 999
moves (F85), and applying them is what costs (4.75 s to 4.26 s for
10 copies). Fewer moves change what a sync writes, so that waits for
Phase 4c.
No behavior change: the golden masters and the suites are unchanged.
Benchmark (lower quantile, ms; baseline / now / ratio):
deepest-edit-through-three-levels 35.2 / 18.6 / 0.53
edit-through-a-chain-of-dependents 68.9 / 36.4 / 0.53
library-update-into-copies 83.1 / 43.0 / 0.52
main-edit-among-many-shapes 4.8 / 2.3 / 0.48
main-edit-into-10-copies 1.8 / 0.9 / 0.48
main-edit-into-100-copies 15.4 / 8.4 / 0.54
main-edit-into-1000-copies 153.1 / 90.1 / 0.59
variant-switch-into-copies 32.1 / 20.0 / 0.63
(main-edit-among-many-shapes: baseline measured before the search
change.)
AI-assisted-by: claude-opus-5-5
Before refactoring the component sync code, record the exact files the
component tests produce, so a refactor that changes any of them fails.
The JVM test common-tests.logic.golden-master-test runs every variant of
the composable cases the pure runner runs and every benchmark world. It
records the file after each user, undo and redo step (the runner's new
:observe option), the final files with the signatures of the undo/redo
round trip failures, and each world's file after its timed work. The
frontend interpreter records the store's files of every variant of its
cases and of the completion contract the same way.
app.common.test-helpers.golden-master writes a state as the normalized
files in sorted "path value" lines: ids become test labels, or numbers
in tree order when unlabelled; grid cells are named after their shape
and position; timestamps are left out. So a state is the same in every
run whatever ids it draws. Only a digest per state is committed; the
full lines go to tmp/golden-master/ and tmp/golden-master-frontend/
(recorded/, and actual/ for a state that differs), so diff -r shows a
change. PENPOT_GOLDEN_MASTER=record records them again.
Both sides repeat exactly across runs. Dropping :opacity from the
attributes sync copies fails the case C variants that edit it.
The interpreter's fixture also turns cts/wasm-enabled? off for each
composable test and restores it after, as it already restores the
store. A test that starts the WASM renderer left the flag on, so in a
full frontend run the cases ran on WASM shape proxies, which leave out
the attributes that are nil or false, and the frontend golden master
differed from a run of the cases alone (:flip-x and :flip-y).
AI-assisted-by: claude-opus-5-5
A review of the suite after the new operations:
- Dead helpers and duplicate nodes go; the lineage reset and swap
nodes are constructors of reset and swap-shape.
- Repeated helpers are shared: grid boards (tho/add-grid-frame,
tho/place-in-grid-columns), n/nested-head-under, n/child-shape,
r/sync-cascade (the benchmark uses it too), the frontend fixture and
fill helpers, and the plugin getMainComponent.
- Runner tests that repeated cases B, C and D go.
- Comments say what the code does, and the wrong ones are fixed.
- Case K's remote edit painted red on red and its unedited variants
asserted nothing; it starts grey and every variant asserts.
- F43 is pinned in one case, AJ, by the value one undo leaves. L, M, Q
and K get real frame sizes, since their pins depended on when the
100 ms layout buffer flushed.
- make-nested-component takes a level count and depth-sweep gives each
depth once, so K, Q, R, T and W stop running depth 1 twice.
- The completion-contract tests no longer depend on fixed pauses or
on how the watcher batches its events.
Benchmark against the baseline (lower quantile): ratios 0.92 to 1.03,
no world flagged.
The coming refactors and fixes change sync code, and a slowdown must be
seen and explained in the commit that causes it. A JVM benchmark now
times component sync on fixed-size worlds:
- one main into 10, 100 and 1000 copies;
- a change in the deepest of three nesting levels;
- a chain of dependents (a in b in c, with copies of each);
- a variant switch in a main with 100 copies;
- a library update into a file with 500 copies.
Each world is built once; only the sync an edit causes is timed, run
to a fixpoint as the frontend watcher does. Each checks its result, so
a world that synced nothing fails instead of timing nothing.
`scripts/bench-components record` writes the baseline, recorded here on
the code before this commit; `scripts/bench-components` compares against
it and exits 1 when a world is more than 10% slower. It compares the lower
quantile: with the devenv running, the mean of the same code moved
from 0.41 to 1.13 of the baseline, the lower quantile by under 12%.