PERF: Mail adopts shared revision, snapshot, mutation and delta contracts (#663) #672

Open
opened 2026-10-02 05:36:15 +00:00 by kayg · 2 comments
Owner

Context: #663 matrix, DESIGN §58 rules 3–6. This is the Mail adapter issue. The only shared owners are #665 revision cache/ETag, #666 view snapshots, #667 optimistic client IDs/Undo receipts and #668 change stream/deltas. Depend on their public contracts; do not implement competing primitives.

Evidence at c4a61e8cf0:

  • R3: GET /api/v1/mail/messages/{id}; apps/web/src/lib/mail/MailView.svelte:384. Detail open awaits a new fetch; no common revision cache/strong conditional body contract. #640 already owns reading layouts and neighbour prefetch.
  • R4: POST /api/v1/mail/messages/{id}/read-state; apps/web/src/lib/mail/MailView.svelte:238. Updates rows after awaited POST, not one synchronous commit. Existing provider sync queue is not a User-visible client-ID mutation receipt and durable Undo.
  • R5: GET /api/v1/mail/inbox/messages; apps/web/src/lib/mail/MailView.svelte:227. 30,000 ms inbox poll; no app-wide changed-since stream/delta. New-mail notice preserves current reading position and must remain.
  • R6: GET /api/v1/mail/inbox/messages; apps/web/src/lib/mail/MailView.svelte:330. Reload obtains accounts, folders, list and detail. #640/#641 have newer folder/body caches; integrate those before adapting shared snapshots.

Expected:

  • Adopt #665 for list/detail revisions and header-complete rows. Cached open is synchronous, keeps object identity and makes zero requests. Authorize before conditional 304; User switch/revoke/plugin disable removes affected retained data.
  • Adopt #666 for a byte/row-bounded complete view snapshot. Restore before await, then one bounded catch-up; no blank/spinner over already-seen data. Deep-link intent and keyboard focus override a retained view when needed.
  • Adopt #667 for read/unread, flag, archive/move and trash when those operations exist; preserve current reader and new-mail notice; restore folder/filter/window and open message in each #640 layout. Changes publish to every visible/retained representation in one frame. Client IDs survive lost acknowledgement; Undo uses the durable receipt and cannot erase an intervening change. Definite rejection rolls back; timeout remains pending until receipt reconciliation. Do not add offline editing.
  • Adopt #668: one per-User wake-up, coalesced capped deltas, stable unchanged objects and deletion tombstones. Stop full-refetch fan-out and timer refreshes only after the delta covers their correctness duties.

Tests:

  • Production API/e2e for the listed actions and views, including stale 304, lost acknowledgement, duplicate ID, Undo after reconnect/restart, obsolete response, empty/error/offline state and cross-view consistency. Existing status/assertion expectations stay unchanged.
  • Two Users, session switch, share revoke and plugin disable cannot restore denied rows. Keep authorization/step-up and server-only writes through calternal-fs.
  • Pointer/touch (≥44 px), keyboard focus/Enter/Space/Escape, screen-reader name/role/state, Copy link and shortcuts work. Keyboard motion keeps shared durations (#611).
  • Extend #549/#641 profiles rather than create another harness. ≥5 locked production/HDD cold and warm samples; median/p95/max, CPU/RSS and large realistic data plus burst. Cached open ≤100 ms, accepted action ≤150 ms, warm return ≤100 ms, first usable view ≤1.5 s at 10k items. Warm blaze: zero incomplete frames; collect identity/unpainted/long-task/heap/RSS metrics.

Reuse and active work: #640 job/maillayouts is the reading layout/neighbor cache owner; #641 owns completeness runs; #626 owns duplicate UID membership; #613 sync status and #614 provider sync are separate. Integrate them first.

Measurement scope: the production/HDD read table on #663 supplies a representative endpoint result, not proof that a complete Mail view meets all budgets. Rule 7/9 findings remain with #641/#549 and the existing surface jobs. This follow-up integrates their results and adds shared-contract acceptance after they land.

Representative measurement from the audit (not a full-view budget result):
/api/v1/mail/inbox/messages?limit=100, five serial requests after fixture readiness, all HTTP 200. Median/p95/max 41.1/48.0/48.0 ms; five-request burst median/p95/max 53.8/54.0/54.0 ms. Serial window server CPU 20 ms, RSS 444309504 bytes; no ETag on these sampled responses. Load inside lock 3.66/2.1/0.93.
Shared release server source cc25c441b7a974185622a1dee853cf38686d2b67, binary SHA-256 2f3567d91c34839851247bc0acbc25a56aaacd14dca269b8f0342ddf83447ed9; embedded production SPA; Chromium browser on the build host through SSH/HTTPS. Server/Home/Index on perf VM HDD emulator: direct-I/O loop, 8 ms read/write dm-delay, 200 IOPS and 150 MiB/s caps. Every measured phase held flock -w 14400 /root/perf.lock. Qualification QD1 115.3 IOPS/8.028 ms median, QD16 200.7 IOPS/96.993 ms. Fixture: 366 Daily notes, 10,980 Logs, 100 Files/Photos, 20 Notes/Tasks, three Budgets and 100 transactions; Mail empty, Admin one User.
Structural source evidence above is the newer audit base, not the measured binary revision. No claim that these revisions are equivalent. The baseline in docs/perf/baseline.json uses another fixture/build/transport; no regression ratio is valid here. See #663 for matching baseline endpoint values and coverage gaps.

Context: #663 matrix, DESIGN §58 rules 3–6. This is the Mail adapter issue. The only shared owners are #665 revision cache/ETag, #666 view snapshots, #667 optimistic client IDs/Undo receipts and #668 change stream/deltas. Depend on their public contracts; do not implement competing primitives. Evidence at c4a61e8cf090170f35b1bed3350d9de20c83ecd5: - R3: `GET /api/v1/mail/messages/{id}`; `apps/web/src/lib/mail/MailView.svelte:384`. Detail open awaits a new fetch; no common revision cache/strong conditional body contract. #640 already owns reading layouts and neighbour prefetch. - R4: `POST /api/v1/mail/messages/{id}/read-state`; `apps/web/src/lib/mail/MailView.svelte:238`. Updates rows after awaited POST, not one synchronous commit. Existing provider sync queue is not a User-visible client-ID mutation receipt and durable Undo. - R5: `GET /api/v1/mail/inbox/messages`; `apps/web/src/lib/mail/MailView.svelte:227`. 30,000 ms inbox poll; no app-wide changed-since stream/delta. New-mail notice preserves current reading position and must remain. - R6: `GET /api/v1/mail/inbox/messages`; `apps/web/src/lib/mail/MailView.svelte:330`. Reload obtains accounts, folders, list and detail. #640/#641 have newer folder/body caches; integrate those before adapting shared snapshots. Expected: - Adopt #665 for list/detail revisions and header-complete rows. Cached open is synchronous, keeps object identity and makes zero requests. Authorize before conditional 304; User switch/revoke/plugin disable removes affected retained data. - Adopt #666 for a byte/row-bounded complete view snapshot. Restore before await, then one bounded catch-up; no blank/spinner over already-seen data. Deep-link intent and keyboard focus override a retained view when needed. - Adopt #667 for read/unread, flag, archive/move and trash when those operations exist; preserve current reader and new-mail notice; restore folder/filter/window and open message in each #640 layout. Changes publish to every visible/retained representation in one frame. Client IDs survive lost acknowledgement; Undo uses the durable receipt and cannot erase an intervening change. Definite rejection rolls back; timeout remains pending until receipt reconciliation. Do not add offline editing. - Adopt #668: one per-User wake-up, coalesced capped deltas, stable unchanged objects and deletion tombstones. Stop full-refetch fan-out and timer refreshes only after the delta covers their correctness duties. Tests: - Production API/e2e for the listed actions and views, including stale 304, lost acknowledgement, duplicate ID, Undo after reconnect/restart, obsolete response, empty/error/offline state and cross-view consistency. Existing status/assertion expectations stay unchanged. - Two Users, session switch, share revoke and plugin disable cannot restore denied rows. Keep authorization/step-up and server-only writes through calternal-fs. - Pointer/touch (≥44 px), keyboard focus/Enter/Space/Escape, screen-reader name/role/state, Copy link and shortcuts work. Keyboard motion keeps shared durations (#611). - Extend #549/#641 profiles rather than create another harness. ≥5 locked production/HDD cold and warm samples; median/p95/max, CPU/RSS and large realistic data plus burst. Cached open ≤100 ms, accepted action ≤150 ms, warm return ≤100 ms, first usable view ≤1.5 s at 10k items. Warm blaze: zero incomplete frames; collect identity/unpainted/long-task/heap/RSS metrics. Reuse and active work: #640 job/maillayouts is the reading layout/neighbor cache owner; #641 owns completeness runs; #626 owns duplicate UID membership; #613 sync status and #614 provider sync are separate. Integrate them first. Measurement scope: the production/HDD read table on #663 supplies a representative endpoint result, not proof that a complete Mail view meets all budgets. Rule 7/9 findings remain with #641/#549 and the existing surface jobs. This follow-up integrates their results and adds shared-contract acceptance after they land. Representative measurement from the audit (not a full-view budget result): `/api/v1/mail/inbox/messages?limit=100`, five serial requests after fixture readiness, all HTTP 200. Median/p95/max 41.1/48.0/48.0 ms; five-request burst median/p95/max 53.8/54.0/54.0 ms. Serial window server CPU 20 ms, RSS 444309504 bytes; no ETag on these sampled responses. Load inside lock 3.66/2.1/0.93. Shared release server source `cc25c441b7a974185622a1dee853cf38686d2b67`, binary SHA-256 `2f3567d91c34839851247bc0acbc25a56aaacd14dca269b8f0342ddf83447ed9`; embedded production SPA; Chromium browser on the build host through SSH/HTTPS. Server/Home/Index on perf VM HDD emulator: direct-I/O loop, 8 ms read/write dm-delay, 200 IOPS and 150 MiB/s caps. Every measured phase held `flock -w 14400 /root/perf.lock`. Qualification QD1 115.3 IOPS/8.028 ms median, QD16 200.7 IOPS/96.993 ms. Fixture: 366 Daily notes, 10,980 Logs, 100 Files/Photos, 20 Notes/Tasks, three Budgets and 100 transactions; Mail empty, Admin one User. Structural source evidence above is the newer audit base, not the measured binary revision. No claim that these revisions are equivalent. The baseline in docs/perf/baseline.json uses another fixture/build/transport; no regression ratio is valid here. See #663 for matching baseline endpoint values and coverage gaps.
Author
Owner

Client audit of queued Mail layouts for #663; no duplicate issue.

The old dev MailView serial account/folder/preferences/list/body chain is superseded by job/maillayouts f9f360e68f. That branch has parallel loads, bounded body caches, virtual rows and directional prefetch; retain those changes.

Remaining evidence in that branch: MailView.svelte:878–891 appends older pages to messages without a mounted row/byte cap. loadOlderThread (:863 onward) also appends all visited thread headers. :177–187 groups the whole accumulated message array after changes. storeListSnapshot (:406–416) copies the whole array; readerCache.ts mailListCache computes weight by reducing all its messages. An oversized snapshot is rejected from that cache, but the mounted messages array remains large. Virtual DOM and cache limits alone do not bound the active model. Long scroll can thus remove warm return caching exactly when it is most needed.

Use a bounded active page window plus forward/backward cursors and selection identity; snapshot the bounded window and reuse unchanged thread rows. Keep Select all semantics across unloaded rows. Acceptance: long 100k-mailbox traversal with resident caps, return to recent folder, old-thread expansion, deep links, 15ms blaze and heap checks. This is source complexity evidence, not a measured timing regression. Coordinate with #685 and the Mail layout owner.

Client audit of queued Mail layouts for #663; no duplicate issue. The old dev MailView serial account/folder/preferences/list/body chain is superseded by job/maillayouts f9f360e68f4e9ca106ddd7fea24d1f4363881a2b. That branch has parallel loads, bounded body caches, virtual rows and directional prefetch; retain those changes. Remaining evidence in that branch: MailView.svelte:878–891 appends older pages to messages without a mounted row/byte cap. loadOlderThread (:863 onward) also appends all visited thread headers. :177–187 groups the whole accumulated message array after changes. storeListSnapshot (:406–416) copies the whole array; readerCache.ts mailListCache computes weight by reducing all its messages. An oversized snapshot is rejected from that cache, but the mounted messages array remains large. Virtual DOM and cache limits alone do not bound the active model. Long scroll can thus remove warm return caching exactly when it is most needed. Use a bounded active page window plus forward/backward cursors and selection identity; snapshot the bounded window and reuse unchanged thread rows. Keep Select all semantics across unloaded rows. Acceptance: long 100k-mailbox traversal with resident caps, return to recent folder, old-thread expansion, deep links, 15ms blaze and heap checks. This is source complexity evidence, not a measured timing regression. Coordinate with #685 and the Mail layout owner.
Author
Owner

#663 bundle/loading audit: MailView.svelte:324–380 (origin/dev c4a61e8cf0) puts account discovery, all account-folder reads, preferences, and only then list/message reads on the first usable path. A thread also reads attachments before its first body. This is a source-backed dependency chain, not a latency measurement. File size is relatively small (Mail static route adds 10,856 B gzip to the 369,672 B shell), so code splitting alone cannot solve Mail readiness. Reuse #672's revision/snapshot contracts, render cached rows/header first and fetch ancillary folders/preferences/attachments independently. Test with delayed ancillary reads to prove the primary content appears before they finish. Keep #640/#672 as owners; no duplicate issue filed.

#663 bundle/loading audit: MailView.svelte:324–380 (origin/dev c4a61e8cf090170f35b1bed3350d9de20c83ecd5) puts account discovery, all account-folder reads, preferences, and only then list/message reads on the first usable path. A thread also reads attachments before its first body. This is a source-backed dependency chain, not a latency measurement. File size is relatively small (Mail static route adds 10,856 B gzip to the 369,672 B shell), so code splitting alone cannot solve Mail readiness. Reuse #672's revision/snapshot contracts, render cached rows/header first and fetch ancillary folders/preferences/attachments independently. Test with delayed ancillary reads to prove the primary content appears before they finish. Keep #640/#672 as owners; no duplicate issue filed.
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#672
No description provided.