4 Commits

Author SHA1 Message Date
alonso.torres
a8fefe3980 ⚡ Make component sync about twice as fast
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

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 11:00:30 +02:00
alonso.torres
717aa026ba ✨ Record a golden master of the component code
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

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 10:46:22 +02:00
alonso.torres
d97cbac3a3 ♻️ Tidy the composable suite and make its cases stricter
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.
2026-10-13 19:58:10 +02:00
alonso.torres
b955a2fc68 ✨ Add a component sync benchmark with a recorded baseline
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%.
2026-10-13 19:58:07 +02:00