PERF: one bounded view-snapshot LRU for recent views (#663) #666

Open
opened 2026-10-02 05:20:40 +00:00 by kayg · 3 comments
Owner

Context: #663 instant-interaction audit. No product change is part of the audit job.

Own rule 6 and only the shared snapshot primitive in DESIGN §58. Tab adoption issues own each adapter.

Evidence at c4a61e8cf0:

  • Calendar already retains 4 ranges, 6 grids and 2 years (apps/web/src/lib/calendar/data.ts:77); it preserves stale snapshots and rejects stale request generations.
  • Files retains 8 first pages (apps/web/src/lib/files/api.ts:39), but does not retain the full loaded window, cursor chain and scroll as one bounded snapshot.
  • Photos retains filter stores (apps/web/src/lib/photos/timeline.svelte.ts:307). Money clears account state on a Budget change (apps/web/src/lib/money/store.svelte.ts:57).

Expected:

  • Extend #549 into one typed LRU keyed by User and stable view identity, including URL filter/sort/zone. Snapshot rows, cursor window, selection, scroll and completeness together. Set byte, row and entry limits, including in-flight work.
  • Restore before any await; keep retained content visible during one bounded catch-up. Restore focus only after the target exists. Preserve deep-link intent if it overrides retained selection or scroll.
  • Keep stale-generation guards and revision/object identity. Coordinate mutations and deltas with the same snapshot, rather than invalidate every view. Auth/session end, revoke and plugin disable clear affected views through #555. Persist only safe recent data if needed; never raw Admin TOML or secrets.

Tests:

  • A→B→A restores rows, scroll, cursors and selected stable ID in the first frame, including after rename. Verify changed query/sort/zone isolation, failed refresh, stale response, deleted selection and cache eviction.
  • User switch and share revoke cannot restore unauthorized rows. Keyboard, touch, reduced motion and deep-link restoration work.
  • Extend #549/#641: ≥5 production/HDD cold and warm samples, median/p95/max, warm Tab ≤100 ms; 500-step RSS/heap remains bounded. No spinner over retained data.

Reuse: #555 and #549; integrate completed #641 blaze-surfaces/blaze-settings, #642 and #640 before adding adapters. This issue does not redo their traversal, decoded-image work or layouts.

Measurement evidence: see the production/HDD matrix posted on #663. Latencies of sampled endpoints do not prove the shared contract is complete.

Locked production measurement evidence:
The Tab phase retained eight samples before requested Tab did not become selected. Warm Photos→Calendar had Calendar DOM at 401.1 ms and full target paint at 3423.8 ms (n=1). Warm Calendar→Photos full paint was 573.1 ms (n=1). Cold Calendar target DOM median/max over three samples was 1390.1/2291.1 ms. A five-sample condition was not completed, so these are diagnostics, not acceptance percentiles or a controlled comparison to #549.
Runtime server source cc25c441b7, binary SHA-256 2f3567d91c34839851247bc0acbc25a56aaacd14dca269b8f0342ddf83447ed9, shared release binary with embedded production SPA. The source audit uses the newer c4a61e8 base; no code equivalence claim. Perf VM HDD emulator uses bench/hdd-emu.sh (direct-I/O/ext4, 8 ms read/write delay, 200 IOPS and 150 MiB/s caps) and flock -w 14400 /root/perf.lock for every measured phase. First valid qualification QD1 115.3 IOPS/8.028 ms median, QD16 200.7 IOPS/96.993 ms; load 0.10/0.18/0.08 and 0.20/0.20/0.09. Fixture: 366 Daily notes, 10,980 Logs, 100 Files/Photos, 20 Notes/Tasks, three Budgets and 100 transactions; Mail empty and Admin one User. No matching transport/HDD/fixture baseline exists, so no regression ratio is claimed. Full raw scopes, failure retention and endpoint baseline references are on #663.

Context: #663 instant-interaction audit. No product change is part of the audit job. Own rule 6 and only the shared snapshot primitive in DESIGN §58. Tab adoption issues own each adapter. Evidence at c4a61e8cf090170f35b1bed3350d9de20c83ecd5: - Calendar already retains 4 ranges, 6 grids and 2 years (`apps/web/src/lib/calendar/data.ts:77`); it preserves stale snapshots and rejects stale request generations. - Files retains 8 first pages (`apps/web/src/lib/files/api.ts:39`), but does not retain the full loaded window, cursor chain and scroll as one bounded snapshot. - Photos retains filter stores (`apps/web/src/lib/photos/timeline.svelte.ts:307`). Money clears account state on a Budget change (`apps/web/src/lib/money/store.svelte.ts:57`). Expected: - Extend #549 into one typed LRU keyed by User and stable view identity, including URL filter/sort/zone. Snapshot rows, cursor window, selection, scroll and completeness together. Set byte, row and entry limits, including in-flight work. - Restore before any await; keep retained content visible during one bounded catch-up. Restore focus only after the target exists. Preserve deep-link intent if it overrides retained selection or scroll. - Keep stale-generation guards and revision/object identity. Coordinate mutations and deltas with the same snapshot, rather than invalidate every view. Auth/session end, revoke and plugin disable clear affected views through #555. Persist only safe recent data if needed; never raw Admin TOML or secrets. Tests: - A→B→A restores rows, scroll, cursors and selected stable ID in the first frame, including after rename. Verify changed query/sort/zone isolation, failed refresh, stale response, deleted selection and cache eviction. - User switch and share revoke cannot restore unauthorized rows. Keyboard, touch, reduced motion and deep-link restoration work. - Extend #549/#641: ≥5 production/HDD cold and warm samples, median/p95/max, warm Tab ≤100 ms; 500-step RSS/heap remains bounded. No spinner over retained data. Reuse: #555 and #549; integrate completed #641 blaze-surfaces/blaze-settings, #642 and #640 before adding adapters. This issue does not redo their traversal, decoded-image work or layouts. Measurement evidence: see the production/HDD matrix posted on #663. Latencies of sampled endpoints do not prove the shared contract is complete. Locked production measurement evidence: The Tab phase retained eight samples before `requested Tab did not become selected`. Warm Photos→Calendar had Calendar DOM at 401.1 ms and full target paint at 3423.8 ms (n=1). Warm Calendar→Photos full paint was 573.1 ms (n=1). Cold Calendar target DOM median/max over three samples was 1390.1/2291.1 ms. A five-sample condition was not completed, so these are diagnostics, not acceptance percentiles or a controlled comparison to #549. Runtime server source cc25c441b7a974185622a1dee853cf38686d2b67, binary SHA-256 2f3567d91c34839851247bc0acbc25a56aaacd14dca269b8f0342ddf83447ed9, shared release binary with embedded production SPA. The source audit uses the newer c4a61e8 base; no code equivalence claim. Perf VM HDD emulator uses bench/hdd-emu.sh (direct-I/O/ext4, 8 ms read/write delay, 200 IOPS and 150 MiB/s caps) and flock -w 14400 /root/perf.lock for every measured phase. First valid qualification QD1 115.3 IOPS/8.028 ms median, QD16 200.7 IOPS/96.993 ms; load 0.10/0.18/0.08 and 0.20/0.20/0.09. Fixture: 366 Daily notes, 10,980 Logs, 100 Files/Photos, 20 Notes/Tasks, three Budgets and 100 transactions; Mail empty and Admin one User. No matching transport/HDD/fixture baseline exists, so no regression ratio is claimed. Full raw scopes, failure retention and endpoint baseline references are on #663.
Author
Owner

Starting #666 on job/perf-snap-666, based on dev at c4a61e8cf090170f35b1bed3350d9de20c83ecd5. I am reviewing the shared snapshot contract in #663 / DESIGN §58 before implementation.

Starting #666 on `job/perf-snap-666`, based on `dev` at `c4a61e8cf090170f35b1bed3350d9de20c83ecd5`. I am reviewing the shared snapshot contract in #663 / DESIGN §58 before implementation.
Author
Owner

Source finding at c4a61e8cf090170f35b1bed3350d9de20c83ecd5: Calendar retains separate count-bounded maps (4 ranges, 6 grids, 2 years) in apps/web/src/lib/calendar/data.ts; Files retains eight first pages in apps/web/src/lib/files/api.ts, while listAll accumulates the full folder in the active view. Photos retains two filter stores in apps/web/src/lib/photos/timeline.svelte.ts. None shares a typed entry/row/byte budget with reserved in-flight snapshots or stores rows, cursor window, stable selection, scroll and completeness as one value. That matches R6 in the #663 matrix. I will implement the shared in-memory LRU with 4 entries, 2,000 resident-plus-reserved rows and 8 MiB estimated resident-plus-reserved bytes; no persistence is needed for this ephemeral view state.

Source finding at `c4a61e8cf090170f35b1bed3350d9de20c83ecd5`: Calendar retains separate count-bounded maps (4 ranges, 6 grids, 2 years) in `apps/web/src/lib/calendar/data.ts`; Files retains eight first pages in `apps/web/src/lib/files/api.ts`, while `listAll` accumulates the full folder in the active view. Photos retains two filter stores in `apps/web/src/lib/photos/timeline.svelte.ts`. None shares a typed entry/row/byte budget with reserved in-flight snapshots or stores rows, cursor window, stable selection, scroll and completeness as one value. That matches R6 in the #663 matrix. I will implement the shared in-memory LRU with 4 entries, 2,000 resident-plus-reserved rows and 8 MiB estimated resident-plus-reserved bytes; no persistence is needed for this ephemeral view state.
Author
Owner

Completed — #666

Built the shared in-memory ViewSnapshotLru and prepared per-User view identities. A snapshot holds rows, cursor window, stable selection IDs, scroll position and anchor, completeness, and optional revision. Filter, sort, zone and other URL state form part of the key. The LRU counts saved snapshots and in-flight reservations against the same limits; session/auth changes and plugin changes clear the shared cache. Selection restore prunes deleted IDs and lets a deep-link target take precedence once that row exists.

The #663 documentation commits were cherry-picked before implementation. The branch has three implementation commits after those docs commits. Head: 253c2a00cade24a7f845a5e67f309093641b8850.

Files: apps/web/src/lib/viewSnapshots.ts, apps/web/src/lib/viewSnapshots.test.ts, bench/view-snapshot-lru.mjs, docs/perf/view-snapshot-lru-2026-10-02.md, docs/perf/runs/view-snapshot-lru-2026-10-02.json, CLAUDE.md, and docs/DESIGN.md §58. No dependencies, APIs, routes, Rust crates or lockfiles changed.

Gates

bun run check:

User browser caches use userStorage; only documented device/public-link exceptions remain.
Text sizes and UI shape values use shared role tokens.
UI transitions and animation options use shared motion tokens or documented exceptions.
Loading svelte-check in workspace: /home/kayg/Developer/calternal-wt/perf-snap-666/apps/web
Getting Svelte diagnostics...
svelte-check found 0 errors and 0 warnings

bun run test:

Test Files  154 passed (154)
      Tests  1061 passed (1061)
   Start at  11:28:13
   Duration  80.52s (transform 49%, environment 19%, import 15%, tests 12%, setup 4%)

bun run build succeeded: ✓ built in 32.45s; the static adapter wrote the site to build. cargo fmt --check exited 0 with no output. No Rust crate changed, so Rust clippy and Rust tests were not applicable. Build output was removed and cargo clean completed.

Performance

The perf VM was occupied by another job holding /root/perf.lock, so I stopped the lock wait and measured locally. The minified production module profile used five samples, 1,000 warm reads per sample, and 500 churn steps over eight view keys with 500 rows per snapshot. Warm read p50/p95/max was 0.00030 / 0.00132 / 0.00132 ms. Churn latency was 405.80 / 578.01 / 578.01 ms; process CPU was 249.90 / 278.59 / 278.59 ms. The maximum cache usage was four entries, 2,000 rows and 1,024,000 estimated bytes. Heap retained after GC was at most 28,208 bytes. The full profile is in docs/perf/runs/view-snapshot-lru-2026-10-02.json.

This is local in-memory evidence, not an HDD or Tab-switch result. No matching profile exists in docs/perf/baseline.json, so there is no regression comparison. The run did not record a perf-VM load average.

UX gaps closed / left

  • Closed in the shared contract and tests: A→B→A returns rows, cursors, selection and scroll together; stable IDs survive rename; deleted IDs are pruned; URL intent can override retained selection; failed refresh keeps the last snapshot; stale work after view/User invalidation cannot publish.
  • Left: no Tab adopts the primitive in this issue. The actual first-frame paint, focus/scroll restore, warm Tab budget and touch/keyboard screen behavior need an adapter in its owning follow-up issue (#669–#676 or #701). No route changed, so screenshots and a server cross-User route matrix were not applicable.

Decisions and known gaps

  • Snapshots stay in memory. userStorage supplies the active User identity and session events clear the cache; persistent view data is not needed for this ephemeral state.
  • Default limits are four entries, 2,000 rows and 8 MiB total, including at most two in-flight reservations. Adapters supply a conservative byte estimate because JavaScript object size is not directly available.
  • The benchmark uses a minified module build and synthetic test rows outside the UI. It does not establish the §58 warm Tab budget; a mode adapter and locked perf-VM run remain follow-up work.
## Completed — #666 Built the shared in-memory `ViewSnapshotLru` and prepared per-User view identities. A snapshot holds rows, cursor window, stable selection IDs, scroll position and anchor, completeness, and optional revision. Filter, sort, zone and other URL state form part of the key. The LRU counts saved snapshots and in-flight reservations against the same limits; session/auth changes and plugin changes clear the shared cache. Selection restore prunes deleted IDs and lets a deep-link target take precedence once that row exists. The #663 documentation commits were cherry-picked before implementation. The branch has three implementation commits after those docs commits. Head: `253c2a00cade24a7f845a5e67f309093641b8850`. Files: `apps/web/src/lib/viewSnapshots.ts`, `apps/web/src/lib/viewSnapshots.test.ts`, `bench/view-snapshot-lru.mjs`, `docs/perf/view-snapshot-lru-2026-10-02.md`, `docs/perf/runs/view-snapshot-lru-2026-10-02.json`, `CLAUDE.md`, and `docs/DESIGN.md` §58. No dependencies, APIs, routes, Rust crates or lockfiles changed. ## Gates `bun run check`: ```text User browser caches use userStorage; only documented device/public-link exceptions remain. Text sizes and UI shape values use shared role tokens. UI transitions and animation options use shared motion tokens or documented exceptions. Loading svelte-check in workspace: /home/kayg/Developer/calternal-wt/perf-snap-666/apps/web Getting Svelte diagnostics... svelte-check found 0 errors and 0 warnings ``` `bun run test`: ```text Test Files 154 passed (154) Tests 1061 passed (1061) Start at 11:28:13 Duration 80.52s (transform 49%, environment 19%, import 15%, tests 12%, setup 4%) ``` `bun run build` succeeded: `✓ built in 32.45s`; the static adapter wrote the site to `build`. `cargo fmt --check` exited 0 with no output. No Rust crate changed, so Rust clippy and Rust tests were not applicable. Build output was removed and `cargo clean` completed. ## Performance The perf VM was occupied by another job holding `/root/perf.lock`, so I stopped the lock wait and measured locally. The minified production module profile used five samples, 1,000 warm reads per sample, and 500 churn steps over eight view keys with 500 rows per snapshot. Warm read p50/p95/max was `0.00030 / 0.00132 / 0.00132 ms`. Churn latency was `405.80 / 578.01 / 578.01 ms`; process CPU was `249.90 / 278.59 / 278.59 ms`. The maximum cache usage was four entries, 2,000 rows and 1,024,000 estimated bytes. Heap retained after GC was at most 28,208 bytes. The full profile is in `docs/perf/runs/view-snapshot-lru-2026-10-02.json`. This is local in-memory evidence, not an HDD or Tab-switch result. No matching profile exists in `docs/perf/baseline.json`, so there is no regression comparison. The run did not record a perf-VM load average. ## UX gaps closed / left - Closed in the shared contract and tests: A→B→A returns rows, cursors, selection and scroll together; stable IDs survive rename; deleted IDs are pruned; URL intent can override retained selection; failed refresh keeps the last snapshot; stale work after view/User invalidation cannot publish. - Left: no Tab adopts the primitive in this issue. The actual first-frame paint, focus/scroll restore, warm Tab budget and touch/keyboard screen behavior need an adapter in its owning follow-up issue (#669–#676 or #701). No route changed, so screenshots and a server cross-User route matrix were not applicable. ## Decisions and known gaps - Snapshots stay in memory. `userStorage` supplies the active User identity and session events clear the cache; persistent view data is not needed for this ephemeral state. - Default limits are four entries, 2,000 rows and 8 MiB total, including at most two in-flight reservations. Adapters supply a conservative byte estimate because JavaScript object size is not directly available. - The benchmark uses a minified module build and synthetic test rows outside the UI. It does not establish the §58 warm Tab budget; a mode adapter and locked perf-VM run remain follow-up work.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
kayg/calternal#666
No description provided.