Merge train BM14d #907

Closed
kayg wants to merge 0 commits from train/2026-11-04-bm14d into dev
Owner

BM14d, based on origin/dev 41f2efa387b74175971a838f602535089d0d5fb4. Head c163abaea67b2a9b6874091b89e30c82d2e761a5. Do not merge.

PR Head merged (verified before merge) Outcome
#850 6eb81f62 --no-ff, clean
#851 d94771b6 (current head) --no-ff, clean
#852 07b6770a --no-ff, clean
#829 72eb0eb1 --no-ff, clean
#835 46be23e3 --no-ff, clean
#895 3c1d8deb --no-ff, clean
#905 8973b7e6 --no-ff, clean
#899 8484849c --no-ff, clean
#906 809a33c2 --no-ff, clean

No conflicts. Gate and perf results follow in comments. No fixes, no retries.

BM14d, based on origin/dev `41f2efa387b74175971a838f602535089d0d5fb4`. Head `c163abaea67b2a9b6874091b89e30c82d2e761a5`. Do not merge. | PR | Head merged (verified before merge) | Outcome | |---|---|---| | #850 | 6eb81f62 | --no-ff, clean | | #851 | d94771b6 (current head) | --no-ff, clean | | #852 | 07b6770a | --no-ff, clean | | #829 | 72eb0eb1 | --no-ff, clean | | #835 | 46be23e3 | --no-ff, clean | | #895 | 3c1d8deb | --no-ff, clean | | #905 | 8973b7e6 | --no-ff, clean | | #899 | 8484849c | --no-ff, clean | | #906 | 809a33c2 | --no-ff, clean | No conflicts. Gate and perf results follow in comments. No fixes, no retries.
kayg added 214 commits 2026-11-04 00:44:15 +00:00
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U5oTUycqAh7DT72mdSUztk
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U5oTUycqAh7DT72mdSUztk
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U5oTUycqAh7DT72mdSUztk
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U5oTUycqAh7DT72mdSUztk
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U5oTUycqAh7DT72mdSUztk
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U5oTUycqAh7DT72mdSUztk
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The room under a phone's Style sheet loads with the idle screens
Some checks failed
CI / server (pull_request) Has been cancelled
CI / rust (pull_request) Has been cancelled
CI / web (pull_request) Has been cancelled
Markdown extraction contract / markdown-native-contract (pull_request) Has been cancelled
Markdown extraction contract / markdown-editor-contract (pull_request) Has been cancelled
7a6d5adf4c
Presets: the thumbnail's page is not .page; backdrop, opacity and width specs follow the new Style sheet
Some checks failed
CI / server (pull_request) Has been cancelled
CI / rust (pull_request) Has been cancelled
CI / web (pull_request) Has been cancelled
Markdown extraction contract / markdown-native-contract (pull_request) Has been cancelled
Markdown extraction contract / markdown-editor-contract (pull_request) Has been cancelled
c1ef1b78ed
Merge port/style-2
Some checks failed
Markdown extraction contract / markdown-native-contract (pull_request) Has been cancelled
CI / server (pull_request) Has been cancelled
CI / rust (pull_request) Has been cancelled
CI / web (pull_request) Has been cancelled
Markdown extraction contract / markdown-editor-contract (pull_request) Has been cancelled
a0f425ac08
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
dee3e961 replaced the fixed 450 ms x4 waits with still(); a settled page
draws no frames, so the screencast held 15-19 frames instead of 40+ and
'frames > 20' failed 16/20 unloaded and 20/20 under load. Wait for the
frame count (state), threshold unchanged.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Opening the note (5 s), typing 55 keys (6 s) and two palette round trips
(6 s) at 6x CPU took 18 s of a 30 s test on a quiet machine and hit the
limit whenever the machine was shared. Loading+typing and switching modes
are now separate tests; nothing about the timeout or the shift check changed.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
e2e: vim :noh waits for the marks to come back; lazy-ui marks the window; links waits for the job first
Some checks failed
CI / server (pull_request) Has been cancelled
CI / rust (pull_request) Has been cancelled
CI / web (pull_request) Has been cancelled
Markdown extraction contract / markdown-native-contract (pull_request) Has been cancelled
Markdown extraction contract / markdown-editor-contract (pull_request) Has been cancelled
3c1d8deb19
- vim.spec '/ searches...': Esc from a half-typed '?cher' removes the marks and puts the
  earlier search's three back about 60 ms later (probe: 0,3,3,...). :noh sent inside that gap
  clears nothing and the marks then return. Wait for the 3 before ':', and for the bar's
  field to hold the focus before typing. Flaked 4/20 on origin/dev, 1/20 on the BM14b head.
- lazy-ui.spec 'failed ... idle warming recovers': detect a reload with a mark on the window,
  not performance.timeOrigin (WebKit derives it from the wall clock; BM14b saw it move by 1 ms).
- links.spec 'Try again': wait for the server's job (15 s poll) before the 5 s URL check.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
yrs 0.28.0 counts the whole merged string again after every squash of two
string items, so a run of appends costs the block so far each time (O(n^2)).
Vendored copy with the one-line fix: add the two lengths.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
test(notes): appends cost the same early and late, UTF-16 length stays exact
Some checks failed
CI / server (pull_request) Has been cancelled
CI / rust (pull_request) Has been cancelled
CI / web (pull_request) Has been cancelled
Markdown extraction contract / markdown-native-contract (pull_request) Has been cancelled
Markdown extraction contract / markdown-editor-contract (pull_request) Has been cancelled
76759d9da5
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Patch to the merged upstream commit until a release after 0.28.0 ships.
Cargo.lock follows in the next commit.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Halves of a run are noisy on a loaded machine (3.2x once in a full suite).
A 5 s bound on 6,000 appends separates the quadratic case (over a minute)
from the fixed one (well under a second) with a wide margin.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
An owner reading their own note never depends on a grant row. For
every credential except an app password the resource check reduces to
the note-ID size and read-scope checks, so run those directly instead
of a database round trip for each queued frame and each delivered
frame. App passwords keep the full check (revocation is in the db).
Same allow/deny outcome for every caller; non-owner paths untouched.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The owner's own live frames paid a clone, a boxed future and a home
lookup per queued frame and again per written frame. Answer them in
Home and Notes with the same size and scope checks, for non-app-password
owner credentials only; everyone else takes the full path unchanged.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A build older than #898 could not deserialize Requirement::ExpectedOwners, so
one such record took a whole Home offline on rollback. Accept the variant in
the reader and validator; admission still refuses it (this build has no
probe). Writes are unchanged. Test reads a real record written by #898.
Deploy doc: roll back past #898 only to a build with this reader.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
style: cargo fmt
Some checks failed
CI / server (pull_request) Has been cancelled
CI / rust (pull_request) Has been cancelled
CI / web (pull_request) Has been cancelled
Markdown extraction contract / markdown-native-contract (pull_request) Has been cancelled
Markdown extraction contract / markdown-editor-contract (pull_request) Has been cancelled
5f40792840
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
perf(web): leave unused remote-cursor code out of first paint (-983 B brotli)
Some checks failed
CI / server (pull_request) Has been cancelled
CI / rust (pull_request) Has been cancelled
CI / web (pull_request) Has been cancelled
Markdown extraction contract / markdown-native-contract (pull_request) Has been cancelled
Markdown extraction contract / markdown-editor-contract (pull_request) Has been cancelled
809a33c2b4
yCollab only uses y-codemirror.next remote selections when it is given an
awareness, and the editor always passes null. The module still shipped.
A build-time empty stand-in drops it; yCollab's own import keeps working.

first-paint JS: brotli 238797 -> 237814 B (-983), raw 845550 -> 841512 B (-4038)
first-paint CSS: unchanged (19250 B brotli)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The upstream pin (26cc6ae3) is 18 commits past 0.28.0 and some change how
unusual input is read (e.g. rejecting malformed updates), which could make a
stored document unopenable. Vendor the release with only the squash length fix.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
An owner reading their own note never depends on a grant row. For every
credential except an app password the resource check reduces to the
note-ID size and read-scope checks, so run those directly instead of a
database round trip for each delivered frame. App passwords keep the
full check (revocation is in the db). Same allow/deny outcome for every
caller; non-owner paths untouched.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Answer the owner's own connection in Home with the same size and scope
checks, without cloning the caller or awaiting the sharing layer.
Everyone else takes the full path unchanged.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The probe stood in for a recipient using the owner. An owner never waits
on a grant lookup, so use a real recipient caller: same recheck, same
assertions, no dependency on the owner path.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The probe stood in for a recipient using the owner. An owner never waits
on a grant lookup, so use a real recipient caller: same recheck, same
assertions, no dependency on the owner path.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
# Conflicts:
#	crates/auth/src/sharing.rs
#	crates/notes/src/home.rs
#	crates/notes/src/room/sync.rs
Opening a note for its owner made two sharing lookups (read, then edit)
plus the delivery check, for a person who needs none: the owner's access
is fixed by the credential. Answer owner Read and Edit with the same size
and scope checks and an Owner result, for session and development
credentials; app passwords (revocable), recipients and every other
operation take the full path unchanged. The sequence is passed through as
given; owner delivery never reads it.

Access logic touched: owner Read/Edit resolution only; outcomes are the
same allow/deny set.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Opening a note for its owner made two sharing lookups (read, then edit)
plus the delivery check, for a person who needs none: the owner's access
is fixed by the credential. Answer owner Read and Edit with the same size
and scope checks and an Owner result, for session and development
credentials; app passwords (revocable), recipients and every other
operation take the full path unchanged. The sequence is passed through as
given; owner delivery never reads it.

Access logic touched: owner Read/Edit resolution only; outcomes are the
same allow/deny set.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
note-bars.css held every bar state's rules and was imported by main.ts, so the
first stylesheet carried the block bar, the typing strip, the kind chip and the
strip fades (5.4 KB of 8.5 KB) before anyone selects a block or types. Move
those to note-bars-lazy.css, imported by BlockBar and Strip (lazy, in the
idle-screens chunk; Vite resolves the import after its CSS, so there is no
unstyled frame). The rest bar's rules and the pill radius stay eager.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Merge #906 (perf/first-paint-diet @ 809a33c2) into train BM14d
Some checks failed
CI / server (pull_request) Has been cancelled
CI / rust (pull_request) Has been cancelled
CI / web (pull_request) Has been cancelled
Markdown extraction contract / markdown-native-contract (pull_request) Has been cancelled
Markdown extraction contract / markdown-editor-contract (pull_request) Has been cancelled
c163abaea6
Author
Owner

BM14d gates and matrix (first run, nothing retried)

Head c163abaea67b2a9b6874091b89e30c82d2e761a5, base origin/dev 41f2efa387b74175971a838f602535089d0d5fb4. CI=1, SLOT_TRAIN=1, train lock with EXIT trap on dev-01 and dev-02, on-disk TMPDIR, --workers=1 --retries=0, df before every step (dev-01 >= 190 GiB, dev-02 >= 300 GiB free). No fixes made. Perf results follow in a separate comment.

Gate Command Result
install (both VMs) bun install --frozen-lockfile pass
fmt cargo fmt --all --check pass
clippy cargo clippy --workspace --all-targets --locked -j4 -- -D warnings pass
Rust cargo test --workspace -j4 pass: 1497 passed, 0 failed
cargo build (both) cargo build -j4 pass
web build (both) cd apps/web && bun run build pass
bundle budgets PERF_DIST=$PWD/apps/web/dist perf/web.sh --bundle-only --gate pass (first JS 232.83 KB / 240, first CSS 19.18 KB / 22.1, total JS 363.4 KB / 404.2)
case paths node ci/check-case-paths.mjs pass
evidence node ci/check-no-evidence.mjs pass
svelte-check cd apps/web && bunx svelte-check --tsconfig ./tsconfig.json 2 errors, 16 warnings: the 2 errors are e2e/review4-signout.spec.ts:225-226 (never), which origin/dev has too (BM14c). No new error.

Playwright matrix (every project, one attempt each, retries 0)

Chromium half on dev-01, WebKit half on dev-02 at the same time, admin-setup on each, same .mts wrapper as BM14c.

VM / project Passed Failed Skipped
chromium / admin-chromium-1440 12 0 9
chromium / admin-chromium-390 12 0 9
chromium / admin-setup 1 0 0
chromium / chromium-1440 443 0 625
chromium / chromium-390 898 1 169
chromium / contrast 9 0 0
chromium / flow 1 0 0
chromium / look-chromium-1440 5 0 0
chromium / look-chromium-390 5 0 0
chromium / passkeys 1 0 0
chromium / prefs-chromium 7 0 0
chromium / sidebar-band-chromium 3 0 0
webkit / admin-setup 1 0 0
webkit / admin-webkit-1440 9 0 9
webkit / admin-webkit-390 9 0 9
webkit / look-webkit-1440 4 0 1
webkit / look-webkit-390 4 0 1
webkit / prefs-webkit 3 0 4
webkit / sidebar-band-webkit 3 0 0
webkit / webkit-1440 439 1 628
webkit / webkit-390 894 1 173
chromium / throttled-1440 * 3 0 2
chromium / throttled-390 * 5 0 0
Total 2771 3

* The throttled projects depend on the failed chromium-390 case, so Playwright skipped them in the matrix ("10 did not run"). Their first attempts were run right after with --no-deps on the same binary and dist: 8 passed, 0 failed (2 skipped by the spec).

Known flakes sidebar-edge.spec.ts:8 and vim.spec.ts:387 did not occur. webkit-390 missing-links.spec.ts:245 did occur (listed below).

First-run failures (3)

Comparison on origin/dev 41f2efa3 (built and run the same way, --no-deps, --repeat-each=10) and --repeat-each=10 --no-deps on the train, in isolation and as the whole spec file.

Case Project First error origin/dev x10 Train x10 (case) Train x10 (whole file)
note-bars.spec.ts:100 Attach > Divider puts a divider after the caret line chromium-390 Expected "attach", Received "tools" after 5 s spec does not exist on dev (no baseline possible) 10/10 pass 130/130 pass, 30 skipped by the spec
accent-labels.spec.ts:26 accent swatch names webkit-1440 accent radios: 0 found, expected 7 10/10 pass 10/10 pass 10/10 pass (dev 10/10 pass)
missing-links.spec.ts:245 renamed in the app webkit-390 URL did not change to the renamed note 10/10 pass 10/10 pass not run

All three are first-run failures that do not reproduce, on the train or on origin/dev. missing-links.spec.ts:245 is on the known-flake list. accent-labels.spec.ts:26 failed the same way in BM14c (first run only, 10/10 after) and is not on the known list. note-bars.spec.ts:100 is new in this stack (note-bars.spec.ts comes with #850) and its first-run failure is a menu-state timeout (5 s); it passes 140/140 afterwards. I count none of them as a train defect, but they are first-run failures and the orchestrator should know.

The 10 BM14c train-only failures (cover, style-sheet, note-bars, opacity, width) did not occur: #852 07b6770a holds the noteId fix.

## BM14d gates and matrix (first run, nothing retried) Head `c163abaea67b2a9b6874091b89e30c82d2e761a5`, base origin/dev `41f2efa387b74175971a838f602535089d0d5fb4`. `CI=1`, `SLOT_TRAIN=1`, train lock with EXIT trap on dev-01 and dev-02, on-disk TMPDIR, `--workers=1 --retries=0`, df before every step (dev-01 >= 190 GiB, dev-02 >= 300 GiB free). No fixes made. Perf results follow in a separate comment. | Gate | Command | Result | |---|---|---| | install (both VMs) | `bun install --frozen-lockfile` | pass | | fmt | `cargo fmt --all --check` | pass | | clippy | `cargo clippy --workspace --all-targets --locked -j4 -- -D warnings` | pass | | Rust | `cargo test --workspace -j4` | pass: 1497 passed, 0 failed | | cargo build (both) | `cargo build -j4` | pass | | web build (both) | `cd apps/web && bun run build` | pass | | bundle budgets | `PERF_DIST=$PWD/apps/web/dist perf/web.sh --bundle-only --gate` | pass (first JS 232.83 KB / 240, first CSS 19.18 KB / 22.1, total JS 363.4 KB / 404.2) | | case paths | `node ci/check-case-paths.mjs` | pass | | evidence | `node ci/check-no-evidence.mjs` | pass | | svelte-check | `cd apps/web && bunx svelte-check --tsconfig ./tsconfig.json` | 2 errors, 16 warnings: the 2 errors are `e2e/review4-signout.spec.ts:225-226` (`never`), which origin/dev has too (BM14c). **No new error.** | ## Playwright matrix (every project, one attempt each, retries 0) Chromium half on dev-01, WebKit half on dev-02 at the same time, `admin-setup` on each, same `.mts` wrapper as BM14c. | VM / project | Passed | Failed | Skipped | |---|---:|---:|---:| | chromium / admin-chromium-1440 | 12 | 0 | 9 | | chromium / admin-chromium-390 | 12 | 0 | 9 | | chromium / admin-setup | 1 | 0 | 0 | | chromium / chromium-1440 | 443 | 0 | 625 | | chromium / chromium-390 | 898 | **1** | 169 | | chromium / contrast | 9 | 0 | 0 | | chromium / flow | 1 | 0 | 0 | | chromium / look-chromium-1440 | 5 | 0 | 0 | | chromium / look-chromium-390 | 5 | 0 | 0 | | chromium / passkeys | 1 | 0 | 0 | | chromium / prefs-chromium | 7 | 0 | 0 | | chromium / sidebar-band-chromium | 3 | 0 | 0 | | webkit / admin-setup | 1 | 0 | 0 | | webkit / admin-webkit-1440 | 9 | 0 | 9 | | webkit / admin-webkit-390 | 9 | 0 | 9 | | webkit / look-webkit-1440 | 4 | 0 | 1 | | webkit / look-webkit-390 | 4 | 0 | 1 | | webkit / prefs-webkit | 3 | 0 | 4 | | webkit / sidebar-band-webkit | 3 | 0 | 0 | | webkit / webkit-1440 | 439 | **1** | 628 | | webkit / webkit-390 | 894 | **1** | 173 | | chromium / throttled-1440 * | 3 | 0 | 2 | | chromium / throttled-390 * | 5 | 0 | 0 | | **Total** | **2771** | **3** | | \* The throttled projects depend on the failed `chromium-390` case, so Playwright skipped them in the matrix ("10 did not run"). Their first attempts were run right after with `--no-deps` on the same binary and dist: 8 passed, 0 failed (2 skipped by the spec). Known flakes `sidebar-edge.spec.ts:8` and `vim.spec.ts:387` did not occur. webkit-390 `missing-links.spec.ts:245` did occur (listed below). ## First-run failures (3) Comparison on origin/dev `41f2efa3` (built and run the same way, `--no-deps`, `--repeat-each=10`) and `--repeat-each=10 --no-deps` on the train, in isolation and as the whole spec file. | Case | Project | First error | origin/dev x10 | Train x10 (case) | Train x10 (whole file) | |---|---|---|---|---|---| | `note-bars.spec.ts:100` Attach > Divider puts a divider after the caret line | chromium-390 | `Expected "attach", Received "tools"` after 5 s | spec does not exist on dev (no baseline possible) | 10/10 pass | 130/130 pass, 30 skipped by the spec | | `accent-labels.spec.ts:26` accent swatch names | webkit-1440 | accent radios: 0 found, expected 7 | 10/10 pass | 10/10 pass | 10/10 pass (dev 10/10 pass) | | `missing-links.spec.ts:245` renamed in the app | webkit-390 | URL did not change to the renamed note | 10/10 pass | 10/10 pass | not run | All three are first-run failures that do not reproduce, on the train or on origin/dev. `missing-links.spec.ts:245` is on the known-flake list. `accent-labels.spec.ts:26` failed the same way in BM14c (first run only, 10/10 after) and is not on the known list. `note-bars.spec.ts:100` is new in this stack (note-bars.spec.ts comes with #850) and its first-run failure is a menu-state timeout (5 s); it passes 140/140 afterwards. I count none of them as a train defect, but they are first-run failures and the orchestrator should know. The 10 BM14c train-only failures (cover, style-sheet, note-bars, opacity, width) did not occur: #852 `07b6770a` holds the `noteId` fix.
Author
Owner

BM14d perf: train vs origin/dev

Train c163abae against origin/dev 41f2efa3. Release binaries built on dev-01, relayed to perf-01 with matching SHA-256 (train calternal 364c9f3c…, dev 789dd476…, control dev2 = byte copy of dev 789dd476…; the same dev calternal-perf 5213cbd0… drives every arm). All measurements on perf-01, one session, ~/bin/perf-lock held on perf-01 and perf-02 (taken perf-01 first, then perf-02, held together for the whole run, released after). Seed 1, 10k Home for the browser. Every round runs all three arms (dev, train, dev2 control) in a rotating order, so each pair is interleaved and the dev-vs-dev control sits in the same session. Server: 5 rounds (each --runs 5, sizes 10k and 100k); append: 30 pairs; browser: 8 rounds. Values below are medians over rounds of the per-round p50 / p95. "control" = dev2 against dev.

Server (calternal-perf --sizes 10k,100k --runs 5 --cold-runs 0 --idle-secs 0 --storm-edits 1, ms)

metric notes dev p50 / p95 train p50 / p95 dev2 (control) p50 / p95 train vs dev p50 / p95 control vs dev p50 / p95 dev spread p50 train spread p50
home.first_page 10000 2.34 / 3.2 2.8 / 6.04 2.43 / 3.39 +19% / +89% +4% / +6% 2.18–2.76 2.1–4.84
home.first_page 100000 2.92 / 3.98 3.07 / 5.23 3.31 / 5.6 +5% / +31% +13% / +41% 2.72–3.1 2.7–3.77
home.page 10000 0.805 / 1.57 0.767 / 1.69 0.746 / 1.86 -5% / +8% -7% / +19% 0.716–0.812 0.726–0.907
home.page 100000 0.991 / 1.47 0.997 / 1.54 0.991 / 1.77 +1% / +5% -0% / +20% 0.829–1.1 0.935–1.13
home.page_big 10000 16.4 / 18.1 16.3 / 20.6 16.4 / 18 -0% / +14% -0% / -0% 16–17.4 16.2–18.4
home.page_big 100000 19.3 / 21.8 19.1 / 26.1 18.8 / 28.2 -1% / +20% -2% / +29% 18–19.8 18.9–19.6
home.page_next 10000 0.747 / 1.21 0.766 / 1.11 0.769 / 1.08 +3% / -8% +3% / -11% 0.708–0.785 0.75–0.815
home.page_next 100000 0.797 / 1.23 0.834 / 1.18 0.761 / 1.18 +5% / -4% -4% / -4% 0.78–0.955 0.775–0.841
home.page_tag 10000 5.07 / 6.19 5.06 / 6.42 5.09 / 6.48 -0% / +4% +0% / +5% 5.03–5.37 5.01–5.78
home.page_tag 100000 55.5 / 61.3 55 / 59.9 55.7 / 60.1 -1% / -2% +0% / -2% 55.1–57.3 54.8–55.3
home.page_title 10000 0.657 / 0.744 0.649 / 0.801 0.622 / 0.747 -1% / +8% -5% / +0% 0.606–0.85 0.614–0.794
home.page_title 100000 0.869 / 1.06 0.924 / 1.14 0.801 / 1.16 +6% / +8% -8% / +9% 0.832–0.99 0.806–0.985
journal.catch_up 10000 28.7 / 31.7 29.2 / 34.4 29.8 / 32.2 +2% / +9% +4% / +2% 28–33.5 28.5–32.5
journal.catch_up 100000 284 / 304 284 / 312 286 / 311 -0% / +3% +0% / +2% 271–301 277–305
mem.idle 10000 19.1 / 19.8 18.8 / 19.6 19 / 19.5 -2% / -1% -0% / -2% 19–19.7 18.7–19.5
mem.idle 100000 19.9 / 20.9 18.7 / 19.3 18.8 / 19.8 -6% / -8% -5% / -5% 19.1–20.4 18.5–18.9
mem.peak 10000 46.3 / 74.6 47.7 / 74.7 47 / 74.3 +3% / +0% +2% / -0% 45.3–46.9 45.1–48.3
mem.peak 100000 124 / 132 125 / 128 124 / 128 +0% / -3% -0% / -3% 121–126 122–127
open.api_get 10000 3.37 / 9.74 2.72 / 9.76 3.32 / 10.2 -19% / +0% -2% / +5% 2.7–3.61 2.59–3.38
open.api_get 100000 2.87 / 8.87 2.9 / 11 2.91 / 6 +1% / +24% +1% / -32% 2.85–3.04 2.81–3.42
open.long_note 10000 49 / 52.4 43.1 / 51.1 43.4 / 54.5 -12% / -2% -11% / +4% 41.9–50.5 42.5–43.9
open.long_note 100000 58.8 / 63.9 59.5 / 66.3 59.3 / 66.9 +1% / +4% +1% / +5% 58.5–60.3 57.9–62.5
open.note 10000 0.727 / 1.29 0.741 / 1.28 0.743 / 1.39 +2% / -1% +2% / +8% 0.686–0.75 0.716–0.779
open.note 100000 1.21 / 3.41 1.22 / 3.26 1.11 / 3.72 +1% / -5% -9% / +9% 1.08–1.34 1.12–1.42
search.first_query 10000 4.26 / 22.8 4.72 / 23.7 4.94 / 24.1 +11% / +4% +16% / +6% 3.84–6.27 3.84–5.34
search.first_query 100000 3.96 / 23.2 6.58 / 27.2 3.94 / 23.3 +66% / +17% -0% / +1% 3.69–4.82 4.04–7.47
search.query 10000 1.57 / 3.91 1.59 / 4.08 1.61 / 4.09 +2% / +4% +3% / +5% 1.49–1.63 1.56–1.73
search.query 100000 1.71 / 4.02 1.69 / 3.82 1.71 / 4 -1% / -5% +0% / -1% 1.64–1.93 1.66–1.8
shutdown 10000 63.2 / 84.2 43.2 / 64.1 43.1 / 64.2 -32% / -24% -32% / -24% 42.9–64 43–64
shutdown 100000 84.3 / 128 85.3 / 106 85.1 / 106 +1% / -17% +1% / -17% 64.1–85.2 64–85.4
startup.healthz 10000 4.92 / 6.91 5.55 / 8.1 4.92 / 7.06 +13% / +17% -0% / +2% 4.88–6.62 4.91–7.79
startup.healthz 100000 5.5 / 8.61 5.07 / 7.08 4.88 / 7.19 -8% / -18% -11% / -17% 4.94–7.03 4.88–5.98
sync.durable_write 10000 19.9 / 24.6 15.7 / 24.8 19.6 / 26.7 -21% / +1% -2% / +9% 15.3–21.7 15.5–20.5
sync.durable_write 100000 15.5 / 22.1 16.1 / 24.4 16.4 / 23.1 +4% / +10% +6% / +4% 15.1–16.2 15.8–16.6
sync.round_trip 10000 0.215 / 0.336 0.22 / 0.345 0.203 / 0.321 +2% / +3% -6% / -4% 0.211–0.231 0.206–0.253
sync.round_trip 100000 0.206 / 0.339 0.222 / 0.339 0.218 / 0.338 +8% / +0% +6% / -0% 0.203–0.24 0.209–0.249
sync.save_ack 10000 3.12 / 4.28 3.08 / 4.3 3 / 3.69 -1% / +0% -4% / -14% 2.42–3.22 2.46–3.14
sync.save_ack 100000 2.46 / 2.84 2.49 / 3.08 2.55 / 3.49 +1% / +8% +3% / +23% 2.4–2.6 2.44–2.99
  • sync.round_trip, the open risk (+17% p50 at 100k, n=3): now n=5 rounds x 5 runs. 10k: p50 +2%, p95 +3% (control -6% / -4%). 100k: p50 +8%, p95 +0% (control +6% / -0%); dev range 0.203-0.240, train range 0.209-0.249, overlapping. In the follow-up window below (100k only, 4 more rounds) dev 0.211 vs train 0.214 (+1.4%). I read it as flat: the owner fast path fixes hold.
  • open.note, notes.list pages (home.page, page_title, page_next, page_tag, page_big, first_page), search.query (prefix/typo mix): all inside the control's spread. The only p95 steps over 15% (home.first_page 10k p95 +89%, page_big 100k p95 +20%, open.api_get 100k p95 +24%) are single outlier rounds (a p95 of 5 one-sample runs); the p50s are flat and the control shows the same size of jumps in other rows.
  • search.first_query at 100k: p50 4.0 -> 6.6 ms (+66%), p95 +17%. Control flat (-0%). Train rounds 6.94, 6.25, 7.47, 4.04, 6.58 ms against dev 3.69, 4.82, 3.84, 3.96, 4.19 and dev2 3.68, 3.79, 3.94, 4.14, 4.26. That is separated from both in this session (one sample per run, the first query after a start). At 10k it is flat (+11%, control +16%).
    Follow-up, different window, 100k only, 4 rounds x 5 runs, bisect of the backend merges (each arm = train tree up to that merge): search.first_query p50 median: dev 4.65 (runs 4.01, 5.67, 5.28, 4.03), through #829 4.21, through #835 4.24, through #905 3.95, through #899 4.24, full train 4.26 (4.23, 5.15, 4.28, 4.13). Not reproduced: every merge, and the full train, is inside the dev range there. sync.round_trip in that window: 0.211 / 0.216 / 0.211 / 0.218 / 0.213 / 0.214 (dev / #829 / #835 / #905 / #899 / train); open.note 1.049 / 1.032 / 1.116 / 1.214 / 1.067 / 1.070 (#905 alone reads +16% there and its next merge (#899) and the full train read +2%, so I take it as noise); search.query 1.689 -> 1.702.
    So: a separated step in the protocol session, not reproduced in a second window and not attributable to any merge. Under the strict gate this is a regression beyond control noise; my reading is a window effect, but I cannot prove it.

200 appends (30 alternating pairs, perf-01, CALTERNAL_NOTES_SAVE_MS=15, same append.py as #792/BM14c)

Comparison median total, tree / dev delta per-append p50 per-append p95 pair ratios (min-max, median)
train vs dev 1.513 s / 1.497 s +1.11% 7.35 / 7.22 ms (+1.7%) 11.64 / 11.80 ms (-1.4%) 0.91-1.39, 0.996
noise control: dev2 (copy of dev) vs dev 1.530 s / 1.511 s +1.23% 7.35 / 7.28 ms (+0.9%) 11.26 / 12.84 ms (-12%) 0.95-1.06, 0.998
The train's +1.11% is no larger than the control's +1.23% (same binary), so it is run-to-run noise.

Browser (Chromium 4x CPU slowdown, 10k Home, perf/boot.perf.ts, list-search.perf.ts, blaze.perf.ts, 8 rounds per arm, each arm served by its own binary and dist; WebKit is not available on the perf VMs)

metric dev p50 / p95 train p50 / p95 dev2 control p50 / p95 train vs dev p50 / p95 control vs dev p50 / p95 rounds dev/train/ctl dev p50 range train p50 range
blaze.back_shown 30.25 / 44.8 30.65 / 47.1 30 / 45.4 +1.3% / +5.1% -0.8% / +1.3% 8/8/8 28.6–31 29.2–31.4
blaze.live 82.5 / 107.5 83 / 101 83 / 96.5 +0.6% / -6.0% +0.6% / -10.2% 8/8/8 81–85 82–84
blaze.long_task_max 141 / 141 144 / 144 146.5 / 146.5 +2.1% / +2.1% +3.9% / +3.9% 8/8/8 130–150 123–176
blaze.shown 35.85 / 58.3 36.35 / 58.75 36.1 / 63.05 +1.4% / +0.8% +0.7% / +8.1% 8/8/8 34.7–37 35.5–38.9
boot.cold_cpu 1288 / 1325 1241 / 1289 1257 / 1277 -3.7% / -2.8% -2.4% / -3.6% 8/8/8 1210–1329 1217–1268
boot.cold_fcp 704 / 720 706 / 724 702 / 732 +0.3% / +0.6% -0.3% / +1.7% 8/8/8 692–780 692–724
boot.cold_list 868 / 913 875.1 / 893.9 875.8 / 921.7 +0.8% / -2.1% +0.9% / +1.0% 8/8/8 789.6–973.9 783.4–903.8
boot.cold_note 885.5 / 919.5 884.8 / 905.8 888.6 / 921.7 -0.1% / -1.5% +0.4% / +0.2% 8/8/8 877.9–973.9 864.5–910.5
boot.cold_tbt 113 / 127.1 114.5 / 121.8 115.2 / 138.9 +1.3% / -4.1% +2.0% / +9.3% 8/8/8 85–139.6 77–127.4
boot.cold_tti 1743 / 1783 1745 / 1776 1741 / 1777 +0.1% / -0.4% -0.1% / -0.3% 8/8/8 1717–1821 1695–1784
boot.warm_list 296.8 / 592.6 298.1 / 591 297.5 / 603.2 +0.4% / -0.3% +0.3% / +1.8% 8/8/8 291.5–329.1 287.7–315.4
boot.warm_note 444.4 / 812 444.6 / 803.7 437.7 / 815.4 +0.0% / -1.0% -1.5% / +0.4% 8/8/8 399.6–484.3 433.6–462.9
list_search.key@10000 5.5 / 6.85 5.5 / 7.15 5.4 / 7.1 +0.0% / +4.4% -1.8% / +3.6% 8/8/8 5.3–5.6 5.4–5.7

Cold FCP (+0.3% p50 / +0.6% p95) and first note painted (cold_note, -0.1% / -1.5%) are flat against the control (-0.3% / +1.7%, +0.4% / +0.2%). list_search.key p50 +0.0%, p95 +4.4% (control +3.6%). warm_tti / warm_tbt medians are bimodal in dev itself (dev range 428-1200 ms) and the control moves as much. The BM14c cold-FCP step (+10%) is gone.

Bundle bytes (brotli KB, perf/web.sh --bundle-only, deterministic; dev built from 41f2efa3 the same way)

Metric dev train delta
first JS (index) 233.200 232.828 -0.372 (-0.2%)
first CSS 18.799 19.177 +0.378 (+2.0%)
total JS 353.255 363.406 +10.15 (+2.9%), of which idle-screens +10.42 (lazy)
all CSS 34.514 37.077 +2.56
Palette / ListSearch / vim chunks 20.847 / 3.014 / 37.876 20.901 / 3.027 / 37.919 +0.05 / +0.01 / +0.04
All inside budgets. First JS is smaller (#906's diet); first CSS is deterministically 0.38 KB larger (the style stack), without any visible effect on cold FCP.

Perf verdict

Flat against the control: sync.round_trip (both sizes), open.note, notes.list pages, search.query, 200 appends, cold FCP, first note, list_search.key, first JS. Not flat: search.first_query at 100k in the protocol session (+66% p50, +2.6 ms, not reproduced in the follow-up window), first CSS +0.38 KB (+2.0%, deterministic), total JS +10 KB (lazy chunk).
Raw data (server/browser JSON, append logs, bisect JSON) is attached as bm14d-perf-raw.tar.gz. Perf work dirs and binaries are removed from the perf VMs.

## BM14d perf: train vs origin/dev Train `c163abae` against origin/dev `41f2efa3`. Release binaries built on dev-01, relayed to perf-01 with matching SHA-256 (train `calternal` `364c9f3c…`, dev `789dd476…`, control `dev2` = byte copy of dev `789dd476…`; the same dev `calternal-perf` `5213cbd0…` drives every arm). All measurements on **perf-01**, one session, `~/bin/perf-lock` held on perf-01 **and** perf-02 (taken perf-01 first, then perf-02, held together for the whole run, released after). Seed 1, 10k Home for the browser. Every round runs all three arms (dev, train, dev2 control) in a rotating order, so each pair is interleaved and the dev-vs-dev control sits in the same session. Server: 5 rounds (each `--runs 5`, sizes 10k and 100k); append: 30 pairs; browser: 8 rounds. Values below are medians over rounds of the per-round p50 / p95. "control" = dev2 against dev. ### Server (`calternal-perf --sizes 10k,100k --runs 5 --cold-runs 0 --idle-secs 0 --storm-edits 1`, ms) | metric | notes | dev p50 / p95 | train p50 / p95 | dev2 (control) p50 / p95 | train vs dev p50 / p95 | control vs dev p50 / p95 | dev spread p50 | train spread p50 | |---|---|---|---|---|---|---|---|---| | home.first_page | 10000 | 2.34 / 3.2 | 2.8 / 6.04 | 2.43 / 3.39 | +19% / +89% | +4% / +6% | 2.18–2.76 | 2.1–4.84 | | home.first_page | 100000 | 2.92 / 3.98 | 3.07 / 5.23 | 3.31 / 5.6 | +5% / +31% | +13% / +41% | 2.72–3.1 | 2.7–3.77 | | home.page | 10000 | 0.805 / 1.57 | 0.767 / 1.69 | 0.746 / 1.86 | -5% / +8% | -7% / +19% | 0.716–0.812 | 0.726–0.907 | | home.page | 100000 | 0.991 / 1.47 | 0.997 / 1.54 | 0.991 / 1.77 | +1% / +5% | -0% / +20% | 0.829–1.1 | 0.935–1.13 | | home.page_big | 10000 | 16.4 / 18.1 | 16.3 / 20.6 | 16.4 / 18 | -0% / +14% | -0% / -0% | 16–17.4 | 16.2–18.4 | | home.page_big | 100000 | 19.3 / 21.8 | 19.1 / 26.1 | 18.8 / 28.2 | -1% / +20% | -2% / +29% | 18–19.8 | 18.9–19.6 | | home.page_next | 10000 | 0.747 / 1.21 | 0.766 / 1.11 | 0.769 / 1.08 | +3% / -8% | +3% / -11% | 0.708–0.785 | 0.75–0.815 | | home.page_next | 100000 | 0.797 / 1.23 | 0.834 / 1.18 | 0.761 / 1.18 | +5% / -4% | -4% / -4% | 0.78–0.955 | 0.775–0.841 | | home.page_tag | 10000 | 5.07 / 6.19 | 5.06 / 6.42 | 5.09 / 6.48 | -0% / +4% | +0% / +5% | 5.03–5.37 | 5.01–5.78 | | home.page_tag | 100000 | 55.5 / 61.3 | 55 / 59.9 | 55.7 / 60.1 | -1% / -2% | +0% / -2% | 55.1–57.3 | 54.8–55.3 | | home.page_title | 10000 | 0.657 / 0.744 | 0.649 / 0.801 | 0.622 / 0.747 | -1% / +8% | -5% / +0% | 0.606–0.85 | 0.614–0.794 | | home.page_title | 100000 | 0.869 / 1.06 | 0.924 / 1.14 | 0.801 / 1.16 | +6% / +8% | -8% / +9% | 0.832–0.99 | 0.806–0.985 | | journal.catch_up | 10000 | 28.7 / 31.7 | 29.2 / 34.4 | 29.8 / 32.2 | +2% / +9% | +4% / +2% | 28–33.5 | 28.5–32.5 | | journal.catch_up | 100000 | 284 / 304 | 284 / 312 | 286 / 311 | -0% / +3% | +0% / +2% | 271–301 | 277–305 | | mem.idle | 10000 | 19.1 / 19.8 | 18.8 / 19.6 | 19 / 19.5 | -2% / -1% | -0% / -2% | 19–19.7 | 18.7–19.5 | | mem.idle | 100000 | 19.9 / 20.9 | 18.7 / 19.3 | 18.8 / 19.8 | -6% / -8% | -5% / -5% | 19.1–20.4 | 18.5–18.9 | | mem.peak | 10000 | 46.3 / 74.6 | 47.7 / 74.7 | 47 / 74.3 | +3% / +0% | +2% / -0% | 45.3–46.9 | 45.1–48.3 | | mem.peak | 100000 | 124 / 132 | 125 / 128 | 124 / 128 | +0% / -3% | -0% / -3% | 121–126 | 122–127 | | open.api_get | 10000 | 3.37 / 9.74 | 2.72 / 9.76 | 3.32 / 10.2 | -19% / +0% | -2% / +5% | 2.7–3.61 | 2.59–3.38 | | open.api_get | 100000 | 2.87 / 8.87 | 2.9 / 11 | 2.91 / 6 | +1% / +24% | +1% / -32% | 2.85–3.04 | 2.81–3.42 | | open.long_note | 10000 | 49 / 52.4 | 43.1 / 51.1 | 43.4 / 54.5 | -12% / -2% | -11% / +4% | 41.9–50.5 | 42.5–43.9 | | open.long_note | 100000 | 58.8 / 63.9 | 59.5 / 66.3 | 59.3 / 66.9 | +1% / +4% | +1% / +5% | 58.5–60.3 | 57.9–62.5 | | open.note | 10000 | 0.727 / 1.29 | 0.741 / 1.28 | 0.743 / 1.39 | +2% / -1% | +2% / +8% | 0.686–0.75 | 0.716–0.779 | | open.note | 100000 | 1.21 / 3.41 | 1.22 / 3.26 | 1.11 / 3.72 | +1% / -5% | -9% / +9% | 1.08–1.34 | 1.12–1.42 | | search.first_query | 10000 | 4.26 / 22.8 | 4.72 / 23.7 | 4.94 / 24.1 | +11% / +4% | +16% / +6% | 3.84–6.27 | 3.84–5.34 | | search.first_query | 100000 | 3.96 / 23.2 | 6.58 / 27.2 | 3.94 / 23.3 | +66% / +17% | -0% / +1% | 3.69–4.82 | 4.04–7.47 | | search.query | 10000 | 1.57 / 3.91 | 1.59 / 4.08 | 1.61 / 4.09 | +2% / +4% | +3% / +5% | 1.49–1.63 | 1.56–1.73 | | search.query | 100000 | 1.71 / 4.02 | 1.69 / 3.82 | 1.71 / 4 | -1% / -5% | +0% / -1% | 1.64–1.93 | 1.66–1.8 | | shutdown | 10000 | 63.2 / 84.2 | 43.2 / 64.1 | 43.1 / 64.2 | -32% / -24% | -32% / -24% | 42.9–64 | 43–64 | | shutdown | 100000 | 84.3 / 128 | 85.3 / 106 | 85.1 / 106 | +1% / -17% | +1% / -17% | 64.1–85.2 | 64–85.4 | | startup.healthz | 10000 | 4.92 / 6.91 | 5.55 / 8.1 | 4.92 / 7.06 | +13% / +17% | -0% / +2% | 4.88–6.62 | 4.91–7.79 | | startup.healthz | 100000 | 5.5 / 8.61 | 5.07 / 7.08 | 4.88 / 7.19 | -8% / -18% | -11% / -17% | 4.94–7.03 | 4.88–5.98 | | sync.durable_write | 10000 | 19.9 / 24.6 | 15.7 / 24.8 | 19.6 / 26.7 | -21% / +1% | -2% / +9% | 15.3–21.7 | 15.5–20.5 | | sync.durable_write | 100000 | 15.5 / 22.1 | 16.1 / 24.4 | 16.4 / 23.1 | +4% / +10% | +6% / +4% | 15.1–16.2 | 15.8–16.6 | | sync.round_trip | 10000 | 0.215 / 0.336 | 0.22 / 0.345 | 0.203 / 0.321 | +2% / +3% | -6% / -4% | 0.211–0.231 | 0.206–0.253 | | sync.round_trip | 100000 | 0.206 / 0.339 | 0.222 / 0.339 | 0.218 / 0.338 | +8% / +0% | +6% / -0% | 0.203–0.24 | 0.209–0.249 | | sync.save_ack | 10000 | 3.12 / 4.28 | 3.08 / 4.3 | 3 / 3.69 | -1% / +0% | -4% / -14% | 2.42–3.22 | 2.46–3.14 | | sync.save_ack | 100000 | 2.46 / 2.84 | 2.49 / 3.08 | 2.55 / 3.49 | +1% / +8% | +3% / +23% | 2.4–2.6 | 2.44–2.99 | - **sync.round_trip, the open risk (+17% p50 at 100k, n=3)**: now n=5 rounds x 5 runs. 10k: p50 +2%, p95 +3% (control -6% / -4%). 100k: p50 +8%, p95 +0% (control +6% / -0%); dev range 0.203-0.240, train range 0.209-0.249, overlapping. In the follow-up window below (100k only, 4 more rounds) dev 0.211 vs train 0.214 (+1.4%). I read it as flat: the owner fast path fixes hold. - open.note, notes.list pages (`home.page`, `page_title`, `page_next`, `page_tag`, `page_big`, `first_page`), `search.query` (prefix/typo mix): all inside the control's spread. The only p95 steps over 15% (`home.first_page` 10k p95 +89%, `page_big` 100k p95 +20%, `open.api_get` 100k p95 +24%) are single outlier rounds (a p95 of 5 one-sample runs); the p50s are flat and the control shows the same size of jumps in other rows. - **`search.first_query` at 100k: p50 4.0 -> 6.6 ms (+66%), p95 +17%. Control flat (-0%).** Train rounds 6.94, 6.25, 7.47, 4.04, 6.58 ms against dev 3.69, 4.82, 3.84, 3.96, 4.19 and dev2 3.68, 3.79, 3.94, 4.14, 4.26. That is separated from both in this session (one sample per run, the first query after a start). At 10k it is flat (+11%, control +16%). **Follow-up, different window, 100k only, 4 rounds x 5 runs, bisect of the backend merges** (each arm = train tree up to that merge): search.first_query p50 median: dev 4.65 (runs 4.01, 5.67, 5.28, 4.03), through #829 4.21, through #835 4.24, through #905 3.95, through #899 4.24, full train 4.26 (4.23, 5.15, 4.28, 4.13). Not reproduced: every merge, and the full train, is inside the dev range there. sync.round_trip in that window: 0.211 / 0.216 / 0.211 / 0.218 / 0.213 / 0.214 (dev / #829 / #835 / #905 / #899 / train); open.note 1.049 / 1.032 / 1.116 / 1.214 / 1.067 / 1.070 (#905 alone reads +16% there and its next merge (#899) and the full train read +2%, so I take it as noise); search.query 1.689 -> 1.702. So: a separated step in the protocol session, not reproduced in a second window and not attributable to any merge. Under the strict gate this is a regression beyond control noise; my reading is a window effect, but I cannot prove it. ### 200 appends (30 alternating pairs, perf-01, `CALTERNAL_NOTES_SAVE_MS=15`, same `append.py` as #792/BM14c) | Comparison | median total, tree / dev | delta | per-append p50 | per-append p95 | pair ratios (min-max, median) | |---|---|---:|---:|---:|---| | train vs dev | 1.513 s / 1.497 s | **+1.11%** | 7.35 / 7.22 ms (+1.7%) | 11.64 / 11.80 ms (-1.4%) | 0.91-1.39, 0.996 | | noise control: dev2 (copy of dev) vs dev | 1.530 s / 1.511 s | **+1.23%** | 7.35 / 7.28 ms (+0.9%) | 11.26 / 12.84 ms (-12%) | 0.95-1.06, 0.998 | The train's +1.11% is no larger than the control's +1.23% (same binary), so it is run-to-run noise. ### Browser (Chromium 4x CPU slowdown, 10k Home, `perf/boot.perf.ts`, `list-search.perf.ts`, `blaze.perf.ts`, 8 rounds per arm, each arm served by its own binary and dist; WebKit is not available on the perf VMs) | metric | dev p50 / p95 | train p50 / p95 | dev2 control p50 / p95 | train vs dev p50 / p95 | control vs dev p50 / p95 | rounds dev/train/ctl | dev p50 range | train p50 range | |---|---|---|---|---|---|---|---|---| | blaze.back_shown | 30.25 / 44.8 | 30.65 / 47.1 | 30 / 45.4 | +1.3% / +5.1% | -0.8% / +1.3% | 8/8/8 | 28.6–31 | 29.2–31.4 | | blaze.live | 82.5 / 107.5 | 83 / 101 | 83 / 96.5 | +0.6% / -6.0% | +0.6% / -10.2% | 8/8/8 | 81–85 | 82–84 | | blaze.long_task_max | 141 / 141 | 144 / 144 | 146.5 / 146.5 | +2.1% / +2.1% | +3.9% / +3.9% | 8/8/8 | 130–150 | 123–176 | | blaze.shown | 35.85 / 58.3 | 36.35 / 58.75 | 36.1 / 63.05 | +1.4% / +0.8% | +0.7% / +8.1% | 8/8/8 | 34.7–37 | 35.5–38.9 | | boot.cold_cpu | 1288 / 1325 | 1241 / 1289 | 1257 / 1277 | -3.7% / -2.8% | -2.4% / -3.6% | 8/8/8 | 1210–1329 | 1217–1268 | | boot.cold_fcp | 704 / 720 | 706 / 724 | 702 / 732 | +0.3% / +0.6% | -0.3% / +1.7% | 8/8/8 | 692–780 | 692–724 | | boot.cold_list | 868 / 913 | 875.1 / 893.9 | 875.8 / 921.7 | +0.8% / -2.1% | +0.9% / +1.0% | 8/8/8 | 789.6–973.9 | 783.4–903.8 | | boot.cold_note | 885.5 / 919.5 | 884.8 / 905.8 | 888.6 / 921.7 | -0.1% / -1.5% | +0.4% / +0.2% | 8/8/8 | 877.9–973.9 | 864.5–910.5 | | boot.cold_tbt | 113 / 127.1 | 114.5 / 121.8 | 115.2 / 138.9 | +1.3% / -4.1% | +2.0% / +9.3% | 8/8/8 | 85–139.6 | 77–127.4 | | boot.cold_tti | 1743 / 1783 | 1745 / 1776 | 1741 / 1777 | +0.1% / -0.4% | -0.1% / -0.3% | 8/8/8 | 1717–1821 | 1695–1784 | | boot.warm_list | 296.8 / 592.6 | 298.1 / 591 | 297.5 / 603.2 | +0.4% / -0.3% | +0.3% / +1.8% | 8/8/8 | 291.5–329.1 | 287.7–315.4 | | boot.warm_note | 444.4 / 812 | 444.6 / 803.7 | 437.7 / 815.4 | +0.0% / -1.0% | -1.5% / +0.4% | 8/8/8 | 399.6–484.3 | 433.6–462.9 | | list_search.key@10000 | 5.5 / 6.85 | 5.5 / 7.15 | 5.4 / 7.1 | +0.0% / +4.4% | -1.8% / +3.6% | 8/8/8 | 5.3–5.6 | 5.4–5.7 | Cold FCP (+0.3% p50 / +0.6% p95) and first note painted (`cold_note`, -0.1% / -1.5%) are flat against the control (-0.3% / +1.7%, +0.4% / +0.2%). `list_search.key` p50 +0.0%, p95 +4.4% (control +3.6%). `warm_tti` / `warm_tbt` medians are bimodal in dev itself (dev range 428-1200 ms) and the control moves as much. The BM14c cold-FCP step (+10%) is gone. ### Bundle bytes (brotli KB, `perf/web.sh --bundle-only`, deterministic; dev built from `41f2efa3` the same way) | Metric | dev | train | delta | |---|---:|---:|---:| | first JS (`index`) | 233.200 | 232.828 | -0.372 (-0.2%) | | first CSS | 18.799 | 19.177 | **+0.378 (+2.0%)** | | total JS | 353.255 | 363.406 | +10.15 (+2.9%), of which `idle-screens` +10.42 (lazy) | | all CSS | 34.514 | 37.077 | +2.56 | | Palette / ListSearch / vim chunks | 20.847 / 3.014 / 37.876 | 20.901 / 3.027 / 37.919 | +0.05 / +0.01 / +0.04 | All inside budgets. First JS is smaller (#906's diet); first CSS is deterministically 0.38 KB larger (the style stack), without any visible effect on cold FCP. ### Perf verdict Flat against the control: sync.round_trip (both sizes), open.note, notes.list pages, search.query, 200 appends, cold FCP, first note, list_search.key, first JS. Not flat: `search.first_query` at 100k in the protocol session (+66% p50, +2.6 ms, not reproduced in the follow-up window), first CSS +0.38 KB (+2.0%, deterministic), total JS +10 KB (lazy chunk). Raw data (server/browser JSON, append logs, bisect JSON) is attached as `bm14d-perf-raw.tar.gz`. Perf work dirs and binaries are removed from the perf VMs.
Author
Owner

BM14d result: RED (strict reading). Do not merge. The access-control review of the #829/#835 owner fast path is still needed.

Head c163abaea67b2a9b6874091b89e30c82d2e761a5, base origin/dev 41f2efa387b74175971a838f602535089d0d5fb4. Merged --no-ff in order, heads verified: #850 6eb81f62, #851 d94771b6, #852 07b6770a, #829 72eb0eb1, #835 46be23e3, #895 3c1d8deb, #905 8973b7e6, #899 8484849c, #906 809a33c2. No conflicts, no fixes, no retries.

  • Gates: all pass (fmt, clippy, 1497 Rust tests, builds, bundle gate, case paths, evidence); svelte-check has only the 2 errors origin/dev has too.
  • Matrix: 2771 passed, 3 first-run failures, none reproduce (train x10 and origin/dev x10 pass): note-bars.spec.ts:100 chromium-390 (spec new with #850, no baseline; 140/140 pass afterwards), accent-labels.spec.ts:26 webkit-1440, and the known flake missing-links.spec.ts:245 webkit-390. accent-labels:26 and note-bars:100 are not on the known list, so on a strict "first run counts" reading they keep the train from being GREEN. The 10 BM14c style failures are gone.
  • Perf: flat against the control on sync.round_trip (10k and 100k), open.note, notes.list pages, search prefix/typo, list_search.key, 200 appends, cold FCP and first note. Open: search.first_query at 100k +66% p50 (+2.6 ms) in the protocol session, not reproduced in a second window or in any per-merge bisect; first CSS +0.38 KB (+2.0%) and total JS +10 KB (lazy).
    Tables and raw data are in the two comments above.
## BM14d result: RED (strict reading). Do not merge. The access-control review of the #829/#835 owner fast path is still needed. Head `c163abaea67b2a9b6874091b89e30c82d2e761a5`, base origin/dev `41f2efa387b74175971a838f602535089d0d5fb4`. Merged `--no-ff` in order, heads verified: #850 `6eb81f62`, #851 `d94771b6`, #852 `07b6770a`, #829 `72eb0eb1`, #835 `46be23e3`, #895 `3c1d8deb`, #905 `8973b7e6`, #899 `8484849c`, #906 `809a33c2`. No conflicts, no fixes, no retries. - Gates: all pass (fmt, clippy, 1497 Rust tests, builds, bundle gate, case paths, evidence); svelte-check has only the 2 errors origin/dev has too. - Matrix: 2771 passed, 3 first-run failures, none reproduce (train x10 and origin/dev x10 pass): `note-bars.spec.ts:100` chromium-390 (spec new with #850, no baseline; 140/140 pass afterwards), `accent-labels.spec.ts:26` webkit-1440, and the known flake `missing-links.spec.ts:245` webkit-390. `accent-labels:26` and `note-bars:100` are not on the known list, so on a strict "first run counts" reading they keep the train from being GREEN. The 10 BM14c style failures are gone. - Perf: flat against the control on sync.round_trip (10k and 100k), open.note, notes.list pages, search prefix/typo, `list_search.key`, 200 appends, cold FCP and first note. Open: `search.first_query` at 100k +66% p50 (+2.6 ms) in the protocol session, not reproduced in a second window or in any per-merge bisect; first CSS +0.38 KB (+2.0%) and total JS +10 KB (lazy). Tables and raw data are in the two comments above.
Author
Owner

BM14d follow-up: first CSS diet (PR #908) and search.first_query at 10 rounds

1. First-paint CSS (brotli q11, exact bytes from apps/web/scripts/bundle-size.mjs)

build first CSS bytes
dev 41f2efa3 19250
train c163abae 19637 (+387)
train + diet (#908, 2bb672ff) 18998 (-252 vs dev, -639 vs train)

Per commit (each measured on its own build): d9f206c9 -31 B (pressed/disabled/danger/done and tall-pill rules to note-bars-lazy.css), 03e9e1c3 -28 B (sheet-open room rule loads with clear-sheet), 93585a40 -92 B (dead bar-stack/bar-line/bar-group), a0c7130a -30 B (unused .visually-hidden), 2bb672ff -458 B (picker list CSS for @ [[ # loads after first paint, the picker's first lookup waits for it). First JS unchanged (232.8 KB).

Pixel check: first note (headings, bold/italic, link, tag, list, tasks, quote, code), chromium 1440 and 390, light and dark (&theme=), fixed clock, list pane masked (it shows wall-clock times). train vs train+diet: all four PNGs byte-identical (sha256 of the first 8: 1440 dark b197bb0e, 1440 light 82edfb27, 390 dark 745b0d97, 390 light a82606ce). A second train run is also byte-identical to the first, so the check can see real changes.

Specs on the diet build (chromium+webkit, 1440+390, retries 0): link-suggest, tag-picker, glass-roots, glass, pill-room, tint, note-bars, note-bars-fit, style-sheet, sheet-geometry, bar-rest, lazy-sheet-recovery: 407 passed, 0 failed, 157 skipped (project filters). The full-suite train gate for the diet still belongs to a train.

2. search.first_query, 10 rounds, interleaved

perf-01, ~/bin/perf-lock held on perf-01 and perf-02 for the whole run (released after). Fresh release builds on dev-01: dev bd6ab4d4… (41f2efa3), train 38f24b87… (c163abae), dev2 = byte copy of dev (bd6ab4d4…), one calternal-perf (7d319e72…) for every arm. Each round runs the three arms in rotating order; calternal-perf --sizes 10k,100k --runs 5 --cold-runs 0 --idle-secs 0 --storm-edits 1, one fixture Home per arm (round 0 builds them, not counted). Cells are per-round p50 / p95 in ms.

100k

round dev train dev2 (control)
1 5.29 / 43.0 5.13 / 29.7 6.97 / 24.9
2 4.54 / 23.6 13.61 / 23.8 4.57 / 24.0
3 8.78 / 29.2 8.43 / 24.0 4.75 / 23.0
4 7.61 / 22.7 11.30 / 33.8 12.04 / 26.5
5 3.99 / 25.4 13.62 / 35.2 5.35 / 22.9
6 5.42 / 29.1 5.11 / 38.4 5.07 / 23.4
7 5.13 / 28.3 6.67 / 40.8 4.87 / 27.9
8 6.24 / 27.1 6.95 / 25.4 6.09 / 28.1
9 17.30 / 35.5 5.64 / 25.4 6.14 / 26.6
10 8.87 / 33.1 5.14 / 24.9 7.45 / 36.3
median p50 / p95 5.83 / 28.7 6.81 / 27.6 5.72 / 25.7
mean p50 7.32 8.16 6.33
min-max p50 (sd) 3.99-17.30 (3.70) 5.11-13.62 (3.27) 4.57-12.04 (2.11)

10k

round dev train dev2 (control)
1 4.93 / 40.4 5.13 / 22.7 3.85 / 27.4
2 4.67 / 23.6 4.17 / 22.9 5.97 / 40.2
3 3.84 / 41.4 4.20 / 22.3 4.53 / 23.2
4 4.51 / 23.6 6.51 / 25.8 6.25 / 24.8
5 4.77 / 25.2 4.70 / 21.9 4.52 / 22.9
6 4.69 / 24.0 5.24 / 41.7 4.21 / 22.8
7 4.58 / 23.3 4.00 / 24.1 3.88 / 18.1
8 4.64 / 26.5 5.39 / 37.1 4.90 / 24.5
9 4.98 / 26.5 4.41 / 24.4 4.63 / 18.7
10 8.69 / 44.6 4.72 / 26.5 7.23 / 36.1
median p50 / p95 4.68 / 25.9 4.71 / 24.2 4.58 / 23.8
min-max p50 (sd) 3.84-8.69 (1.25) 4.00-6.51 (0.72) 3.85-7.23 (1.06)

Train vs dev (median p50): 10k +0.7% (control -2.2%); 100k +16.8% (control -1.9%). Paired per round (train minus dev p50, 100k): median +0.27 ms, train lower in 5 of 10 rounds; 10k: median +0.06 ms, 5 of 10.

Verdict: not a regression; noise, no reproducible effect. At 10k it is flat. At 100k the medians differ (+1.0 ms) but dev itself ranges 3.99-17.30 ms across rounds (sd 3.7 ms), so the gap is a third of dev's own spread: train is lower than dev in half the rounds, high outliers hit every arm (dev 17.3, dev2 12.0, train 13.6/13.6/11.3), and the dev-vs-dev control sits on dev (-1.9%). The 100k p95 is flat or lower (27.6 vs 28.7). This agrees with the earlier per-merge bisect, where no merge moved it. The metric is one first query per fresh start, and this session was noisier than the first one (dev's p50 was 4.0 there, 5.8 here; the perf VMs share a disk with other jobs, which were queued on the same lock). I did not bisect: there is nothing to bisect. If you want a tighter read, search.first_query needs more than 5 samples per round (it is one sample per run).

Both perf locks released (my units fcd-perf, fcd-hold stopped). Never merged.

## BM14d follow-up: first CSS diet (PR #908) and `search.first_query` at 10 rounds ### 1. First-paint CSS (brotli q11, exact bytes from `apps/web/scripts/bundle-size.mjs`) | build | first CSS bytes | |---|---:| | dev 41f2efa3 | **19250** | | train c163abae | **19637** (+387) | | train + diet (#908, 2bb672ff) | **18998** (**-252 vs dev**, -639 vs train) | Per commit (each measured on its own build): d9f206c9 -31 B (pressed/disabled/danger/done and tall-pill rules to note-bars-lazy.css), 03e9e1c3 -28 B (sheet-open room rule loads with clear-sheet), 93585a40 -92 B (dead bar-stack/bar-line/bar-group), a0c7130a -30 B (unused .visually-hidden), 2bb672ff -458 B (picker list CSS for `@` `[[` `#` loads after first paint, the picker's first lookup waits for it). First JS unchanged (232.8 KB). **Pixel check:** first note (headings, bold/italic, link, tag, list, tasks, quote, code), chromium 1440 and 390, light and dark (`&theme=`), fixed clock, list pane masked (it shows wall-clock times). train vs train+diet: all four PNGs byte-identical (sha256 of the first 8: 1440 dark b197bb0e, 1440 light 82edfb27, 390 dark 745b0d97, 390 light a82606ce). A second train run is also byte-identical to the first, so the check can see real changes. Specs on the diet build (chromium+webkit, 1440+390, retries 0): link-suggest, tag-picker, glass-roots, glass, pill-room, tint, note-bars, note-bars-fit, style-sheet, sheet-geometry, bar-rest, lazy-sheet-recovery: **407 passed, 0 failed**, 157 skipped (project filters). The full-suite train gate for the diet still belongs to a train. ### 2. `search.first_query`, 10 rounds, interleaved perf-01, `~/bin/perf-lock` held on perf-01 and perf-02 for the whole run (released after). Fresh release builds on dev-01: dev `bd6ab4d4…` (41f2efa3), train `38f24b87…` (c163abae), dev2 = byte copy of dev (`bd6ab4d4…`), one `calternal-perf` (`7d319e72…`) for every arm. Each round runs the three arms in rotating order; `calternal-perf --sizes 10k,100k --runs 5 --cold-runs 0 --idle-secs 0 --storm-edits 1`, one fixture Home per arm (round 0 builds them, not counted). Cells are per-round p50 / p95 in ms. **100k** | round | dev | train | dev2 (control) | |---:|---|---|---| | 1 | 5.29 / 43.0 | 5.13 / 29.7 | 6.97 / 24.9 | | 2 | 4.54 / 23.6 | 13.61 / 23.8 | 4.57 / 24.0 | | 3 | 8.78 / 29.2 | 8.43 / 24.0 | 4.75 / 23.0 | | 4 | 7.61 / 22.7 | 11.30 / 33.8 | 12.04 / 26.5 | | 5 | 3.99 / 25.4 | 13.62 / 35.2 | 5.35 / 22.9 | | 6 | 5.42 / 29.1 | 5.11 / 38.4 | 5.07 / 23.4 | | 7 | 5.13 / 28.3 | 6.67 / 40.8 | 4.87 / 27.9 | | 8 | 6.24 / 27.1 | 6.95 / 25.4 | 6.09 / 28.1 | | 9 | 17.30 / 35.5 | 5.64 / 25.4 | 6.14 / 26.6 | | 10 | 8.87 / 33.1 | 5.14 / 24.9 | 7.45 / 36.3 | | median p50 / p95 | 5.83 / 28.7 | 6.81 / 27.6 | 5.72 / 25.7 | | mean p50 | 7.32 | 8.16 | 6.33 | | min-max p50 (sd) | 3.99-17.30 (3.70) | 5.11-13.62 (3.27) | 4.57-12.04 (2.11) | **10k** | round | dev | train | dev2 (control) | |---:|---|---|---| | 1 | 4.93 / 40.4 | 5.13 / 22.7 | 3.85 / 27.4 | | 2 | 4.67 / 23.6 | 4.17 / 22.9 | 5.97 / 40.2 | | 3 | 3.84 / 41.4 | 4.20 / 22.3 | 4.53 / 23.2 | | 4 | 4.51 / 23.6 | 6.51 / 25.8 | 6.25 / 24.8 | | 5 | 4.77 / 25.2 | 4.70 / 21.9 | 4.52 / 22.9 | | 6 | 4.69 / 24.0 | 5.24 / 41.7 | 4.21 / 22.8 | | 7 | 4.58 / 23.3 | 4.00 / 24.1 | 3.88 / 18.1 | | 8 | 4.64 / 26.5 | 5.39 / 37.1 | 4.90 / 24.5 | | 9 | 4.98 / 26.5 | 4.41 / 24.4 | 4.63 / 18.7 | | 10 | 8.69 / 44.6 | 4.72 / 26.5 | 7.23 / 36.1 | | median p50 / p95 | 4.68 / 25.9 | 4.71 / 24.2 | 4.58 / 23.8 | | min-max p50 (sd) | 3.84-8.69 (1.25) | 4.00-6.51 (0.72) | 3.85-7.23 (1.06) | Train vs dev (median p50): 10k **+0.7%** (control -2.2%); 100k **+16.8%** (control -1.9%). Paired per round (train minus dev p50, 100k): median +0.27 ms, train lower in 5 of 10 rounds; 10k: median +0.06 ms, 5 of 10. **Verdict: not a regression; noise, no reproducible effect.** At 10k it is flat. At 100k the medians differ (+1.0 ms) but dev itself ranges 3.99-17.30 ms across rounds (sd 3.7 ms), so the gap is a third of dev's own spread: train is lower than dev in half the rounds, high outliers hit every arm (dev 17.3, dev2 12.0, train 13.6/13.6/11.3), and the dev-vs-dev control sits on dev (-1.9%). The 100k p95 is flat or lower (27.6 vs 28.7). This agrees with the earlier per-merge bisect, where no merge moved it. The metric is one first query per fresh start, and this session was noisier than the first one (dev's p50 was 4.0 there, 5.8 here; the perf VMs share a disk with other jobs, which were queued on the same lock). I did not bisect: there is nothing to bisect. If you want a tighter read, `search.first_query` needs more than 5 samples per round (it is one sample per run). Both perf locks released (my units `fcd-perf`, `fcd-hold` stopped). Never merged.
Author
Owner

Landed in dev via 12103f1c97 — closing.

Landed in dev via 12103f1c973f1e810dd4a9a3bff5a705514d70b0 — closing.
kayg closed this pull request 2026-11-07 17:53:13 +00:00
Some checks failed
CI / server (pull_request) Has been cancelled
CI / rust (pull_request) Has been cancelled
CI / web (pull_request) Has been cancelled
Markdown extraction contract / markdown-native-contract (pull_request) Has been cancelled
Markdown extraction contract / markdown-editor-contract (pull_request) Has been cancelled

Pull request closed

Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
kayg/calternal!907
No description provided.