WIP: perf: executor writes cost what they touch (5-20x faster; 40 ms gate not yet met) #898
Draft
kayg
wants to merge 3 commits from
perf/executor-hot-path into dev
pull from: perf/executor-hot-path
merge into: kayg:dev
kayg:main
kayg:feat/style-sheet-fixes
kayg:storage/wal-checkpoint-off-save-1051
kayg:t3/paged-search-reconcile-final
kayg:feat/pass8
kayg:review/calendar-2
kayg:fix/shared-row-swipes
kayg:feat/contrast
kayg:dev
kayg:feat/collections-db
kayg:fix/instant-keys-1056
kayg:feat/money-round
kayg:perf/stalls-1054
kayg:wip/tiering-v3-cold4
kayg:wip/tiering-v3-cold5
kayg:port/rows-actions
kayg:port/rows-drop
kayg:port/hidden-blocks
kayg:port/rows
kayg:port/port-tables
kayg:port/port-tables-2
kayg:t3/mail-1011-liveness
kayg:port/devices
kayg:port/port-attachments
kayg:port/port-attachments-2
kayg:port/port-attachments-3
kayg:docs/design-consistency
kayg:docs/engineering-standards
kayg:fix/rusqlite-0.41-rustsec-2026-0459
kayg:feat/iphone-fixes
kayg:shared/restore-and-stale-918-919
kayg:fix/phone-top-band-contrast
kayg:feat/notes-approve
kayg:perf/soak-max-gate
kayg:feat/files-s1-identity
kayg:fix/lifecycle-event-id
kayg:feat/quickfix
kayg:wip/tiering-v3-cold
kayg:perf/web-mockup-frame-fixes
kayg:port/contacts-merge
kayg:port/contacts-edit
kayg:test/shared-room-no-deleted-text
kayg:perf/first-paint-credits
kayg:fix/shared-history-after-grant
kayg:fix/phone-swipe-regressions
kayg:feat/contrast-r1
kayg:review/sharing-pr3-review
kayg:feat/tiering-v3-s2-tags
kayg:feat/folder-drop
kayg:fix/deskpills-lazy-share
kayg:feat/sharing-s1-pr3
kayg:tmp/money-round-before
kayg:fix/phone-pane-backdrop-roots
kayg:fix/palette-preload-chips
kayg:fix/invite-defaults
kayg:port/tasks-matrix
kayg:fix/restore-green-gates
kayg:fix/sharing-fast-path-hardening
kayg:t3/photos-s1
kayg:feat/tiering-v3-s1-namespace
kayg:notes-archive-hide
kayg:feat/tasks-actions
kayg:t3/photos-plan-slice-one
kayg:feat/whiteboard-s1
kayg:t3/paged-search-reconcile
kayg:t3/smooth-pane-transitions
kayg:t3/index-position-startup-scan
kayg:fix/private-staging-759b-r2
kayg:feat/dav-transport
kayg:feat/imap-notes-fetch
kayg:feat/imap-notes-projection
kayg:feat/imap-notes-readonly
kayg:feat/imap-notes-auth
kayg:feat/imap-notes-server-foundation
kayg:docs/undo-block-ids-plan
kayg:fix/undo-768
kayg:proto/block-identity-v7d
kayg:issue/carddav-read-perf-fix-r2
kayg:issue/carddav-read-r2
kayg:docs/imap-766-plan-v2
kayg:docs/imap-notes-plan
kayg:docs/quota-plan-2
kayg:fix/parallel-test-flakes
kayg:feat/contacts-companion-pure
kayg:docs/calendar-backend-v2-plan
kayg:feat/calendar-ical-converter
kayg:docs/tasks-writes-v2-plan
kayg:docs/carddav-writes-v2-plan
kayg:docs/contacts-import-v2-plan
kayg:issue/610-pm-midnight
kayg:feat/calendar-v2-s1-containment
kayg:docs/dav-recorder-v2-plan
kayg:plan/previews-865
kayg:plan/save-log-durable-entry
kayg:fix/e2e-data-on-ram
kayg:feat/appwindow3
kayg:feat/appwindow2
kayg:feat/appwindow
kayg:feat/money-p1
kayg:feat/keys-sheet
kayg:review/calendar-2-themes
kayg:feat/smooth-cursor
kayg:fix/float-style
kayg:review/calendar-2-float
kayg:release/2026-11-06-hotfix
kayg:archive/port/rows-drop-code
kayg:archive/port/rows-actions
kayg:archive/port/rows
kayg:scratch/tasks-894-ui-gates
kayg:review/calendar-2-night-wip
kayg:release/2026-11-05
kayg:archive/feat/tiering-v3-s1-namespace
kayg:archive/flaky/rows-drop-fixed
kayg:archive/flaky/rows-actions
kayg:archive/feat/tiering-v3-reader
kayg:archive/feat/tiering-v3-roots
kayg:archive/flaky/rows
kayg:sim/bm15-final
kayg:sim/bm15-fix
kayg:sim/bm15-bis-932
kayg:sim/bm15-bis-909
kayg:sim/bm15
kayg:sim/bm15-bis-887
kayg:perf/tiering-option-b
kayg:port/style-3
kayg:port/style-2
kayg:port/style
kayg:fix/sw-lazy-precache
kayg:fix/vim-w-waits-for-save
kayg:fix/signout-test-races
kayg:fp-scratch/all
kayg:sim/bm15-first-paint
kayg:archive/bm15/rows-drop
kayg:archive/bm15/rows-actions
kayg:fix/palette-snippet-contrast
kayg:archive/bm15/rows
kayg:archive/bm15/hidden
kayg:archive/plan/save-log-durable-entry
kayg:mock/money-fix2
kayg:archive/fix/e2e-data-on-ram
kayg:mock/appearance-all
kayg:mock/pill-press
kayg:archive/docs/design-consistency
kayg:mock/auto-swipes
kayg:mock/mail-actions
kayg:archive/feat/mail-p1
kayg:mock/money-static
kayg:archive/review/standards-917
kayg:archive/docs/engineering-standards
kayg:perf/tiering-761-fix
kayg:mock/type-audit
kayg:mock/third-pane
kayg:feat/contacts-import-v2-s1
kayg:archive/port/hidden-blocks
kayg:archive/port/devices
kayg:scratch/executor-on-894
kayg:scratch/executor-spans2
kayg:scratch/executor-spans
kayg:train/2026-11-03-bm14c
kayg:archive/port/rows-drop
kayg:feat/tiering-761-d-scale
kayg:feat/tiering-761-c-index
kayg:feat/tiering-761-b-reader
kayg:archive/review/sharing-pr3-review-9fec3d1f
kayg:train/2026-11-03-bm14b
kayg:perf/ports-lazy-budgets
kayg:feat/tiering-761-a-migration
kayg:train/2026-11-03-bm14
kayg:train/2026-11-03-bm12
kayg:archive/review/sharing-pr3-review
kayg:archive/feat/sharing-s1-pr3
kayg:train/2026-11-03-bm11
kayg:archive/feat/imap-notes-fetch
kayg:train/2026-11-03-bm10
kayg:archive/review/sharing-pr3-review-code
kayg:archive/plan/previews-865
kayg:train/2026-11-02-bm9
kayg:archive/port/port-attachments-3
kayg:archive/port/contacts-merge
kayg:fix/motion-audit-859
kayg:train/2026-11-02-bm8
kayg:archive/scratch/bm8-prep
kayg:archive/port/port-pickers
kayg:archive/port/port-tables-2
kayg:archive/port/port-tables
kayg:archive/feat/sharing-s1-pr3-code
kayg:archive/scratch/bm7-fixed
kayg:train/2026-11-02-bm7
kayg:archive/fix/parallel-test-flakes-code-minipc01
kayg:train/2026-11-02-bm6
kayg:docs/imap-766-readonly-slice
kayg:feat/dav-recorder-v3
kayg:research/788-diagnosis
kayg:archive/fix/parallel-test-flakes
kayg:archive/perf/pr792-cold-index-edit
kayg:docs/sharing-slice-1
kayg:archive/docs/imap-766-plan-v2
kayg:feat/dav-recorder-v2
kayg:archive/docs/dav-recorder-v2-plan
kayg:spike/imap-766-s0-cases
kayg:archive/fix/undo-768
kayg:feat/dav-recorder
kayg:archive/docs/contacts-import-v2-plan
kayg:archive/feat/calendar-ical-converter
kayg:feat/contacts-import
kayg:perf/cold-growth-on-764
kayg:perf/cold-growth-on-tiering
kayg:archive/docs/tasks-writes-v2-plan
kayg:archive/docs/calendar-backend-v2-plan
kayg:archive/feat/contacts-companion-pure
kayg:feat/tasks-set-status
kayg:feat/tasks-move-reorder
kayg:archive/docs/carddav-writes-v2-plan
kayg:archive/feat/calendar-v2-s1-containment
kayg:feat/tasks-delete-promote-duplicate
kayg:archive/feat/dav-transport
kayg:archive/fix/carddav-rebuild-r1
kayg:archive/fix/carddav-retention-r1
kayg:feat/carddav-writes
kayg:perf/quota-on-tiering
kayg:perf/quota-simple
kayg:perf/cold-growth-on-quota-stack
kayg:feat/calendar-identity
kayg:feat/calendar-event-parser
kayg:feat/calendar-props-maps
kayg:port/glass-when-ring
kayg:feat/tiering-761-s1
kayg:wip/10by32-01-impl-759b-scratch-20261030
kayg:wip/6by32-01-bm3-webkit-pause-20261030
kayg:wip/10by32-01-bm3-chromium-pause-20261030
kayg:wip/6by32-01-tier-761-s1-pause-20261030
kayg:spike/imap-766-s0
kayg:wip/6by32-02-calternal-pause-20261030
kayg:wip/8by64-01-review-688-pause-20261030
kayg:wip/6by32-01-verify-744-pause-20261030
kayg:wip/6by32-01-verify-742-pause-20261030
kayg:fix/private-staging-759b
kayg:wip/6by32-01-review-748-pause-20261030
kayg:wip/6by32-01-review-733c-pause-20261030
kayg:wip/6by32-01-review-733b-pause-20261030
kayg:wip/6by32-01-perf-idle-8-pause-20261030
kayg:wip/6by32-01-perf767-base-pause-20261030
kayg:wip/6by32-01-perf767b-767-pause-20261030
kayg:wip/6by32-01-perf767b-764-pause-20261030
kayg:wip/6by32-01-perf767-767-pause-20261030
kayg:wip/6by32-01-perf767-764-pause-20261030
kayg:wip/6by32-01-lists-b-pause-20261030
kayg:wip/6by32-01-ids-verify5-pause-20261030
kayg:wip/6by32-01-ids-verify4-pause-20261030
kayg:review/calendar-month
kayg:wip/10by32-02-undo-768-20261030
kayg:test/ids-verify3
kayg:wip/10by32-02-bm3-chromium-20261030
kayg:wip/10by32-01-review-753-20261030
kayg:wip/10by32-01-perf-idle-2-20261030
kayg:wip/10by32-01-bm2-webkit2-20261030
kayg:plan/759b-staging
kayg:train/2026-10-30-bm2
kayg:review/758b-tests
kayg:review/758-tests
kayg:review/764d-tests
kayg:review/767b-tests
kayg:review/preview-2026-10-29
kayg:review/767-tests
kayg:docs/tiering-731-plan
kayg:wip/c6b02-when2
kayg:wip/c6b02-tasks-create-fix5
kayg:wip/c6b02-settings-frost-mock
kayg:wip/c6b02-settings-frost-base
kayg:wip/c6b02-settings-frost
kayg:wip/c6b02-ring
kayg:wip/c6b02-review-733d
kayg:wip/c6b02-bm1-full-a
kayg:wip/compute02-when-field
kayg:wip/compute02-webkit-lists-repro
kayg:wip/compute02-train-bl2-dev
kayg:wip/compute02-dev-green-pw164
kayg:docs/deploy-plan
kayg:review/764c-tests
kayg:review/764b-tests
kayg:review/748g-tests
kayg:review/764-tests
kayg:review/759b-tests
kayg:fix/storage-owner-liveness-642
kayg:fix/storage-fix-7
kayg:wip/compute02-storage-fix-7
kayg:review/759-tests
kayg:feat/tiering-731
kayg:wip/tiering-731-first-attempt
kayg:wip/compute02-storage-fix-1
kayg:review/754g-tests
kayg:perf/idle-honest-2
kayg:review/754f-tests
kayg:review/754e-tests
kayg:review/755-tests
kayg:review/754d-tests
kayg:review/754c-tests
kayg:review/754b-tests
kayg:review/754-tests
kayg:fix/storage-private-tmp-642-dev
kayg:wip/compute02-verify-750
kayg:test/ids-verify5
kayg:test/ids-verify4
kayg:fix/storage-exact-file-quarantine-642
kayg:wip/compute02-verify-740
kayg:perf/one-flush-save
kayg:review/calendar-static
kayg:wip/compute02-verify-737
kayg:review/indexing-ring
kayg:review/when-field-2
kayg:wip/compute02-review-733
kayg:proto/block-identity-2-review
kayg:proto/block-identity-review
kayg:wip/c6b02-tc-design
kayg:review/when-field
kayg:issue/tasks-create-ids
kayg:issue/tasks-create
kayg:review/combined
kayg:wip/compute02-review-combined
kayg:review/task-polish
kayg:review/top-buttons
kayg:review/q1-q5
kayg:train/2026-10-26-bl2
kayg:archive/wt-398h
kayg:archive/train-bm-proptest-seed
kayg:archive/integ325-293
kayg:archive/integ325-ci
kayg:archive/client-auth-review-baseline
kayg:archive/wt-sim-4853
kayg:archive/wt-sim-4811
kayg:archive/wt-448m
kayg:archive/wt-398a-allocator
kayg:issue/calendar-adr
kayg:archive/old/train/2026-10-23bk2
kayg:archive/old/train/2026-10-23bk
kayg:issue/469-mockup-9a
kayg:issue/smart-tasks-mockup
kayg:issue/469-menus
kayg:issue/652-signout-flake
kayg:issue/652-history-fix7
kayg:issue/652-history-fix6
kayg:archive/old/issue/spring-check-perf
kayg:issue/spring-check-perf
kayg:issue/659-rename-fast
kayg:archive/old/train/2026-10-21ar
kayg:issue/search-hardening-3
kayg:issue/652-history-fix5
kayg:issue/660-port-sheets-glass
kayg:issue/652-history-fix4
kayg:issue/652-history-fix3
kayg:issue/652-history-fix2
kayg:issue/652-history-flake
kayg:train/2026-10-21an
kayg:issue/settings-admin-port
kayg:issue/search-port
kayg:train/2026-10-20am
kayg:issue/storage-split-fix2
kayg:issue/storage-split
kayg:archive/old/issue/folder-ids-fix
kayg:train/2026-10-20al
kayg:archive/old/issue/folder-ids
kayg:train/2026-10-20ak
kayg:issue/tables-mockup
kayg:train/2026-10-19aj
kayg:train/2026-10-19ai
kayg:issue/469-merge-fixes
kayg:issue/sidebar-band
kayg:archive/old/issue/608-settings-spec
kayg:train/2026-10-19ah
kayg:issue/swipe-offscreen
kayg:train/2026-10-19ag
kayg:archive/old/issue/590-nlp-adr
kayg:archive/old/issue/587-event-after-write-2
kayg:train/2026-10-19af
kayg:archive/old/issue/587-event-after-write
kayg:issue/469-pill-room-c
kayg:issue/469-pill-room
kayg:train/2026-10-19ae
kayg:issue/469-narrow-handle
kayg:issue/397-ios-swipe-2
kayg:archive/old/issue/adrs-photos-files-tasks
kayg:issue/469-desk-fixes
kayg:archive/old/issue/auto-update-adr
kayg:issue/469-filter-gap
kayg:issue/469-port-1b
kayg:issue/442-opacity-flake
kayg:archive/397-ios-swipe-2
kayg:issue/327-web-fast-3
kayg:archive/old/issue/469-port-1
kayg:issue/469-port-1
kayg:issue/327-web-fast-2
kayg:archive/old/issue/327-web-fast
kayg:archive/old/issue/425-stt-bench-vq1
kayg:archive/old/issue/445-no-bleed
kayg:archive/old/issue/400-swipe-mockup-2
kayg:issue/426-client-review
kayg:issue/426-client-2
kayg:issue/426-client
kayg:archive/old/issue/476-safe-area-lab
kayg:archive/445-top-light-2
kayg:archive/old/issue/445-top-light
kayg:issue/471-blocks-mockup
kayg:archive/old/issue/448-links-app-2
kayg:archive/old/issue/448-links-app
kayg:archive/old/issue/398-cold-links
kayg:issue/434-442-small-2
kayg:issue/434-442-small
kayg:archive/old/issue/398-unload
kayg:archive/old/issue/445-bottom-band
kayg:issue/445-bottom-band
kayg:issue/351-rem-3
kayg:archive/312-views-mockup
kayg:issue/351-rem-2
kayg:issue/351-rem
kayg:archive/old/issue/327-first-note
kayg:issue/395-width-phone
kayg:archive/old/issue/327-typing
kayg:issue/396-ios-edges
kayg:issue/397-ios-swipe
kayg:archive/398-allocator
kayg:archive/old/issue/327-notes-index
kayg:issue/345-347-contrast-empty-2
kayg:archive/old/issue/44-arenas
kayg:issue/44-arenas
kayg:archive/integ325
kayg:archive/old/issue/327-perf-harness
kayg:archive/old/issue/44-thp
kayg:issue/44-thp
kayg:archive/old/issue/285-sticky-pills
kayg:archive/old/issue/44-trim-3
kayg:issue/44-trim-3
kayg:archive/old/issue/165-cards
kayg:archive/old/issue/165-picker-2
kayg:archive/old/issue/212-multi-select
No reviewers
Labels
Milestone
Clear milestone
No items
No milestone
Projects
Clear projects
No items
No project
Assignees
Clear assignees
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!898
Loading…
Reference in a new issue
No description provided.
Delete branch "perf/executor-hot-path"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
The exact-byte note-write executor (#809, #814) was far over the 40 ms write budget on perf-01 with the 10k fixture (#894 comment 76971). This PR makes the executor's CPU and I/O per write scale with the notes it touches, not with every write it has ever made. It also fixes the two spurious
block_changedrefusals.Status: a 5–20× faster write path, but the 40 ms and move-many 200 ms gates are NOT met. What's left is about 9 durable flushes per write; getting under them needs a journal format change, proposed as the next PR in the results comment. Some other-gate medians read +6–9% under heavy VM contention and need a quiet rerun before merge.
ExpectedOwners: a digest of the write's own owner rows instead of the global index epoch;version: 0for unopened notes;Coordinates with #894: rebase guide in #894 comment 77087; a worked rebase is on
scratch/executor-on-894.🤖 Generated with Claude Code
Merge order: #905 (tolerant journal reader) must be in prod BEFORE this PR. Rolling back past this PR is only safe to a build that has #905.
Plan: executor hot path
Where the time goes (from reading the code, spans to confirm)
Profiling binaries with timing spans (
scratch/executor-spans= #894 headcac156ddplusCALTERNAL_EXEC_SPANS=1spans, never merged) are queued on dev-01 behind train BM14c. The span table on perf-01 will follow in the next comment. What the code already shows, per Home write:journal::load/records()/read_recordsPayload::validate(3 Yjs forks + an update apply per image). Home records are never compacted, so R grows by one per write (the bench: ~1000 records when set-status ran)recover()on every writecleanup_stagingfor every retained record, plus a journal dir scan and fsync, even when nothing is in flightjournal::usagefinish_innercheck()per noteR = retained journal records, k = notes the write touches, N = notes in the Home. Nothing above is O(N); the dominant term is O(R) with a large constant (Yjs work), which is why set-status after a few hundred writes takes 1.7 s.
Big-O per write
statper record file (≈1 µs each; the one O(R) term left, and it keeps the "damaged record refuses every write" guarantee)The fix
write_executor/index.rs): a per-journal cache of small summaries (key, state, author, undo_of, published, image ids, evidence names), never images. Each write lists the journal and stats every record file. A file is read again only if its inode/size/mtime/ctime changed, or if it was written within the last 2 s (a same-tick edit could keep the stamp equal). Even then it is fully parsed and checked only if its BLAKE3 differs; the check is a pure function of the bytes. Disk use = Σ record files + other journal files + evidence (re-counted while a record is in flight or recent). The hot path reads full records by key only: replay, commit, publish, undo.recover()runs when any record is non-terminal, a Home record is unpublished or needs release, or the previous write failed (executor_sweptis cleared on any error). Startup always sweeps.Requirement::ExpectedOwners { digest }holds a digest of the exacttask_linesandlinksowner rows the plan was built from. Home re-reads those rows with an adapter probe undermeta_update, on the index connection, at the same point the old epoch was checked. The oldExpectedInventory { epoch }stays readable for retained records.Home::row_versions(ids)gives each note's newest change. A note nobody has opened gets a negative file token from its index stamp instead.Home::version_matchesaccepts a token only if the history is just the firstseedand the file on disk still has that stamp. A token never equals a real version (those are ≥ 0).Failure table
journal_corruption_gates_every_write_and_recovery(unchanged)executor_sweptclearedtests/crash.rs, Tasks B1 crash child)recover_note_writessweeps fullyNOTE_EXECUTOR_STOP/NOTE_HOME_STOPcrash testsmeta_updateblock_changed, client re-plansowner_digest_ignores_unrelated_index_writes_and_refuses_new_ownersExpectedOwnersbut no probeno-probevariantblock_changedfile_tokens_write_and_go_stale_on_any_changeblock_changedExpectedOwners, then an older binary is deployed backfix/journal-tolerant-reader) ships in a train BEFORE this PR; roll back past this PR only to a build that contains it (also indocs/deploy.mdon #905); otherwise roll forwardwrite_executor::tests::tolerant: a record written by this PR's head is read, recovered and compactedGates (perf-01 under
perf-lock, 10ktasks-benchfixture,tasks-write-bench --n 200)tasks.set-status,tasks.move(both kinds),tasks.undo,tasks.delete;move-many 50≤ 200 ms41f2efa3) vs this PR. move/undo/delete/move-many: #894 (cac156dd) vs #894 + this PRstorage-benchappends,open.note, list, search, cold index (tasks-bench), each before/after on the same VMcargo test -p calternal-notes write_executor,-p calternal-tasks,-p calternal-engine, the crash children, clippy-D warningsUpdate 11-03 (review 77166 fixes): P2-1 above; P3-1 creations synced before the first removal; P3-2 sweep flag fail-safe; P3-3 journal cache dropped on a failed folder sync; P3-4/P3-5 rows added to
docs/implementation/note-write-executor.md.Results: perf-01 under
perf-lock, 10ktasks-benchfixtureBuilds (release, built on dev-01, relayed through the control host, sha256 checked on both ends):
scratch/executor-spans= #894 headcac156ddplus no-op spans (env unset) ·tasks-write-bench11928a10…scratch/executor-on-894=cac156dd+ this PR (head1b81f852), rebased the way the #894 comment 77087 describes ·tasks-write-benchbc6df09a…calternal-perfat dev41f2efa3(5185e941…, server6bace468…) vs this PR1b81f852(57fb1d2f…, serverd0591511…)Command:
tasks-write-bench --base data/base-10000 --state <tmp> --n 200. Before and after ran interleaved (B A B A B A). The VM was heavily loaded the whole evening: other agents' browser and perf runs on perf-01 and perf-02, which share the backup01 disk. An fsync probe (file + folder) measured p50 63 ms, p95 338 ms at 18:28; the earlier quiet window measured ~6.5 ms. Absolute numbers are noisy; the before/after ratio is consistent.Write actions (ms; three runs each, p50 / p95)
tasks.set-statustasks.moveWhentasks.moveSomeday → daytasks.undotasks.deletemove-many50move-many50 undoThe before runs 2–3 used
--n 100(shorter journal, so faster than run 1: the O(R) cost);deletehas no rows at n=100. In a quiet window (after1, before the batching commit) the after numbers were: set-status p95 130, move p95 93, undo p95 87.Verdict: 5–20× faster, and the cost no longer grows with the journal. Both spurious refusals are gone. The 40 ms and 200 ms gates are not met.
Where the remaining time goes (spans,
scratch/executor-spans2, mean ms per write)sync_allof the record file +fsyncof the journal folder1b81f852)record_once+ history install1b81f852)So a one-note write is now about 9 durable flushes. At ~6.5 ms per flush on a quiet perf-01 that is ~60 ms, above budget before any other work. Under tonight's contention it's worse. Getting to 40 ms needs fewer flushes, not less CPU. That means changing the journal's on-disk format. Per the foundational-work rule I have not done it here; I propose it as the next PR, plan first:
fdatasync, with no per-record file or folder sync (−4 flushes);publishedmark rides on the next frame or a lazy marker. Recovery already republishes idempotently from receipts, so losing it costs one republish (−1 flush);That brings it to ~4 flushes per write, or ~26 ms at 6.5 ms per flush. move-many scales the same way.
Other gates: dev vs this PR, interleaved 3 × 3 (
calternal-perf --sizes 10k --runs 3 --cold-runs 2 --idle-secs 0)Every PR range overlaps dev's own run-to-run range. None of these paths calls the changed code: the executor runs only when
write-executor/exists, which it doesn't in this suite, androw_versions/stamps_ofhave no caller on dev. But under this contention I cannot show them flat.home.page,home.page_tag,search.queryandtrash.deleteread +6–9% at the median. They need a rerun in a quiet window before this merges.Tests (dev-01, Nix shell,
~/bin/slot, at1b81f852)cargo clippy --locked -j4 -p calternal-notes -p calternal-tasks -p calternal-engine --all-targets -- -D warnings✓ ·cargo test --locked -j4 -p calternal-notes -p calternal-tasks -p calternal-engine✓: notes 549 (includeswrite_executor/tests/crash.rsand theio_failure_at_every_durability_boundarysweep), tasks 69 (includes the B1 Home crash children), engine 20. New tests:same_length_record_edit_with_restored_mtime_still_gates_writes,owner_digest_ignores_unrelated_index_writes_and_refuses_new_owners, theowners/no-proberefusal variants,file_tokens_write_and_go_stale_on_any_change,tokens_are_negative_stable_and_json_exact.Not run: the full workspace and the Playwright suite (that's for trains).
WIP: perf: executor write path costs O(touched notes), not O(journal)to WIP: perf: executor writes cost what they touch (5-20x faster; 40 ms gate not yet met)Independent review: data safety + quiet perf rerun (head
1b81f852)I read the plan (77003), the results (77110),
docs/implementation/note-write-executor*.mdand the full diff against dev41f2efa3. I changed no code.Counts: P1 0 · P2 1 · P3 6. Verdict at the bottom.
P2
P2-1. Rolling back past this PR takes whole Homes offline (no data loss). An older binary can't deserialize
Requirement::ExpectedOwners(an externally tagged enum, so the variant is unknown). Home records are never compacted, so the plan's suggested mitigation, "drain the journal first", isn't possible: one committed set-status record is enough to block the downgrade forever.Scenario: deploy #898 → a user ticks one task → prod is rolled back to the previous build →
journal::readfails inrecover_note_writes→Notes::homeshuts the Home down and returns the error → that user's whole Home (all notes, not just Tasks) fails to open until you roll forward again. Every user who wrote through the executor after the deploy is affected.Fix (process, not code): in the deploy issue (#684) and in the plan's failure-table row, write "roll forward only past #898". Alternatively, ship a tolerant reader in a separate earlier deploy first. Either way, it must be in place before prod.
P3
P3-1. Batched placement drops "creations durable before removals" across folders on power loss. Latent: no current plan has both. The old loop synced each target's folder before placing the next note, and the sort put creations and replacements before removals. Now every placement happens first, then one
sync_parents.Scenario (future plan that moves a note from folder X to Y): power cut after both placements and before
sync_parents→ ext4 keeps X's rename (target → evidence) but not Y's link → after reboot the note is at neither public path. Startup recovery fixes it before the Home is served: the Prepared record holds the post bytes, andcheckclassifies each note on its own, so any subset of placements is handled. But an outside reader ofNotes/that starts before calternal (a sync tool or backup) sees a deletion. The crash suite uses process exits, where the page cache survives, so no test covers this ordering.Today's plans (set-status/move = replacements only, delete = one removal, duplicate = one creation) never mix the two. Fix when a mixed plan appears: sync the creation folders before the first removal. That's one extra folder sync, only for such plans.
P3-2.
executor_sweptisn't cleared on every failure path. It's cleared inrun_note_write's error arms. It isn't cleared inexecutor_undoerrors, or whenspawn_blockingreturns aJoinError.Scenario: an
executor_undofails afterfinishpersisted a terminal state, andcleanup_stagingerrors →.calternal-tmp-executor-*.stagingstays in the user's note folder and counts against the 1 GB budget until restart.Impact is limited: any non-terminal record sets
openand forces the sweep anyway, and every restart sweeps. Fix: a fail-safe flag, cleared at the start of a write and set only after success.P3-3.
index::viewcaches before the caller's folder barrier succeeds.view()inserts the refreshed cache. ThenNoteWriteExecutor::viewrunssync_dirwhenunconfirmedis set. If thatsync_dirfails once (EIO), the next view sees equal stamps and hashes, sounconfirmed = falseand the barrier is skipped for that entry.Mitigated: a non-terminal record forces
recover(), whose fullload()always syncs. Only terminal records are affected: an idempotent retry can get a Committed receipt before the entry is confirmed durable, and after power loss that record is Prepared again, so recovery rolls the same plan forward. Fix: keepunconfirmedin the cache until a sync succeeds, or sync insideindex::viewbeforeall.insert.P3-4. Damaged old records are now found at startup or on the next full read, not on every write. The plan's table accepts this; I agree it's acceptable. Two follow-ups:
docs/implementation/note-write-executor.md, row "corrupt/unknown journal → recovery withheld", should say "at startup and on any full read of that record".execute()idempotent path now answers from the cached summary (policy::receipt). A retried key whose record later rots on disk returns Committed instead of refusing. That's acceptable, because the operation was committed when it was cached.P3-5. Stat trust assumes a local POSIX filesystem. On ext4, the stamp's ctime (ns), the 2 s window and rename-only replacement (always a new inode) make a stale cache entry practically impossible:
On network or FUSE filesystems with attribute caching (NFS
actimeo) this doesn't hold. Add one line to the module doc: Homes on local filesystems only.P3-6. Fewer checks between placements, and a narrower owner guard.
ExpectedOwnerscovers each target'stask_linesandlinksrows, but not the task's owntasksrow (path,broken) thatupdate::targetreads. A task whose properties broke between plan and admission is still stopped by the exact-byte pre check andMetadataValidation. Only the error message changes; no wrong write is possible.Answers to the specific questions
meta_updateon the Home's ownmetaconnection, which is the one the task index writes through. Every owner note is also a participant with exact pre bytes. Owner edits that haven't been indexed yet are invisible to both the old epoch and the new digest, the same as before.seedrow equal to the room's version. A collision has a chance of about 2⁻⁵². Remaining race (pre-existing, also true for real versions):row_versionsreads in a separate snapshot from the row, so a client can hold a token newer than the row it shows. Writes are still computed from current bytes, so nothing is lost..stagingfiles, removed by the sweep.sync_parents: a process crash is fine; on power loss any subset survives, and per-note classification handles it (P3-1 for mixed plans).sync_parents: placements are durable, and skipping the final barrier for notes placed in this call is sound. Image data was synced before its link, and evidence lives in the target's folder, so one folder sync covers both.flushedis recorded only aftersync_dirreturns Ok).open()syncs the folder once per process and re-creates it if!is_dir(): fine.Quiet perf rerun (perf-01,
perf-lockon perf-01 then perf-02, held together)Locks held from 18:28 to 20:03 UTC. dsync probe (200 × 4 KiB): 4.2 / 4.2 / 5.7 / 4.3 / 6.0 ms per write at start, mid, after gates 1–4, after the n=200 runs, and at the end. That's quiet; the earlier run measured 63 ms. Builds were done on dev-01 and relayed through the control host, with sha256 checked on both ends:
calternaldevb4b76071…, PR6110b01a…calternal-perfdev7c879bed…, PR8a700757…tasks-write-bench#894cac156dda166c2b3…, #894 + this PR (scratch/executor-on-89408057dbd)57974c5a…Write actions. Command:
tasks-write-bench --base data/base-10000 --state st --n 100, #894 vs #894 + this PR, alternating A B A B A B, fresh state each run. Dev has no write bench, so this is the base. Cells are the three runs. Delete only gets samples when n > 100, so it comes from--n 200run A B B A.tasks.set-statustasks.moveWhentasks.moveSomeday → daytasks.undomove-many50move-many50 undotasks.delete(n=200)tasks.reorder(control, no executor)Gates are still not met: p95 is 62–83 ms against the 40 ms budget, and move-many 50 takes 218–244 ms against 200 ms. That matches 77110. Of the two PR n=200 runs, the second was about 30% slower on every action, including
duplicate, which doesn't use the executor; that's environment noise, and it's still 16× faster than base. With 200 records instead of 100, PR cost is roughly the same (set-status 68 vs 61 ms) while base grows from 1000 to 1700 ms: the O(R) term is gone.Other gates. Command:
calternal-perf --sizes 10k --runs 3 --cold-runs 2 --idle-secs 0, dev vs PR, 8 pairs alternating (D P × 4, then P D × 4). dev run 8 is excluded: I released the perf-02 lock about a minute before it finished, and it reads slower on most metrics. So the table has 7 dev runs against 8 PR runs. "dev control" = median of the even dev runs vs the odd dev runs.The four metrics flagged in 77110 are flat or better in a quiet window; the earlier +6–9% was contention.
The
cold.*medians lean against the PR, so I checked them separately:recover_note_writesreturns early withoutwrite-executor/,stamps_ofandrow_versionshave no caller on this branch, and the only other change there is oneAtomicBool.I read them as noise. If you want certainty, run a cold-only A/B with more cold runs (
--cold-runs 6).All 46 metrics (dev runs 1–7, PR runs 1–8)
Verdict
Safe to train. I found no path that loses or corrupts data, or accepts a write it should refuse, under process crashes, restarts, outside edits or concurrent processes. Power-loss ordering is weaker only for mixed create + remove plans, which don't exist yet (P3-1).
Before prod: the "roll forward only past #898" deploy note (P2-1) is required. P3-2 and P3-3 are small follow-ups worth folding in. Perf: the targets are 3–22× faster, and no non-target metric regresses in the quiet rerun. The 40 ms and 200 ms gates are still not met, as the PR already says.
🤖 Generated with Claude Code
Review fixes for 77166 (head
2432b5a4)Rollback safety is in #905 (
fix/journal-tolerant-reader, head8973b7e6): the reader acceptsExpectedOwners, no write behaviour changes. It must ship in a train BEFORE this PR. Plan (77003) failure-table row,docs/implementation/note-write-executor.mdand the PR body now say rollback past this PR is only safe to a build with #905.1b81f852recovery.rs); one extra sync only for mixed plans; order testAlso one
cargo fmtcommit: the earlier head was not fmt-clean.Gates (dev-01,
~/bin/slot, nix develop)cargo fmt --all -- --check: OKcargo clippy -j4 -p calternal-notes -p calternal-tasks -p calternal-engine --all-targets -- -D warnings: OKcargo test -j4 --no-fail-fast -p calternal-notes -p calternal-tasks -p calternal-engine: engine 20 passed; notes 551 passed, 0 failed, 6 ignored (includes the crash suites); tasks 69 passed.Perf (perf-01,
perf-lockheld on perf-02 and perf-01 together; 11-03 ~21:35 to 21:44 UTC)tasks-write-bench --base data/base-10000 --n 100, release builds on dev-01, sha256 checked: previous head (scratch/executor-on-89408057dbd,12d22600…) = old; same + these fixes (3b715ca2…) = new. Order old, new, new, old, old, new. Run 1 (old) is the first run after the fixture and ran in a noisy disk window (dsync probe 94 ms/write at start, 6-7 ms at the end): discarded. Cells p50 / p95 ms:Flat: new is equal or slightly better than old on every action.
Caveat, not hidden: this window was slower in absolute terms than comment 77166 (set-status p50 about 85 ms against about 61 ms, reorder control 16 ms against 11.5 ms). The control moved by the same factor (about 1.4x), so it is the shared disk, not this change. Numbers against 77166 are therefore not directly comparable; the same-session old-vs-new pair is the valid comparison. If you want an absolute rerun, wait for a quiet disk window (the perf VMs were busy with other jobs for about 1 h before this).
Cleaned up: perf work dirs, bench worktree targets.
🤖 Generated with Claude Code
kayg referenced this pull request2026-11-05 23:21:11 +00:00
Housekeeping #1009 status: audited head
2432b5a472f83401eb0164ccd2ea92b70a45e39dagainst devcd7f84432feb.git cherry origin/dev <head>reports 17 unmatched commits and 0 equivalent patches. Separate stable patch-id indexing and key-file checks do not establish full landing. Absent from dev:crates/notes/src/history/row_versions.rs,crates/notes/src/row_version.rs,crates/notes/src/write_executor/index.rs. Left unchanged as WIP, per kayg’s instruction; no rebase, close, tests, or merge. The #1009 audit records the unmatched commit list.View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.Merge
Merge the changes and update on Forgejo.Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.