notes: paged identity projection and bounded child lookups (#761 C) #792
Closed
kayg
wants to merge 16 commits from
feat/tiering-761-c-index into feat/tiering-761-b-reader
pull from: feat/tiering-761-c-index
merge into: kayg:feat/tiering-761-b-reader
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:perf/executor-hot-path
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-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!792
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/tiering-761-c-index"
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?
SQLite temp-storage follow-up plan (2026-11-01)
Cause under investigation. A path-annotated C trace recorded 236,342 writes to five SQLite
etilqs_*temporary files while the scan had staged 20,000 identities. The writes were 4-byte markers and 4 KiB pages; they came from the same identity-writer thread asids.dbWAL writes and identity-log fsyncs. The mainIndex::loadconnection already uses in-memory temp storage. The traced run then hit the existing 60-second writer acknowledgement timeout under instrumentation, so this is attribution evidence, not a completed 100k trace or timing result.Change. Apply
temp_store=MEMORYandtemp.cache_size=-128to the identity-writer and compaction-reader SQLite connections. Keepsynchronous=FULL, the identity-log fsync, directory sync, and durable acknowledgements unchanged. The per-connection temporary cache remains 128 KiB; the existing projection lease continues to bound aggregate SQLite caches to 64 MiB across 128 Homes. Runtime and storage remain O(N) in indexed notes, with O(128 KiB) temp-cache budget per connection.stracestill showsetilqs_*writesGates. On dev-01 through Nix and
~/bin/slot: full workspace tests, format, strict clippy, and release build. Build and SHA-256-check C/dev/perf binaries on dev-01; run three alternating cold pairs and one warm pair on perf-01 under~/bin/perf-lockwithRuntimeMaxSec >= 6h. Cold p95 must be at most 1.25x dev; open.note p95 must be at most 15 ms and idle.rss p95 at most 100 MB. No durability relaxation is allowed.scan batch cache experiment: rejected
The 400-row experiment (
df742b61) kept log fsync and FULL projection commits but did not reduce syscall writes: 1,876,854 pwrite64 calls versus 1,868,042 at 4,000 rows. Its untraced cold run took 47.447 s and wrote 1,306.8 MB versus the prior diagnostic’s 26.068 s / 983.0 MB (shared dev-01, so timing is diagnostic). Reverted the experiment; the retained implementation and durability protocol are identical to92a28435. The fresh retained-binary comparison measured 25.500 s / 962.302 MB. Final head558e78e93ae4fd5aa3d0400216573509f1ecbde5passes targeted tests, crash tests, fmt, strict clippy and release build; isolated qualification remains blocked by both shared perf locks. No job of ours remains running.Bounded discovery reader follow-up
The latest shared dev-01 pair still measures C 29.3 s / dev 16.2 s. Metadata description is 14.3 s; the reader performs repeated absolute-root checks for each file. Add a background-only reader group capped at 128 opens: validate the original root before and after the group, retain per-file plain-name/no-symlink/no-cross-mount opens, and return derived results only after final validation. Metadata still validates again before its transaction. Ordinary request reads keep their existing checks. Each worker describes one body at a time; no group retains 128 bodies. Root validation becomes O(groups), with O(128) derived results per worker and unchanged 64-worker admission.
Failure: root replacement during a group discards the entire result and fails discovery for retry; no note file is changed. Gate this with deterministic replacement and group-limit regressions, the existing retained-reader tests, notes and crash suites. Both perf VMs are currently occupied by the separate 759b matrix; isolated qualification is still required.
Resumed cold-index plan (2026-11-01)
The new dev-01 phase profile shows repeated exact SQL preparation and repeated absolute-root ancestor opens, in addition to the pending-overlay exclusion scan. Cache the two point-lookup statements and replace Linux root-validation component walks with one
openat2rooted at/, retaining no-symlink resolution and all before/after root mount/device/inode/owner checks. Root path parsing stays O(depth), descriptor resolution becomes O(1) syscalls; each validation holds at most two transient descriptors. No bodies are added to tier storage and no log sync or FULL projection commit is removed.Re-run fsio, notes, crash and fmt/clippy gates on dev-01; build there, verify SHA-256 through control and perf hosts, then collect three alternating cold pairs on perf-01 and one designated warm pair on perf-02 under the shared locks. Nothing merged.
Paged identity projection and bounded background I/O (#761 C)
Stacked on B #791. Identity, folded-path, child, and descendant lookups use SQLite B-trees.
ids.jsonlremains the committed rollback/recovery log; its identity and length validate the projection on reopen.Cold-index follow-up plan and dev-01 profile
Cause. On C head
9c8c2873,identify_manyawaited a durable write after each 50-note batch. At 100k notes that made about 2,000 sequential writer transactions, preventing the writer's eight-command queue from grouping scan commits. A server-only dev-01straceprofile counted 6,569fsync/fdatasynccalls: 2,000 onids.jsonl, 2,004 on its directory, 2,295 onids.db-wal, 144 onids.db, 98 onmeta.db, 16 onhistory.db, and 12 other syncs. The perf harness reported 2,253 MB physically written for that traced cold run; the instrumentedcold.indextime was 920 s, so its elapsed time is diagnostic only. The final files were 8.8 MBids.jsonl, 35.2 MBids.db, and 4.5 MBids.db-wal. The scan reads note bodies but its write paths are the identity log and SQLite index/metadata/history stores.Fix. Cold discovery now uses a scan-only limit of 400 identities per durable append and
synchronous=FULLprojection transaction. Ordinaryput_manyremains capped at 50. Staging clears up to the same group size of stale overlay rows, so the disk-backed pending table remains O(400). At 100k notes this reduces scan groups from about 2,000 to 250; discovery and log replay remain O(N). The identity log sync, directory sync, and FULL projection commit are retained.r2_scan_recovers_after_restart_between_identity_groupsids.jsonl; SQLite projection may lagscan_batch_of_400_is_durable_and_replayedids.jsonlif neededscan_batch_of_400_is_durable_and_replayedscan_batches_clear_the_previous_staged_groupThe scan still reports incomplete until discovery finishes. No note file is modified. The event batch remains 1,000 records. The dev-01 trace is diagnostic, not a performance qualification.
Gates. Run the notes crate suite on dev-01. On perf VMs, alternate C/dev samples under
~/bin/perf-lockusing the #issuecomment-72012 harness options; collect three cold and five warm 100k runs and reportcold.index,open.note, andidle.rss. Requirecold.indexp95 at most 60 s and at most 1.25x dev. Report page-cache drop availability. Keep C idle RSS at or below the prior C p95 of 107.668 MB; the existing 100 MB RSS budget remains tracked separately.Round-2 design and bounds
try_acquire_owned()before staging identities or changing create/rename/delete files and history. The transport cannot accumulate more than 64 admitted commands, including in-flight commands. Admission never waits. Saturation/closed writers returnNotSavedYet(HTTP 503, retry).ids.jsonl; it never rebuilds the request connection's projection.r2_scan_batches_keep_duplicate_embedded_ids_distinct_across_pages, and the new restart regression cover completion, identity preservation, and interrupted discovery.ids.jsonlplus matching projection metadata is authoritative. Startup takes the exclusive index ownership lock, then removes reserved UUID scratch names, including interrupted legacy atomic-write scratch. Unrelated files remain untouched. A live owner prevents cleanup of active snapshots.Exact lookups are O(log n); descendant/child pages are O(log n + 50). Replay and complete compaction remain O(entries), with bounded compaction steps outside request locks. Initial projection replay occurs in the existing blocking Home-open worker.
Review findings and failure recovery
r2_full_queue_never_waits_for_the_writer_or_stages_rejected_ids,r2_saturated_identity_queue_refuses_create_without_blocking_another_homer2_compaction_failure_does_not_fail_an_admitted_append,r2_compaction_steps_keep_concurrent_updates_and_deletions_on_reopen(50k rows)r2_descendant_queries_never_materialize_a_wide_subtree(50k),r2_watcher_removes_every_descendant_across_pages,r2_contact_candidates_page_beyond_fifty_and_bound_ambiguous_namesr2_startup_removes_only_uncommitted_compaction_scratch,r2_replacement_error_orphans_are_recovered_only_after_owner_exitsr2_acknowledgement_has_a_deadline_and_another_writer_keeps_saving, existing failed-log-write regressionr2_create_retry_cannot_acknowledge_an_uncommitted_identityfailed before the guard, passed afterr2_shutdown_does_not_wait_for_an_unsent_admission,a_create_that_outlives_shutdown_writes_nothingThe four original regressions (queue, compaction failure, 50k descendant cap, orphan startup cleanup) all failed on the pre-fix C head
cb8ef0b0. The map functional model waits for actual disk acknowledgement rather than applying the request deadline. The anchor refusal fixture waits for save-time title normalization before its whole-note equality snapshot; the previous full run captured only that title property racing the snapshot, with unchanged block text. Both original assertions remain. Restart fixtures now release the old index owner without settling unsaved rooms; the crash-recovery assertions remain in place.Validation
Head:
9c8c287357240912daf895c4166bf00f2212ce4a.PASS at
9c8c287357240912daf895c4166bf00f2212ce4a: fmt, strict clippy, and the full workspace suite; 1,205 passed, zero failed, 20 ignored, exit 0. Run sequentially through Nix/slot on dev-01 withRUST_TEST_THREADS=2. Evidence:~/tier-c-r2-final6.{log,exit}. D's exact-head gate also passed: 1,208 passed, zero failed, 20 ignored (~/tier-d-r2-final6.{log,exit}). The final-head release rebuild passed; runtime hashes match the prepared perf binaries.Final head
9c8c287357240912daf895c4166bf00f2212ce4abuilt successfully on dev-01. The final-head rebuild produced the same runtime binaries as9e15ece7; the only later change is a test fixture barrier. SHA-256 matched on dev-01, control, and perf-01:0a541a0ba472631fe53872fbdce85f6e93663188144434c1b2e24f2bf15c7ea5081e036a464cb95bc13f67732e87079437874155f3c5bf9933ebc7f66de5b297Final-head performance qualification is blocked. Both perf VMs are occupied by another campaign using
/var/lock/calternal-perf.lock, whichclaudecannot open. The attempted final-head run under the documented~/bin/perf-lockwas stopped after detecting the other campaign. No results from that attempt are treated as qualification. No other processes were stopped. Earlier48ae327emeasurements (3.218 ms open.note p95; 89.293 MB idle RSS) remain historical evidence, not isolated final-head qualification.The outstanding command, on an idle perf VM under its exclusive measurement lock:
The budget file contains only
open.note@100000.p95=15andidle.rss@100000.p95=100. Prepared binaries/fixture/run script: perf-01~/tier-c-perf-9e15/. Aborted measurement log:~/tier-c-r2-perf-final4.log. The attempted shared-lock acquisition returned permission denied (~/tier-c-r2-perf-final5.{log,exit}, exit 66). No measurement remains running.Remaining limits
A five-second timeout does not cancel an admitted disk write; the client retries through existing idempotent operations. A stopped writer requires disk repair/restart. Cache-lease exhaustion refuses a new Home; fair eviction is future work. These changes do not qualify FO1, online tier movement, history-heavy 100k-edit opens, or power-loss protocols.
Stack: A #790 → B #791 → C #792 → D #793. Nothing merged.
Independent review, round 2
Verdict: fix first
P1 — synchronous queue backpressure and compaction block request threads
crates/notes/src/index.rs:426,430,444;crates/notes/src/home.rs:296Index::appendcallssnapshot()inline at the threshold, traversing and serializing the projection and fsyncing it before returning. When the 64-messagesync_channelfills,send()also blocks.home.index()holds astd::sync::MutexGuardduring this synchronous call, andput()is used by async room/action paths. A slow disk or large Home can occupy a Tokio worker and hold the Home index lock for an O(entries) interval. The tests assert structure and SQLite VM work, not runtime contention. This conflicts with ADR 0014's no-job-blocks-another rule; the existingopen.noteand 100k Home RSS budgets have no measurement here.open.notep95 15 ms;idle.rss@100000p95 100 MB).P2 — descendant removal materializes every path
crates/notes/src/index.rs:244-255; used bycrates/notes/src/watch.rs:303-318under()collect every descendant into oneVec; reconciliation then buildsgone_idsand another filteredVec. A wide subtree can spike RSS even thoughchildren()is paginated.P2 — interrupted compaction leaves orphan snapshots
crates/notes/src/index.rs:402-418,547-580snapshot()durably writesids.compact-<uuid>before sendingCmd::Compact. A process kill while that send is blocked, before the writer removes the file, or a replacement error that returns before line 580 leaves a full extra log.Index::loaddoes not enumerate or clean these files, so repeated failures can exhaust state storage and prevent later writes.No P0 or P3 findings.
Final round-2 fix plan
Keep the existing SQLite projection and rollback log. Queue admission reserves one of 64 slots without blocking, before staging; acknowledgement waits asynchronously with a 5-second deadline and returns retryable NotSavedYet (503) on saturation, writer failure or timeout. No request builds a snapshot. Request mutations stay bounded; the writer uses its own SQLite connection so disk fsync never holds the Home index connection mutex.
Compaction runs on the writer in steps of at most 50 rows or 64 KiB, servicing queued writes between steps. A separate read transaction pins the committed projection; after writing those rows, stream the original log tail from the snapshot boundary, then fsync and rename the completed scratch log. Update projection identity/length only after publication; startup replays the authoritative log if metadata does not match. No O(entries) projection rebuild during compaction. Memory is O(50 rows + 64 KiB + 64 bounded commands); total work remains O(entries + concurrent log bytes).
Descendant enumeration uses 50-row keyset pages; watcher reconciliation consumes pages without accumulating subtree-sized path/id vectors. Startup takes an exclusive identity-log ownership lock before deleting only named compaction scratch files. This log format has no committed manifest referencing scratch paths: ids.jsonl is the sole committed log, and the projection stores its identity/length.
Gates: fmt, clippy -D warnings and full workspace for C and rebased D; targeted red/green regressions. Build release tools on a build VM and attempt the established 100k open.note / idle.rss gates on a locked perf VM. No merge or production operation.
Round-2 review fixes implemented and pushed. C
9c8c287357240912daf895c4166bf00f2212ce4a; D rebased unchanged at659b0f4e420f6272e801ea78eba8fe10d66c1ca5. Final-head fmt, strict clippy and full workspace gates PASS on both. Final performance qualification remains blocked. Nothing merged.-D warningscargo test --workspace~/tier-{c,d}-r2-final6.{log,exit}, exit 0; two test threads, sequential suitesclaude; overlapping attempt stoppedThe four original queue/compaction/descendant/orphan regressions failed on pre-fix C
cb8ef0b0. Both final-head suites retain and pass them. The map functional model now waits for the actual disk acknowledgement; the separate deadline test still checks production 503 behavior. The anchor refusal fixture now waits for save-time title normalization before comparing the whole note, preserving the original equality assertion. Its captured failure was only the automatic title property racing the snapshot; all six anchor tests and both final suites pass after the save barrier.The final release build passed. Prepared perf binaries match the final head by SHA-256; the fixture-only final commit does not change runtime code. Earlier performance numbers remain historical evidence, not isolated final-head qualification. See the PR body for exact commands, hashes, bounds and remaining limits. D's two patches are unchanged by range-diff. No measurement or PR watcher remains active.
Independent review, round 3 (final)
Reviewed C at
9c8c287357240912daf895c4166bf00f2212ce4a, round-2 comment 71074 and fix status 71522. Verdict: ready for the reviewed code scope; the final 100k performance gate remains open and withholds performance/cutover qualification.r2_full_queue_never_waits_for_the_writer_or_stages_rejected_idsstalls the writer, exceeds capacity and checks prompt return without rejected identities.r2_saturated_identity_queue_refuses_create_without_blocking_another_homechecks refusal before file creation and progress in another Home. The deadline/independent-writer test also passes.r2_compaction_failure_does_not_fail_an_admitted_appendisolates snapshot creation failure.r2_compaction_steps_keep_concurrent_updates_and_deletions_on_reopenuses 50k rows, requires at least 1,000 steps and checks updated/deleted/new identities after reopen. These tests do not establish the latency/RSS budgets.r2_descendant_queries_never_materialize_a_wide_subtreechecks all 50k descendants with a per-page cap.r2_watcher_removes_every_descendant_across_pagesverifies 125 removals and the unrelated survivor. The contact regression reaches beyond the first 50 children and checks bounded ambiguity.r2_startup_removes_only_uncommitted_compaction_scratchcovers current/legacy scratch and preserves unrelated files.r2_replacement_error_orphans_are_recovered_only_after_owner_exitsleaves a real replacement/removal-error orphan, refuses a second live owner, then cleans it on reopen while preserving committed state.r2_regressions pass in the independent downstream run.New findings, P0–P3: none. I checked writer publication/tail handling, the pending overlay/watermark, admission before create mutations, and the changed lifecycle call sites. The new same-ID create retry regression prevents success while identity is uncommitted; shutdown with an unsent reservation and duplicate embedded IDs across discovery pages also pass.
Independent verification on dev-01 at downstream D
659b0f4e, inside Nix through~/bin/slot:22 passed, zero failed, including 13 C regressions across notes and contacts-actions. Evidence:
~/tier-r3-independent.{log,exit}, exit 0. I inspected C's exact-head~/tier-c-r2-final6.logand exit 0. The full workspace suite was not independently rerun. No code pushed or production access.Open gate, not a finding: final-head isolated 100k qualification remains pending because the perf VMs were occupied. Required results are
open.note@100000p95 ≤15 ms andidle.rss@100000p95 ≤100 MB under the documented exclusive perf lock. Historical measurements and bounded structures do not satisfy this gate. Final-head macOS, real disk/power-cut and broader history/FO1 campaigns also remain unqualified as documented.100k qualification results
C
9c8c287357240912daf895c4166bf00f2212ce4awas measured against dev baselinea55521f94ef7337f0793037ae2cb2c27266d5720on perf-01 (x86_64, 4 CPUs). Release binaries were built on dev-01 through Nix and~/bin/slot; SHA-256 manifests passed on dev-01, the control host, and perf-01.The #792 gate protocol was run under the shared
~/bin/perf-lock: head-1, dev-1, head-2, dev-2, head-3, dev-3; seed 1; 60-second idle; one cold run in sample 1 and warm runs in samples 2–3. The per-sample harness invocation used--sizes 100k --runs 1 --cold-runs {1,0} --seed 1 --idle-secs 60 --budgets budgets.json.open.note@100000idle.rss@100000Aggregation follows
crates/perf/src/stats.rs:open.notepools each run’s p50/p95 summaries;idle.rssis one value per run, so its p95 is the maximum of the three values. Noise was 207.5% for dev and 22.5% for C onopen.note; the baseline exceeds the 25% noise band, so the relative latency delta is descriptive. RSS noise was 3.2% for dev and 6.8% for C.The profiled pair was diagnostic, with no page-cache drop. At the recorded idle RSS point,
smaps_rollupshowed C at 103.37 MiB RSS (84.49 MiB Pss_Anon, 17.00 MiB Pss_File) and dev at 123.19 MiB (104.01 MiB Pss_Anon, 17.36 MiB Pss_File). After-search RSS was 108.44 MiB for C and 139.38 MiB for dev. Most of the measured RSS is anonymous memory; the exact allocation owner is not isolated.The general #761 budget table also lists 100k
cold.indexp95 ≤ 60 s, though that metric is not in #792’s two-entry gate budget file. The gate run’s single cold observation was 68.256 s for C and 12.079 s for dev. A later diagnostic pair measured 48.063 s and 10.508 s, respectively. These samples do not establish a cold-index p95. The broader #761 three-cold/five-warm matrix was not run: cache drops were unavailable to the perf account, and the #792 gate command specifies one cold/three warm.strace/perfare not installed on perf-01.The diagnostic
/proccounters recorded 47.6 CPU-seconds and 1,747.8 MB written for C’s cold-index server, versus 12.5 CPU-seconds observed and 771.1 MB written for dev. The C scan awaits a durable identity write per 50-note batch, about 2,000 batches for 100k notes; each writer flush syncs the identity log and commits thesynchronous=FULLSQLite projection. The baseline queues the scan’s identity writes for a combined flush. This is a likely contributor to the cold-index cost; no syscall breakdown was available.No code was changed. The 50-row batch bound has durability and memory implications, so I left it for design review and re-qualification. The #792 RSS gate remains over budget on both binaries.
kayg referenced this pull request2026-10-31 23:51:17 +00:00
100k qualification: fail on relative cold.index
C
02491f790009e88a97b372303baf7052c34084e5versus deva55521f94ef7337f0793037ae2cb2c27266d5720. completed the resumed 3 cold + 5 designated warm pairs, alternating C then dev, with each pair on the same VM. the eight existing reports were recovered and validated; eight remaining reports were collected. all 16 reports have zero scenario failures; both resumed runners exited 0.aggregation follows
crates/perf/src/stats.rs: nearest-rank p95 for single-value metrics (the maximum with 3 or 5 observations); median of per-run p95s for open.note, with nearest-rank/lower median for an even count. primary warm results use samples 4–8, matching the resumed runner's plan. cold samples 1–3 also execute warm scenarios as part of the referenced harness invocation; including those supplemental scenarios gives eight-run p95s of C/dev 3.332/3.002 ms for open.note and 94.652/108.227 MB for idle.rss, with the same verdict. C also passes the historical 100 MB RSS budget.cold.index observations, C/dev in seconds: 40.154/11.452, 41.254/10.757, 40.185/11.305. cold noise was C 2.7%, dev 6.1%. combined warm open.note noise was C 39.5%, dev 58.1%, above the 25% band, so relative latency deltas are descriptive. every designated warm run's open.note p95 was below 15 ms (largest C 4.477 ms, dev 4.916 ms). RSS noise was C 9.5%, dev 6.2%.
execution and evidence
~/bin/perf-lock ~/bin/slot /bin/bash ~/pr792-resume/resume.sh ..., withRuntimeMaxSec=43200(12 hours). both runners are now inactive and have released their locks.4ef83a60a21bbe1485c9d20f3e4f11699599ef44b736c0eda739fd45591f9696; dev server90129ef4ec5838602eb781ea06f54c3077cbbe19f5a43e402366c3bd014a63b4; harness2cf7df4da8fe381ae9d813512bfc10489f92835b9076f53e1d11018a147073fa.exact per-sample command, preserving #issuecomment-72012 options:
cold=1for samples 1–3;cold=0for samples 4–8. budgets containopen.note@100000.p95=15andidle.rss@100000.p95=100; the relative cold and RSS gates were evaluated from the table. perf-01 originals:~/tier-c-qual-02491f79/; resumed evidence on each VM:~/pr792-resume/{C,dev,logs,session.log,session.exit,SHA256SUMS}. control-host reports, aggregate JSON and pair CSV:/home/kayg/Developer/perf-results/pr792-02491f79/.no code changed and nothing merged. qualification is withheld because cold.index exceeds the relative gate.
cold-index resume: fixes checked, isolated qualification blocked
Head
558e78e93ae4fd5aa3d0400216573509f1ecbde5; baselinea55521f94ef7337f0793037ae2cb2c27266d5720. The final source tree is identical to92a28435; the intervening 400-row experiment was measured and reverted. The ≤1.25× dev goal remains unqualified.profile and retained fixes
openat2. Background stat/head/metadata reads use groups capped at 128 files, validate the root before and after each group, and return results only after final validation. File name, symlink and mount checks remain; request reads retain individual validation. Root replacement and batch-bound regressions pass.synchronous=FULLprojection commits are retained. No crash-safety setting was relaxed.Nix
perfcounters were denied by the host'sperf_event_paranoid=3; no settings were changed. The original bounded strace sample recorded 172,523openatand 198,639closecalls during repeated root walks. Full current-server strace diagnostics recorded 1,868,042pwrite64calls for C versus 304,202 for dev. These counts include the cold server's stores; file-level attribution remains open. Traced wall times are not qualification results.A single-variable 400-row trial (
df742b61) did not reduce write calls (1,876,854). It increased physical writes and took 47.447 s untraced / 332.665 s traced, versus the retained binary's fresh 25.500 s untraced / 222.424 s traced diagnostic. It was reverted in the final head. Both full trace reports and the trial/comparison reports have zero scenario failures.Latest retained-binary dev-01 phase breakdown, seconds: stat 0.587, recovery 0, classify 0.657, read heads 0.804, identities 6.165, describe 12.531, events 0.701, sync metadata 2.609, save state 0.215. Identity projection and description remain the largest phases.
before / after
02491f79(#issuecomment-72561)e0af7578, dev-01 single cold eachDev-01's untraced dev observations ranged from 12.729 to 35.490 s under shared load. They cannot establish the relative gate. Current-head
open.noteandidle.rsswarm qualification is also outstanding; historical warm results are not promoted to this head.exact-head checks
Passed on dev-01 through Nix and
~/bin/slot, all exit 0:43 fsio tests, 453 notes tests (6 ignored), and all 3 real-binary crash tests passed. The full workspace suite and a new independent review were not rerun.
qualification blocker and prepared inputs
Both
/var/lock/calternal-perf.lockprobes still timed out (124). The759bmatrix campaign holds the locks and advances through additional configurations without releasing them. Our queued runners collected no samples and were stopped; no other campaign's processes were stopped. No job or monitor of ours remains running. The throwaway dev-01target/and temporary comparison binary were removed after preserving reports and binaries.Both perf VMs have the final inputs at
~/pr792-final-nov01/. Binaries were built on dev-01, streamed through the control host and SHA-256 checked at every hop; final rebuild hashes match the retained source's earlier binaries:The outstanding foreground command, on either free perf VM, runs all three alternating C/dev cold pairs and the designated warm pair on that same VM:
Each label/sample uses the #issuecomment-72561 invocation:
cold=1for samples 1–3 andcold=0for sample 4. Budgets remain open.note p95 ≤15 ms and idle.rss p95 ≤100 MB; compare RSS to dev as in the previous status. Cold p95 is the maximum of the three observations and must be ≤60 s and ≤1.25× dev. Cold means a fresh unindexed Home; OS page caches are not dropped.Evidence on the control host:
~/Developer/perf-results/pr792-e0af7578/, includingcurrent-profile/,restored-evidence.tar.gz, exact-head gate logs, reports, binary manifests and provenance. No PR was merged.100k cold-index attribution and qualification —
fab01191C head:
fab011917e9408f5301b839aab879f1e95a481aa; dev baseline:a55521f94ef7337f0793037ae2cb2c27266d5720.Write attribution and fix
The completed pre-fix 100k diagnostic trace attributed the excess to SQLite temporary sorter files (
/var/tmp/etilqs_*). In the file-attributed C trace, these 30 files took 1,492,369pwrite64calls; dev took 21,223, a difference of 1,471,146. C's calls were mostly 759,313 writes of 4,096 bytes and 732,978 writes of 4 bytes, plus 25 writes of 61,504 bytes and small tails. The full trace totals were 1,882,588 C / 308,790 dev, consistent with the earlier 1.87M / 304k signal.The current exact-head trace is down to 369,170 C / 306,546 dev
pwrite64calls. It shows zero C sorter-temp writes; dev has 21,223. The fix already on this head (7a983946) setsPRAGMA temp_store=MEMORYfor metadata cold-fill sorters. It does not change the metadata WAL sync setting;metadata_temp_tables_use_memory_without_changing_wal_syncassertstemp_store=2andsynchronous=1. Identity-log fsyncs, durable acknowledgements, and the identity projection'ssynchronous=FULLremain in place.For
describe, the scan reads bounded note heads to resolve IDs, then reads each full note once for list metadata.describe_listskips building the cleaned search body. This is a note-head read plus a full read, not two full-note parses; required title, tag and date extraction remains.Perf qualification
Ran on perf-01, same VM for all pairs, C then dev, seed 1, no page-cache drops. Command was
~/bin/perf-lock ~/bin/slot /bin/bash ~/pr792-fab01191/run-perf.shunderRuntimeMaxSec=21600. Per sample the harness used--sizes 100k --runs 1 --cold-runs 1for samples 1–3 and--cold-runs 0for the designated warm sample 4, plus--seed 1 --idle-secs 60 --budgets budgets.json. Sample 4 includes the harness's unscored cold setup for the warm run. All eight reports have no scenario failures; runner exit 0; lock released. C, dev and harness binaries were built on dev-01 and SHA-256 checked through the control host and perf-01.C/dev p95 is 1.248×, under the 1.25× limit (C ceiling 15.807 s) by 31 ms. Both pass the 60 s absolute limit. Three-pair range/median is C 10.8%, dev 16.1%.
Reports and the pair summary are on the control host at
/home/kayg/Developer/perf-results/pr792-fab01191/. Exact-head dev-01 gates for fmt, the identity-cache regression, fsio/notes and notes crash tests also exited 0. No source change was needed in this pass: the sorter fix was already present on the current PR head.Independent review: cold-index perf fixes
Reviewed head:
fab011917e9408f5301b839aab879f1e95a481aa.Findings
temp_store=MEMORYleaves the metadata sorter without an application-level memory budget. One 100k cold sample on dev-01 measured peak RSS of 175 MiB for C and 186 MiB for dev, so this sample shows no 100k RSS increase. If total C high-water scales linearly, 1M notes extrapolate to about 1.7 GiB; that is not a 1M measurement or sorter-only attribution. At sufficiently large sizes or concurrent cold fills, this can create RAM pressure where the old sorter spilled to temporary files. Define a large-Home memory budget or qualify a 1M fixture before relying on this setting at that scale.SCAN_IDENTITY_BATCH=4000;IDENTITY_LOOKUP_BATCH=400is the SQL parameter chunk size. The 400-identity scan-group description does not match the reviewed head. I exercised the actual 4,000-row setting.Checks
sync_all; projection commits retainsynchronous=FULL; acknowledgements follow the commit. The first log creation still syncs its parent directory, and compaction syncs the directory after replacement.openat2withBENEATH | NO_SYMLINKS | NO_MAGICLINKS | NO_XDEV. A failed post-group root validation returns an error before current callers consume their accumulated results. I found no symlink or root-replacement path escape.Verdict
Approve for the stated 100k target, with a P2 follow-up for memory limits at larger Homes.
Rebase and conflict resolutions
Rebased the stack onto dev
41f2efa387b74175971a838f602535089d0d5fb4:91c034ebb36ca42a19530c50996244e892e4acbeeeb9db4f5db70acf9e6e30a8ba9f4e617c567fd69b06c65b05b4f9b6506393f2a85501c52f6fb7c8f838ca3d22b9ec5542b732f540c064b98a5ecc1The first C rebase tried to replay A and B again because their old ancestry had been rewritten. I aborted it and replayed only C's commits after its former base
58ffcc7182a8c7c1ff64ecc366d0e52ebe2eddf8onto the new B head. D was transplanted from old C base9c8c287357240912daf895c4166bf00f2212ce4aonto the new C head.Resolution decisions:
layout::NOTESstate classification, plus both sides' fsio coverage and the split instructions.FileStamp, metadata refresh executor checks, and watcher path/identity lifecycle guards remain.FileStamp. Metadata updates retain the room metadata lock and executor identity check. Paged watcher deletion checks executor path and identity availability, confirms the ID still maps to the same path, then rechecks the identity after acquiring the lifecycle guard. Reserved index removal remains, with dev's delete-intent recovery behavior.temp_store=MEMORY; WAL sync settings, durable identity acknowledgements, andsynchronous=FULLprojection commits remain unchanged.capture_testsand its tiering model/fixture module.The functional and performance gates are running separately; results will follow with exact commands and measurements.
C on dev
41f2efa3: restack, resolutionsNew head
487e058908d3b2f24389627741c33907ee7beb25(was896babb4). Its commits aftereeb9db4fwere replayed onto B's new headefcaaaf7, plus one fmt commit.git diff 896babb4 487e0589shows only the order of two#[cfg(test)] modlines:cargo fmt --checkfailed on C alone, and the earlier fix (0200eb7a) had landed in D. The meta/executor hunks of896babb4now live in B, so C applies them as no-ops.Resolutions (dev's side kept; the paged traversal goes through it)
Search, peek and backlink sources: read only pinned accepted generations (
generation::Snapshot::pin,generation.source). The file-readingread_docis now#[cfg(test)]. Scan'sread_noteis a working-file read in dev too; it now takes the pinned reader and builds no unused search body (describe_list).Executor (#814): the only diff from dev in
write_executor/ishome.rs, a type change. The paged index returns ownedStrings, so.map(str::to_owned)goes andpath_of(..).as_deref()is compared. Every refusal (unknown Home binding,changed binding,executor_path_available) is dev's.write_executor/recovery.rsis untouched.#828 restart stamp validation:
meta/restart.rsuntouched. Background scans read through the pinned reader in groups of at most 128 and persist dev's fullFileStamp. Metadata updates keep the room metadata lock and the executor identity check.Watcher deletion (
watch.rs::reconcile_vanished, one 50-row descendant page at a time, never a subtree-sized list). In order, skip when:executor_path_available(rel)fails;id_at(rel)no longer maps to this id;executor_available(id)fails.Then take
lifecycle(id)and recheckexecutor_available(id), file absence andpath_of(id) == relbefore trashing or removing. Renames/restores that finish while the watcher waits on a room are never trashed. Reserved index removal keeps dev's delete-intent recovery.Durability: unchanged. Identity log
sync_all, acknowledgement after commit, projectionsynchronous=FULL. Only the metadata sorter usestemp_store=MEMORY.Gates: fmt and clippy
-D warningspass at487e0589.cargo test --workspace1,537 passed, 0 failed, 22 ignored, run at896babb4(the tree differs only by the mod order above). Cold-index gate 1.196× dev (limit 1.25×, was 1.248×); cold memory peak 227 vs 291 MB. 200-append −1.09% vs dev. Table in the gate comment below; dry run in #790.Open item carried from the last review (P2):
temp_store=MEMORYhas no app-level budget for 1M-note Homes. Not addressed here; no new 1M measurement.Gate table — stack on dev
41f2efa3(takeover after the Codex usage limit)Heads: #790 A
91c034eb→ #791 Befcaaaf7→ #792 C487e0589→ #793 Dd422d0ca. Base: dev41f2efa387b74175971a838f602535089d0d5fb4(what prod runs). Nothing merged.~/bin/slot)91c034ebefcaaaf7487e0589d422d0cacargo fmt --all --checkcargo clippy -j4 --workspace --all-targets --locked -- -D warningscargo test -j4 --workspace --locked -- --test-threads=2¹ Run by the earlier agent at
89989e62;git diff 89989e62 d422d0cais empty (same tree).² Run at
896babb4;487e0589differs only by the order of two#[cfg(test)] modlines (cargo fmt).Perf VMs (binaries built on dev-01 in release at the D tree, relayed through the control host, SHA-256 checked on both perf VMs: C
2006ab8e…, dev724535bd…, harness980045d9…):41f2efa3notes.append, 30 alternating pairs, perf-02, median totalProtocol unchanged from the
fab01191run:~/bin/perf-lock ~/bin/slot run-perf.sh, per samplecalternal-perf --sizes 100k --runs 1 --cold-runs {1,1,1,0} --seed 1 --idle-secs 60 --budgets {"open.note@100000":{"p95":15},"idle.rss@100000":{"p95":100}}, C then dev, same VM, no page-cache drop; all 8 reports have no scenario failures. No limit was changed.Notes:
fab01191run (deva55521f912.6 s → dev41f2efa320.9 s on the same harness and VM), so only the ratio is comparable. The absolute memory numbers are from a different source (harnesscold.mem_peakon perf-01 vs one dev-01 sample earlier); the ratio to dev improved.~/Developer/perf-results/pr792-41f2efa3/. Perf VM binaries and work dirs deleted.Perf pass on the stack: causes, fixes, and where it still regresses (not mergeable yet)
Branch
perf/tiering-761-fix(head3a65e27c), cut from #793's headd422d0ca; I did not touch the PR branches. Fixes below are for C's files (index.rs,scan.rs,meta.rs,write_executor/home.rs); they fold into #792 when the orchestrator decides.Causes found (callgrind on a 10k cold start, same fixture, dev vs head)
Head ran 12.4 G instructions against dev's 9.8 G. The surplus was all in scan classify/identify, none in the metadata store (
MetaDb::applyis identical, 2.19 G on both):Index::path_of/id_atran the TEMP view (stored rows UNION ALL staged rows, correlated subquery, sort), one query per file, 2×100k times per scan.Index::all()re-prepared its SQL for every page of 50 rows (page_after0.89 G for 10k notes).Commits (all pushed, one change each)
95716b4eview-freepath_of/id_at, 1000-rowall()pages ·14e74b8boverlap identity groups with the next head reads ·da957b87skip the executor path lookup when nothing is blocked ·51ea0761look scan identities up 400 at a time ·f2375638index lock per group, not per pass ·2473b998batch claimed-id lookups,known_idswithout the view ·4dae439bsorter budget (below) ·3a65e27crustfmt.Callgrind, 10k cold:
Index::id_at0.93 G → 0.49 G;page_after0.89 G → 0.11 G;scan::run2.83 G → 1.64 G (dev 0.33 G).Measured (perf-01,
calternal-perf --sizes 100k --runs 3 --cold-runs 1 --idle-secs 0, scenarios list,query,journal,open, alternating, same VM, no page-cache drop)41f2efa3first page / cold index / open.note p50 / p95d422d0ca(before)h44.83 s / 23.6 s / 9.0 / 22.6h44.81 / 23.4 / 8.9 / 28.8h54.81 / 22.3 / 2.89 / 12.5h54.65 / 22.3 / 2.96 / 8.2h65.59 / 30.5 / 6.0 / 43.5h65.47 / 24.0 / 2.98 / 21.4h6= tree at2473b998(identical code to3a65e27cbar formatting).Reading it honestly:
h6pair (21.4 vs 9.9). The first-open stall fell from 640–940 ms to ~200 ms (maxcolumn) but is not gone (dev ~21 ms). Not yet flat.index::apply+ writer, ~0.7 G per 10k notes, which dev's in-memory map plus append-only log does not pay) and a staging transaction per group.h6was cut off: the perf locks on both VMs were held for hours by other jobs (onesleep 5400holder), and my one cleanh6sample (cold 19.4 s, first page 4.98 s, open.note 1.17 / 5.4 ms) had no valid dev partner (its pair ran while a 1M-fixture copy was filling the disk, which I stopped and cleaned).Options for the remaining gap (need a decision, not a patch)
begin_fresh): drops the writer cost, butid_atthen needs a covering lookup during the fill.Sorter memory budget (item 4)
4dae439b: the metadata store's cold-fill sorters stay in memory up to 250,000 notes (same speed as now; the 100k path is unchanged) and spill to temp files (temp_store=FILE) beyond that, so a 1M-note fill's sorter memory stays near the 8 MiB page cache. Testsorter_memory_is_bounded_and_wal_sync_is_unchanged. Peak RSS at 1M notes is not shown: the perf harness cannot cold-start a 1M Home on dev either (notes.listreturnsnote discovery has not produced a validated page yetafter its 60 s first-page wait, on dev41f2efa3too), and my 1M RSS script never got the perf lock. 100k reference: cold peak 243 MB (head) vs 284–291 MB (dev), idle unchanged.Gates (dev-01,
~/bin/slot)cargo fmt --all --check3a65e27c)cargo clippy -j4 --workspace --all-targets --locked -- -D warnings2473b998)cargo test -j4 --workspace --locked --no-fail-fast -- --test-threads=22473b998;3a65e27cis rustfmt only)Option A (secondary indexes after the first fill): implemented, target not met — first page 2.5× dev, cold index 1.16× dev
Branch
perf/tiering-761-fix. New commits (production change isfe04455e; the rest are test/format fixes):fe04455ebuild the projection's secondary indexes after the first fill ·4a5e2dc6rustfmt ·362e6d1b,c0b06fb1clippy fixes in the new tests ·43d81bc1rustfmt. Head43d81bc1. Nothing merged.What changed
entries_foldonly.entries_foldis what the scan'sids_at_folds/id_atread while filling (scan.rsclassify and identify), so it stays.entries_child(children by name) andentries_descendants(watcher,under_after) are left out.scan::runend →Index::fill_done), the writer thread builds each missing index with oneCREATE INDEX IF NOT EXISTS, one per idle slot (only when no append is waiting). A restart finds what is missing and builds it again (index/deferred.rs).PRAGMA integrity_check= ok in the test).Tests (
index/deferred_tests.rs, 7 new)fresh fill keeps only the fill indexes · scan signal builds both, children/descendants answers identical before and after, later
put/removekeep them current,EXPLAIN QUERY PLANusesentries_child· populated projection without the signal builds on restart · empty projection does not build before its first fill · imported log builds all at once · kill mid-build: a child process stops betweenCREATE INDEXandCOMMIT, the parent kills it, checks the index is absent andintegrity_checkok, restarts and checks both indexes plus 3,000 children/descendants/path_ofanswers.Perf-01, 100k Home, 6 reps, rotated order (head / dev / dev copy "devB" as control), no page-cache drop, both perf locks held
Binaries built on dev-01 (release): head tree
4a5e2dc6(later commits change only#[cfg(test)]code and formatting) sha256f6ac38e1…, dev41f2efa301522f72…(devB is a byte copy), harness352d448a….PERF_ONLY=list,query,journal,open,--runs 3 --cold-runs 1 --seed 1 --idle-secs 0. Medians over the 6 reps.notes.append, 30 pairs (CALTERNAL_NOTES_SAVE_MS=15)Per-rep cold.index / first page: head 17.2 s/3.9 s, 17.5/4.3, 18.7/5.5, 16.8/3.9, 17.1/3.9, 17.3/4.2; dev 14.9/1.63, 14.8/1.62, 18.8/1.69, 15.1/1.66, 14.8/1.59, 14.7/1.59; control within ±3% of dev on every rep except one (cold.index 18.8 s).
Note on search: the scenario filter
PERF_ONLY=list,query,journal,opendid not emitsearch.first_query/search.queryrows in these reports; I did not run a separate search pass.Reading
id TEXT PRIMARY KEYrandom-order inserts (two B-trees per row), thetotalstrigger running anUPDATEper inserted row, and onesynchronous=FULLcommit per 4,000-row group. I have not measured any of them (callgrind on a 10k cold start would split it in one pass). Option 2 from the earlier comment (acknowledge after the log fsync) is still the other route.Gates (dev-01,
~/bin/slot, tree43d81bc1)cargo fmt --all --checkcargo clippy -j4 --workspace --all-targets --locked -- -D warningscargo test -j4 --workspace --locked --no-fail-fast -- --test-threads=2tiering-scalerelease: ids, inventory, aggregate, history, blocks, moves)bm14esince 09:30 CEST; my job is queued (tpf-fix). Result follows in a new comment.Perf VM locks on perf-01 and perf-02 released (
tpf4-holdstopped on both). Reports and scripts on the control host:/tmp/tpf/(JSON per run in/tmp/tpf/reports).Correction to the table above:
PERF_ONLY=list,query,journal,open) produced nosearch.*rows (checked in the JSON). The brief asked for search; that is still open. It needs another lock window with the scenario name the harness uses for search.#793 fixtures at
43d81bc1: not run. Trainbm14eheld dev-01's build slots for 2.5 h+ (~/.train.lock), so mytiering-scalerelease build never started; I stopped my queued job (tpf-fix) rather than leave it running. The previous fixture results (tree2473b998, comment above) are the latest. The change here touchesindex.rs/scan.rsonly through the deferred index build; the full workspace suite passed (1,547).Update on the #793 fixtures at
43d81bc1: still not run. My first fixture job (tpf-fix, started 09:21 CEST) ended without output and I do not know why; the second (tpf-fix2, 11:50) waited about 35 min with no~/bin/slotslot while trainbm14eheld~/.train.lock(it had run 2.5 h+), and I stopped it. Runtiering-scale DIR ids|inventory|aggregate|history|blocks|moves(release,--metadata-onlywhere noted in #793) once the train ends; I did not do it, so the last fixture results are still those at2473b998.Rethink after repeated misses: where the time goes, and three designs
No code changed in this lane. These are measurements of
perf/tiering-761-fixhead43d81bc1(binary from tree4a5e2dc6, sha256f6ac38e1…; the later commits only change tests and formatting) against dev41f2efa3(01522f72…). The harness is352d448a…. All three binaries were built on dev-01 and checked by sha256 on dev-01, the control host and perf-01.What tiering slice 1 has to achieve
From the approved plan §2–3 and #761:
ids.jsonlload/unload with paged SSD B-trees (ID → parent/name, child index). Use cursors of at most 50 entries and a global index cache of 64 MiB, so 100 Homes of 100k notes with ten active fit without holding every Home's id map in RAM.The steady-state win is real: idle RSS is about 9% lower than dev (#issuecomment-72561), and cold peak memory is 0.82× here. The misses are all on the cold fill, plus the window right after a warm start.
Measured (perf-01, 100k Home, seed 1, both perf locks held, three rotated reps head/dev/devB, no page-cache drop)
Medians of 3. The command for each rep:
PERF_ONLY=list,query,journal,open calternal-perf --bin <bin> --work <w> --sizes 100k --runs 1 --cold-runs 1 --seed 1 --idle-secs 0. This reproduces the Option A comment (2.49× / 1.16×).Cost breakdown: cold first page and cold index
The first page waits for the first metadata commit, and that commit waits for the whole identity phase. Head's scan phase timers on dev-01 at 100k (shared host, diagnostic only): stat 0.73 s · classify 0.32 · read_heads 1.37 · identities 5.40 · describe 29.5 (in batches; the first page lands after its first batch). Dev doesn't log phases; the earlier dev-01 pass put its identity phase near 0.7 s. The identity phase is the gap.
Callgrind, 10k cold start, same fixture (instructions, inclusive):
scan::identify_manyflush_identities→Index::enqueue(stage into the TEMP overlay with 3 indexes +known_ids)paths_of/ids_at_foldsindex::apply, own connection)page_after+ids_at_folds)meta::described)read_ids)MetaDb::apply/end_fresh)The writer pattern alone was replayed with Python + SQLite 3.53.3 at 100k identities on quiet perf-01. Durability was kept: log fsync per 4,000-row group,
synchronous=FULL. Read the shares, not the absolute numbers (Python adds per-row overhead to every variant):synchronous=NORMALinstead of FULLfsync count from a cold run under
strace -f -T -e fsync,fdatasyncon perf-01: head made 249 fsyncs (4.75 s of cumulative fsync wall time over the cold run), dev 157 (3.83 s). The extra +92 are all onids.db-wal65,ids.db16 andids.jsonl+7, for 0.97 s vs 0.12 s on the identity files, spread over the whole fill. fsyncs and FULL sync are not the main cost. The main cost is the B-tree work per group (TEXT keys, fold index, trigger rows) through a small cache, plus staging every row twice (TEMP overlay, then the projection).Not measured: a usable
strace -csyscall count for head at 100k on dev-01. Understrace -fthe first page passed its own deadline ("note discovery has not produced a validated page yet") and the run aborted. The fsync-only trace above replaces it.Steady-state regressions: query shape is unchanged, they overlap the warm start-up scan
meta.rscandidate_page and the store are unchanged), and so is themeta.dbschema.EXPLAIN QUERY PLANon a copy of each finished Home is identical:SEARCH tags USING PRIMARY KEY+SEARCH n USING INDEX sqlite_autoindex_notes_1+ temp B-tree for ORDER BYSCAN n USING INDEX notes_updatedmeta.dbis 206,735 vs 206,710 pages.The first burst took 1.84–1.92 s on head vs 1.51–1.56 s on dev.
scan::run535 M vs 144 M instructions. Head runs 250 × 2IN (…)lookups (ids_at_folds171 M,paths_of146 M) plus a separatepage_afterpass (94 M); dev does one hash lookup per file (id_at33 M).Three designs, simplest first
index.rs/scan.rsto dev's map + log)entries_fold, not 500INqueries +page_after.Rejected by measurement:
Recommendation for kayg: B. It's the only design predicted flat on every hot path that still delivers the paging goal (lower steady-state RSS, bounded caches across many Homes).
The trade-offs:
If kayg wants the least code and accepts "paging later", C is a revert: zero risk and zero perf delta. A alone doesn't meet the no-regression rule on this evidence.
Gate for whichever design is chosen: the same 3-rep rotated head/dev/dev-copy run plus the during/after warm split, and the #793 fixtures. Search is still unmeasured (see the correction above).
Artifacts are attached to this comment:
perf01.tgzhas the rep JSON/MD, warm split, replay output, fsync traces and EQP;dev01.tgzhas the 10k/100k phase logs, replay output and harness reports. Work dirs were deleted on dev-01, perf-01 and perf-02. Both perf-lock holders (trr-hold) were stopped at about 15:40 CET (20:10 IST). Nothing merged.kayg referenced this pull request2026-11-05 23:21:11 +00:00
housekeeping #1009: needs-decision.
Old paged identity projection has unresolved cold-index/first-page regressions and A/B/C design choices, plus option-B WIP #953. Retain the measurements; do not revive the old tiering stack.
original head:
487e058908d3b2f24389627741c33907ee7beb25. against devcd7f84432febfc63b788fbb9b9f9950452982d74, git cherry reports 68 unmatched and 0 equivalent patches. the landing audit lists each candidate missing commit and stable patch match inpr-792.md; unmatched patches can include superseded behavior.left open, source branch unchanged. no rebase or new test claim for this decision lane.
Superseded by tiering v3 (#968/#970/#1008) and #953. The branch and original tips are archived under
archive/(see #1009), so nothing is lost; useful regression vectors are noted there for the active lanes. Closing.Pull request closed