Tabs: rapid switching storm must not spike memory, CPU or requests #1338

Open
opened 2026-10-07 07:26:01 +00:00 by kayg · 48 comments
Owner

Owner request 2026-10-07: Tab switching must be instant, and randomly spamming Tab switches in rapid succession must not cause memory growth, request pile-up, or CPU spikes. This issue owns a repeatable storm profile with budgets and the baseline measurement on dev; fixes land under #1233 (warm Tab switch owner).

Budgets for the storm (proposed; DESIGN §59 rules 6-9):

  • Final state correct: after the storm stops, the last selected Tab shows its own content within 100 ms; never stale content of an earlier Tab.
  • No request pile-up: requests for a Tab the User already left are aborted; in-flight requests stay bounded (at most one per visible Tab view).
  • Memory bounded: JS heap and renderer RSS after the storm and a forced GC return to within 10% of the steady state after one visit of each Tab; no growth across repeated storms.
  • CPU: no long task over 50 ms caused by a switch; main thread returns to idle within 1 s after the storm; server CPU and RSS do not climb across storms.
Owner request 2026-10-07: Tab switching must be instant, and randomly spamming Tab switches in rapid succession must not cause memory growth, request pile-up, or CPU spikes. This issue owns a repeatable storm profile with budgets and the baseline measurement on dev; fixes land under #1233 (warm Tab switch owner). Budgets for the storm (proposed; DESIGN §59 rules 6-9): - Final state correct: after the storm stops, the last selected Tab shows its own content within 100 ms; never stale content of an earlier Tab. - No request pile-up: requests for a Tab the User already left are aborted; in-flight requests stay bounded (at most one per visible Tab view). - Memory bounded: JS heap and renderer RSS after the storm and a forced GC return to within 10% of the steady state after one visit of each Tab; no growth across repeated storms. - CPU: no long task over 50 ms caused by a switch; main thread returns to idle within 1 s after the storm; server CPU and RSS do not climb across storms.
Author
Owner

Starting tabstorm-1338 on branch job/tabstorm-1338, based on dev SHA 3601719b80. I have read CLAUDE.md, CONTEXT.md, and DESIGN §59. I will add the storm profile and baseline report only; no product code changes.

Starting tabstorm-1338 on branch job/tabstorm-1338, based on dev SHA 3601719b80a39f712daf07b37879b22929dfffa0. I have read CLAUDE.md, CONTEXT.md, and DESIGN §59. I will add the storm profile and baseline report only; no product code changes.
Author
Owner

Finding: the brief names bench/interaction-1124.mjs, but no such file exists in this checkout or in the origin/dev tree (rg --files and git ls-tree -r origin/dev). I will reuse the project-wide Playwright/auth/API fixtures from apps/web/e2e/harness.mjs and the existing benchmark/fixture utilities instead. The perf VM is held by tabswitch-1233 at the time of this check; I am not starting a measurement until perf-run releases it.

Finding: the brief names bench/interaction-1124.mjs, but no such file exists in this checkout or in the origin/dev tree (`rg --files` and `git ls-tree -r origin/dev`). I will reuse the project-wide Playwright/auth/API fixtures from apps/web/e2e/harness.mjs and the existing benchmark/fixture utilities instead. The perf VM is held by tabswitch-1233 at the time of this check; I am not starting a measurement until perf-run releases it.
Author
Owner

Finding: the requested helper bench/interaction-1124.mjs is absent on the baseline branch. I verified this with git ls-tree origin/dev bench/interaction-1124.mjs. The profile reuses the existing e2e harness and fixture generators named in the report; no product code changed.

Finding: the requested helper `bench/interaction-1124.mjs` is absent on the baseline branch. I verified this with `git ls-tree origin/dev bench/interaction-1124.mjs`. The profile reuses the existing e2e harness and fixture generators named in the report; no product code changed.
Author
Owner

Decision for two budgets without numeric cutoffs: “idle within one second” means at most 5% main-thread busy in the one-second sample. “Server CPU and RSS do not climb” means the three post-storm values do not rise in strict order. The memory baseline is after visiting all nine views and before storm one. These rules are also in the committed method report.

Decision for two budgets without numeric cutoffs: “idle within one second” means at most 5% main-thread busy in the one-second sample. “Server CPU and RSS do not climb” means the three post-storm values do not rise in strict order. The memory baseline is after visiting all nine views and before storm one. These rules are also in the committed method report.
Author
Owner

Setup finding: the perf VM could not launch the release server. The loader reported libsherpa-onnx-c-api.so missing. Local ldd showed that both libsherpa-onnx-c-api.so and libonnxruntime.so resolve from this worktree’s Cargo target directory. The VM session was stopped and its lock is free. I am updating the benchmark bootstrap to copy only those target-local shared libraries and set LD_LIBRARY_PATH to the job-owned directory; no product code changes.

Setup finding: the perf VM could not launch the release server. The loader reported `libsherpa-onnx-c-api.so` missing. Local `ldd` showed that both `libsherpa-onnx-c-api.so` and `libonnxruntime.so` resolve from this worktree’s Cargo target directory. The VM session was stopped and its lock is free. I am updating the benchmark bootstrap to copy only those target-local shared libraries and set `LD_LIBRARY_PATH` to the job-owned directory; no product code changes.
Author
Owner

Setup finding: /readyz returned 200 and the seeded Files view loaded, but the Photos timeline did not reach 500 items in three minutes. The Photos Index can start before the Files Index adopts an offline fixture, and startup adoption does not emit a Photos change event (#470). apps/web/e2e/photos-perf.mjs handles this by changing the Photos library roots after Files are ready, which starts a refresh. I am applying that existing fixture setup to this profile; no product code changes.

Setup finding: `/readyz` returned 200 and the seeded Files view loaded, but the Photos timeline did not reach 500 items in three minutes. The Photos Index can start before the Files Index adopts an offline fixture, and startup adoption does not emit a Photos change event (#470). `apps/web/e2e/photos-perf.mjs` handles this by changing the Photos library roots after Files are ready, which starts a refresh. I am applying that existing fixture setup to this profile; no product code changes.
Author
Owner

The perf VM is still locked by tabswitch-1233 (started 2026-10-07 11:32:40 CEST, expected 60 minutes). The production web build, release server, and runtime libraries for this baseline are staged in this job's VM directory. The profile is queued with a bounded wait and has not yet taken samples. I will run both conditions after the lock is released, subject to the job time limit.

The perf VM is still locked by `tabswitch-1233` (started 2026-10-07 11:32:40 CEST, expected 60 minutes). The production web build, release server, and runtime libraries for this baseline are staged in this job's VM directory. The profile is queued with a bounded wait and has not yet taken samples. I will run both conditions after the lock is released, subject to the job time limit.
Author
Owner

Benchmark setup finding: the current readiness probe calls GET /api/v1/files/entries?path=Photos&limit=1 and waits for total >= 500. On the running seeded Home, that endpoint reported total=10 while the Photos timeline reported 0/500. The fixture writes images under Photos/<year>/<year-month>/, so the Files listing at Photos counts immediate year folders, not all nested image files. This condition cannot reach 500. I am stopping this setup before it records any storm samples and changing the probe to wait for the final seeded image in its month folder before requesting the existing Photos refresh (#470).

Benchmark setup finding: the current readiness probe calls `GET /api/v1/files/entries?path=Photos&limit=1` and waits for `total >= 500`. On the running seeded Home, that endpoint reported `total=10` while the Photos timeline reported 0/500. The fixture writes images under `Photos/<year>/<year-month>/`, so the Files listing at `Photos` counts immediate year folders, not all nested image files. This condition cannot reach 500. I am stopping this setup before it records any storm samples and changing the probe to wait for the final seeded image in its month folder before requesting the existing Photos refresh (#470).
Author
Owner

Benchmark setup finding: after Files listed the final seeded image, the profile's PUT /api/v1/photos/library-roots returned 200, but /api/v1/photos/timeline/buckets?filter=all stayed at 5/500 for more than three minutes. The existing apps/web/e2e/photos-perf.mjs setup alternates the equivalent roots ([''] and ['Photos']) every 60 seconds while the timeline is incomplete. I am bringing that bounded retry behavior into this profile and extending its setup deadline; no Tab-switch samples have been recorded.

Benchmark setup finding: after Files listed the final seeded image, the profile's `PUT /api/v1/photos/library-roots` returned 200, but `/api/v1/photos/timeline/buckets?filter=all` stayed at 5/500 for more than three minutes. The existing `apps/web/e2e/photos-perf.mjs` setup alternates the equivalent roots (`['']` and `['Photos']`) every 60 seconds while the timeline is incomplete. I am bringing that bounded retry behavior into this profile and extending its setup deadline; no Tab-switch samples have been recorded.
Author
Owner

Benchmark setup finding: the perf VM has /usr/bin/bwrap, prlimit, vips, and vipsheader, but no calternal-media-sandbox in PATH. The release server logs native media command failed with cause=Launch, and the Photos timeline stayed at 5/500 after repeated accepted root refreshes. The existing local e2e media runtime supplies the launcher and its dropcaps helper. I am staging that runtime locally and copying the required files to this job's isolated VM directory; no product code changes are involved.

Benchmark setup finding: the perf VM has `/usr/bin/bwrap`, `prlimit`, `vips`, and `vipsheader`, but no `calternal-media-sandbox` in PATH. The release server logs `native media command failed` with `cause=Launch`, and the Photos timeline stayed at 5/500 after repeated accepted root refreshes. The existing local e2e media runtime supplies the launcher and its dropcaps helper. I am staging that runtime locally and copying the required files to this job's isolated VM directory; no product code changes are involved.
Author
Owner

Benchmark setup finding: Files readiness was still incomplete after the final seeded image appeared. Read-only probes returned Photos total=1 (Backgrounds only), Photos/2019/2019-10 total=0, Photos/2023/2023-10 total=0, and Photos/2027/2027-09 total=5. The Photos timeline total also remained 5/500 after seven accepted root refreshes. The last-leaf marker does not prove the full Photos tree is in the Files Index. I am changing setup readiness to check the expected item count in every fixture month folder with bounded concurrency, then refresh Photos only when all 500 are visible.

Benchmark setup finding: Files readiness was still incomplete after the final seeded image appeared. Read-only probes returned `Photos` total=1 (`Backgrounds` only), `Photos/2019/2019-10` total=0, `Photos/2023/2023-10` total=0, and `Photos/2027/2027-09` total=5. The Photos timeline total also remained 5/500 after seven accepted root refreshes. The last-leaf marker does not prove the full Photos tree is in the Files Index. I am changing setup readiness to check the expected item count in every fixture month folder with bounded concurrency, then refresh Photos only when all 500 are visible.
Author
Owner

The corrected profile is queued again. perf-run status now shows tabswitch-1233 holding the VM from 2026-10-07 12:33:15 CEST for 60 minutes (expected release 11:33 UTC). That is eight minutes after this job's four-hour limit. The corrected Photos readiness is committed and the runner is ready to stage under its lock. If the holder releases early, this job will run; otherwise I will stop at the time limit and report the remaining measurement work.

The corrected profile is queued again. `perf-run status` now shows `tabswitch-1233` holding the VM from 2026-10-07 12:33:15 CEST for 60 minutes (expected release 11:33 UTC). That is eight minutes after this job's four-hour limit. The corrected Photos readiness is committed and the runner is ready to stage under its lock. If the holder releases early, this job will run; otherwise I will stop at the time limit and report the remaining measurement work.
Author
Owner

Tab storm profile status (#1338)

Branch job/tabstorm-1338; head a7691f609. The profile is in bench/tab-storm.mjs; the method and status are in docs/perf/2026-10-07-1338-tabstorm.md.

No storm measurements were captured. At 10:38 UTC, perf-run status showed tabswitch-1233 holding the perf VM lock from 10:33:15 UTC with a 60 minute reservation, through 11:33:15 UTC. That was past this job's 11:25 UTC limit. Earlier setup-only attempts stopped at Photos Index readiness. The final queued attempt was stopped before lock acquisition. The directory-wide Photos readiness change remains unverified end to end. No storm, warm-pair run, CSV, Chrome trace, heap snapshot, or measured cause exists. No performance-cause issue was filed because no budget failure was measured.

Condition Warm ordered pairs 120 s storms p50 / p95 / max Post-GC growth Result
CPU ×4 0 / 72 0 / 3 Not measured Not measured Perf VM lock was reserved past job limit
Native CPU 0 / 72 0 / 3 Not measured Not measured Perf VM lock was reserved past job limit

Post-merge web gates:

PASS production dependency licences: 759 locked package releases across apps/web, apps/docs and packages
perf-lint: PASS; 0 violations; 22544 scoped exceptions
svelte-check found 0 errors and 1 warning in 1 file
 Test Files  287 passed (287)
      Tests  2036 passed (2036)
   Duration  437.22s (environment 27%, transform 26%, import 24%, tests 17%, setup 6%)

One AttachmentDeck.svelte empty CSS ruleset warning was reported by bun run check; both commands exited 0. The historical 316 ms / 594 ms profile is not Tab storm data. Numeric choices are documented in the report: idle means at most 5% main-thread busy at the one-second sample, and a server climb means three strictly increasing post-storm CPU or RSS samples.

Tab storm profile status (#1338) Branch `job/tabstorm-1338`; head `a7691f609`. The profile is in `bench/tab-storm.mjs`; the method and status are in `docs/perf/2026-10-07-1338-tabstorm.md`. No storm measurements were captured. At 10:38 UTC, `perf-run status` showed `tabswitch-1233` holding the perf VM lock from 10:33:15 UTC with a 60 minute reservation, through 11:33:15 UTC. That was past this job's 11:25 UTC limit. Earlier setup-only attempts stopped at Photos Index readiness. The final queued attempt was stopped before lock acquisition. The directory-wide Photos readiness change remains unverified end to end. No storm, warm-pair run, CSV, Chrome trace, heap snapshot, or measured cause exists. No performance-cause issue was filed because no budget failure was measured. | Condition | Warm ordered pairs | 120 s storms | p50 / p95 / max | Post-GC growth | Result | | --- | ---: | ---: | --- | --- | --- | | CPU ×4 | 0 / 72 | 0 / 3 | Not measured | Not measured | Perf VM lock was reserved past job limit | | Native CPU | 0 / 72 | 0 / 3 | Not measured | Not measured | Perf VM lock was reserved past job limit | Post-merge web gates: ```text PASS production dependency licences: 759 locked package releases across apps/web, apps/docs and packages perf-lint: PASS; 0 violations; 22544 scoped exceptions svelte-check found 0 errors and 1 warning in 1 file Test Files 287 passed (287) Tests 2036 passed (2036) Duration 437.22s (environment 27%, transform 26%, import 24%, tests 17%, setup 6%) ``` One `AttachmentDeck.svelte` empty CSS ruleset warning was reported by `bun run check`; both commands exited 0. The historical 316 ms / 594 ms profile is not Tab storm data. Numeric choices are documented in the report: idle means at most 5% main-thread busy at the one-second sample, and a server climb means three strictly increasing post-storm CPU or RSS samples.
Author
Owner

Continuing on job/tabstorm-1338 from a7691f609. Current origin/dev base is 0419170fe; I am merging it now, then I will repair Photos Index fixture readiness and run the local plus queued perf VM storms.

Continuing on job/tabstorm-1338 from a7691f609. Current origin/dev base is 0419170fe; I am merging it now, then I will repair Photos Index fixture readiness and run the local plus queued perf VM storms.
Author
Owner

Finding: the old Photos readiness fallback restarted the setup server immediately after the second accepted library-root update. That endpoint starts with (crates/plugins/photos/src/routes.rs, put_library_roots), so the restart could kill the asynchronous rebuild it had just requested. I changed the fixture to use the Photos perf probe's bounded root-alternation retry and to retry busy Index reads after 5 seconds; it now leaves the server running while Photos rebuilds. Node syntax and diff checks pass; end-to-end verification is next.

Finding: the old Photos readiness fallback restarted the setup server immediately after the second accepted library-root update. That endpoint starts with (crates/plugins/photos/src/routes.rs, put_library_roots), so the restart could kill the asynchronous rebuild it had just requested. I changed the fixture to use the Photos perf probe's bounded root-alternation retry and to retry busy Index reads after 5 seconds; it now leaves the server running while Photos rebuilds. Node syntax and diff checks pass; end-to-end verification is next.
Author
Owner

Correction to my previous finding comment: shell quoting removed the code identifiers from the message. The endpoint starts index::refresh_user with tokio::spawn in crates/plugins/photos/src/routes.rs, function put_library_roots. The previous fixture restarted the server in the same loop iteration that accepted the second library-root update, so it could terminate that background rebuild. The fixture now follows the bounded root-alternation retry used by photos-perf and leaves the server running while Photos rebuilds. Node syntax and diff checks pass; end-to-end verification is next.

Correction to my previous finding comment: shell quoting removed the code identifiers from the message. The endpoint starts index::refresh_user with tokio::spawn in crates/plugins/photos/src/routes.rs, function put_library_roots. The previous fixture restarted the server in the same loop iteration that accepted the second library-root update, so it could terminate that background rebuild. The fixture now follows the bounded root-alternation retry used by photos-perf and leaves the server running while Photos rebuilds. Node syntax and diff checks pass; end-to-end verification is next.
Author
Owner

The first local capture stopped before fixture setup with server exited before becoming ready: NotFound. Inspection of startServer showed that it uses explicit dataDirectory and userDataDirectory paths without creating them. The local profile created only its Chrome profile, while the remote runner already creates both server roots. I added those two directory creations in startLocalSession; node --check bench/tab-storm.mjs and git diff --check pass. Retrying the capture now.

The first local capture stopped before fixture setup with `server exited before becoming ready: NotFound`. Inspection of `startServer` showed that it uses explicit `dataDirectory` and `userDataDirectory` paths without creating them. The local profile created only its Chrome profile, while the remote runner already creates both server roots. I added those two directory creations in `startLocalSession`; `node --check bench/tab-storm.mjs` and `git diff --check` pass. Retrying the capture now.
Author
Owner

After Photos Index reached 500/500 entries, profile setup stopped at /api/v1/money/budgets with 404. The benchmark User had not enabled Money, whose Plugin::default_user_enabled() is false in crates/plugins/money/src/lib.rs; this was a fixture setup gap rather than an API failure. I now enable Money through its real /api/v1/plugins/money/me endpoint during User setup and assert the 204 response. The profile syntax check and whitespace check pass; retrying the local capture.

After Photos Index reached 500/500 entries, profile setup stopped at `/api/v1/money/budgets` with 404. The benchmark User had not enabled Money, whose `Plugin::default_user_enabled()` is false in `crates/plugins/money/src/lib.rs`; this was a fixture setup gap rather than an API failure. I now enable Money through its real `/api/v1/plugins/money/me` endpoint during User setup and assert the 204 response. The profile syntax check and whitespace check pass; retrying the local capture.
Author
Owner

The deterministic fixture generator reports 1,000 Tasks, but the one-pass API cursor sweep saw only 607 and stopped local setup. Task indexing continues asynchronously after Files adoption. I changed profile setup to recount the Notes and Tasks pages every two seconds, for up to three minutes, before starting measurements; the generated dimensions and expected count remain 1,000. node --check and git diff --check pass. Retrying the local capture.

The deterministic fixture generator reports 1,000 Tasks, but the one-pass API cursor sweep saw only 607 and stopped local setup. Task indexing continues asynchronously after Files adoption. I changed profile setup to recount the Notes and Tasks pages every two seconds, for up to three minutes, before starting measurements; the generated dimensions and expected count remain 1,000. `node --check` and `git diff --check` pass. Retrying the local capture.
Author
Owner

After Photos, Money, Notes, and Tasks fixtures became ready, setup reached its final Today navigation and threw ReferenceError: base is not defined in waitForProfileData. The helper used the app origin without receiving it. I added the base parameter and passed the active session origin from the caller; syntax and whitespace checks pass. Retrying the local capture.

After Photos, Money, Notes, and Tasks fixtures became ready, setup reached its final Today navigation and threw `ReferenceError: base is not defined` in `waitForProfileData`. The helper used the app origin without receiving it. I added the `base` parameter and passed the active session origin from the caller; syntax and whitespace checks pass. Retrying the local capture.
Author
Owner

The final Today assertion timed out at 10 seconds even though the Calendar range endpoint returned 200; the assertion also requires the seeded Daily recurring Event 01 to be visible. Calendar refresh is accepted asynchronously. I replaced the short navigation-plus-assertion with the existing navigateDirect helper, which waits up to 60 seconds for that exact real fixture content. Syntax and whitespace checks pass; retrying.

The final Today assertion timed out at 10 seconds even though the Calendar range endpoint returned 200; the assertion also requires the seeded `Daily recurring Event 01` to be visible. Calendar refresh is accepted asynchronously. I replaced the short navigation-plus-assertion with the existing `navigateDirect` helper, which waits up to 60 seconds for that exact real fixture content. Syntax and whitespace checks pass; retrying.
Author
Owner

The Calendar API projection contains the seeded Events, but the screen assertion could not find Daily recurring Event 01 after navigating to Today. The desktop Week view scrolls to roughly three hours before now and virtualizes off-screen grid rows; Event 01 is at 07:00. I moved the fixture readiness marker to Daily recurring Event 21 at 13:40, in the visible window at capture startup, and updated the same marker used by measurement checks. node --check and git diff --check pass; retrying.

The Calendar API projection contains the seeded Events, but the screen assertion could not find `Daily recurring Event 01` after navigating to Today. The desktop Week view scrolls to roughly three hours before now and virtualizes off-screen grid rows; Event 01 is at 07:00. I moved the fixture readiness marker to `Daily recurring Event 21` at 13:40, in the visible window at capture startup, and updated the same marker used by measurement checks. `node --check` and `git diff --check` pass; retrying.
Author
Owner

The failure screenshot (artifacts/tabstorm-1338/calendar-readiness-failure.png) showed the Calendar Week view rendering overflow badges at the busy day and no Event titles, although /api/v1/calendar/range returned the seeded Event. The previous fixture put all thirty daily recurring Events on the same day, on top of its seeded Log entries. I changed the fixture to create thirty one-time Events across the month, with one at 15:00 on the anchor day. The Calendar Tab can now show real fixture content in the initial Week view without hiding it behind overflow badges. Syntax and whitespace checks pass; retrying.

The failure screenshot (`artifacts/tabstorm-1338/calendar-readiness-failure.png`) showed the Calendar Week view rendering overflow badges at the busy day and no Event titles, although `/api/v1/calendar/range` returned the seeded Event. The previous fixture put all thirty daily recurring Events on the same day, on top of its seeded Log entries. I changed the fixture to create thirty one-time Events across the month, with one at 15:00 on the anchor day. The Calendar Tab can now show real fixture content in the initial Week view without hiding it behind overflow badges. Syntax and whitespace checks pass; retrying.
Author
Owner

The second readiness screenshot shows the desktop Week view’s Calendar grid with its date headers and overflow counts, but the event title is not in the rendered text even though the API projection contains the anchor Event. The selected Calendar Tab and its visible date header are the stable screen state for this view. I changed the per-switch Calendar marker to the Wed 7 header and kept a separate pre-measurement API assertion for Tab storm Event 01, so fixture presence and visible selection are both checked without depending on an overflow-hidden title. Syntax and whitespace checks pass; retrying.

The second readiness screenshot shows the desktop Week view’s Calendar grid with its date headers and overflow counts, but the event title is not in the rendered text even though the API projection contains the anchor Event. The selected Calendar Tab and its visible date header are the stable screen state for this view. I changed the per-switch Calendar marker to the `Wed 7` header and kept a separate pre-measurement API assertion for `Tab storm Event 01`, so fixture presence and visible selection are both checked without depending on an overflow-hidden title. Syntax and whitespace checks pass; retrying.
Author
Owner

After all fixture data passed readiness, the direct [role=tablist][aria-label="App modes"] scan in assertTabAvailability returned no Calendar Tab. The profile already has a shared accessible tabLocator that waits for each named Tab to attach and scrolls it into view. I reused that helper and now assert each Tab is not disabled. The profile parses and git diff --check passes; retrying the local storm.

After all fixture data passed readiness, the direct `[role=tablist][aria-label="App modes"]` scan in `assertTabAvailability` returned no Calendar Tab. The profile already has a shared accessible `tabLocator` that waits for each named Tab to attach and scrolls it into view. I reused that helper and now assert each Tab is not disabled. The profile parses and `git diff --check` passes; retrying the local storm.
Author
Owner

The fixture was ready, and all Tabs were present, but discoverModeShortcuts recorded only Calendar and Tasks because it pressed each Meta+Alt+number chord and read aria-selected immediately. Svelte had not applied the selection before the next key. The profile now reads the actual Tab at each shortcut position and waits up to three seconds for that Tab to become selected before recording the shortcut. Syntax and whitespace checks pass; retrying.

The fixture was ready, and all Tabs were present, but `discoverModeShortcuts` recorded only Calendar and Tasks because it pressed each Meta+Alt+number chord and read `aria-selected` immediately. Svelte had not applied the selection before the next key. The profile now reads the actual Tab at each shortcut position and waits up to three seconds for that Tab to become selected before recording the shortcut. Syntax and whitespace checks pass; retrying.
Author
Owner

Waiting for the expected Tab selection did not make mode shortcut discovery pass: one chord failed to select the Tab at that same DOM index within three seconds. The fresh discovery page had not explicitly waited for the full accessible Tab list. It now runs the existing availability check before learning shortcuts and reports the failed chord, selected Tab, and actual Tab order if an index still differs. The profile parses and git diff --check passes; retrying for the exact order.

Waiting for the expected Tab selection did not make mode shortcut discovery pass: one chord failed to select the Tab at that same DOM index within three seconds. The fresh discovery page had not explicitly waited for the full accessible Tab list. It now runs the existing availability check before learning shortcuts and reports the failed chord, selected Tab, and actual Tab order if an index still differs. The profile parses and `git diff --check` passes; retrying for the exact order.
Author
Owner

Mode discovery now waits for the accessible Tab list, but Meta+Alt+3 still did not select the third Tab within three seconds (actual order: Calendar, Tasks, Notes, Files, Photos, Mail, Money, Ask; selected stayed Tasks). I added a capture-phase key-event record for failed discovery so the next attempt identifies the delivered key/code/modifier flags. The change is isolated to the temporary profile page and the profile syntax/whitespace checks pass.

Mode discovery now waits for the accessible Tab list, but Meta+Alt+3 still did not select the third Tab within three seconds (actual order: Calendar, Tasks, Notes, Files, Photos, Mail, Money, Ask; selected stayed Tasks). I added a capture-phase key-event record for failed discovery so the next attempt identifies the delivered key/code/modifier flags. The change is isolated to the temporary profile page and the profile syntax/whitespace checks pass.
Author
Owner

The capture-phase log confirms the Mac-emulated chords arrive as Meta+Alt+DigitN. On the latest attempt, shortcut 3 selected Notes, but the next shortcut (4) had not changed the selected Tab from Notes within three seconds; the profile now waits up to 15 seconds and confirms each shortcut destination’s real content before sending the next chord. Syntax and whitespace checks pass; retrying.

The capture-phase log confirms the Mac-emulated chords arrive as Meta+Alt+DigitN. On the latest attempt, shortcut 3 selected Notes, but the next shortcut (4) had not changed the selected Tab from Notes within three seconds; the profile now waits up to 15 seconds and confirms each shortcut destination’s real content before sending the next chord. Syntax and whitespace checks pass; retrying.
Author
Owner

After fixture readiness and shortcut discovery passed, visitAllTargets timed out waiting for the two-frame paint signal during warm-up. This path is outside measured samples, so its wait is now 60 seconds; if it still fails, the report includes the target, selected Tab, bounded route text, marker state and an ignored screenshot. Profile syntax and whitespace checks pass; retrying.

After fixture readiness and shortcut discovery passed, `visitAllTargets` timed out waiting for the two-frame paint signal during warm-up. This path is outside measured samples, so its wait is now 60 seconds; if it still fails, the report includes the target, selected Tab, bounded route text, marker state and an ignored screenshot. Profile syntax and whitespace checks pass; retrying.
Author
Owner

The warm-up reached the real Settings sheet at /settings/tabs/calendar, but its readiness check inspected #route-content behind the sheet and waited for the Account-only Profile marker. The screenshot showed the Settings sheet’s Calendar section with “Dragging Items”. I aligned the Settings fixture route and marker with that actual surface, and changed all Settings readiness checks to read .surface[aria-label="Settings"]. Syntax and whitespace checks pass; retrying.

The warm-up reached the real Settings sheet at `/settings/tabs/calendar`, but its readiness check inspected `#route-content` behind the sheet and waited for the Account-only `Profile` marker. The screenshot showed the Settings sheet’s Calendar section with “Dragging Items”. I aligned the Settings fixture route and marker with that actual surface, and changed all Settings readiness checks to read `.surface[aria-label="Settings"]`. Syntax and whitespace checks pass; retrying.
Author
Owner

Settings readiness now passes, but warm-up then failed opening Search: it sent Meta+K while the Settings sheet was still open, so Search did not appear. dispatchTarget now dismisses the Settings sheet with Escape and waits for it to close before opening Search. The Search surface wait is 15 seconds on the loaded host. Syntax and whitespace checks pass; retrying.

Settings readiness now passes, but warm-up then failed opening Search: it sent Meta+K while the Settings sheet was still open, so Search did not appear. `dispatchTarget` now dismisses the Settings sheet with Escape and waits for it to close before opening Search. The Search surface wait is 15 seconds on the loaded host. Syntax and whitespace checks pass; retrying.
Author
Owner

Closing Settings allowed ⌘K to open Search. The profile then failed because .search-window matches both the accessible Search dialog and its aria-hidden visual backing. I replaced Playwright interactions and visibility checks with getByRole('dialog', { name: 'Search' }), while keeping DOM-side paint checks on the dialog class. node --check and git diff --check pass; retrying.

Closing Settings allowed ⌘K to open Search. The profile then failed because `.search-window` matches both the accessible Search dialog and its aria-hidden visual backing. I replaced Playwright interactions and visibility checks with `getByRole('dialog', { name: 'Search' })`, while keeping DOM-side paint checks on the dialog class. `node --check` and `git diff --check` pass; retrying.
Author
Owner

The Search dialog opened correctly, but its accessible searchbox did not mount within Playwright’s default ten-second fill timeout on this loaded host. openSearch now waits up to 20 seconds for the visible searchbox before entering the real query. Syntax and whitespace checks pass; retrying.

The Search dialog opened correctly, but its accessible searchbox did not mount within Playwright’s default ten-second `fill` timeout on this loaded host. `openSearch` now waits up to 20 seconds for the visible searchbox before entering the real query. Syntax and whitespace checks pass; retrying.
Author
Owner

Search opens; inspection of SearchField.svelte showed the input uses type="search" with role="combobox" and accessible name “Search calternal”, so Playwright’s searchbox role does not match it. I changed openSearch to locate the real combobox by role and name. Syntax and whitespace checks pass; retrying.

Search opens; inspection of `SearchField.svelte` showed the input uses `type="search"` with `role="combobox"` and accessible name “Search calternal”, so Playwright’s `searchbox` role does not match it. I changed `openSearch` to locate the real combobox by role and name. Syntax and whitespace checks pass; retrying.
Author
Owner

The run passed fixture setup and the complete all-Tab warm-up, then failed before its first sample with ReferenceError: page is not defined in cdpMetrics. The function calls page.evaluate to drain the input and long-task observers but did not receive the Playwright Page. I added page to its signature and all three call sites. Syntax and whitespace checks pass; retrying the three-storm run.

The run passed fixture setup and the complete all-Tab warm-up, then failed before its first sample with `ReferenceError: page is not defined` in `cdpMetrics`. The function calls `page.evaluate` to drain the input and long-task observers but did not receive the Playwright Page. I added `page` to its signature and all three call sites. Syntax and whitespace checks pass; retrying the three-storm run.
Author
Owner

Local run finding (2026-10-07): after warm-up, the first measured storm could not find the Money Tab because the Search or Settings overlay from the previous target remained open and made the Tab Bar unavailable. The failure reproduced in dispatchTarget at the Money getByRole("tab") lookup after all warm-up targets completed. I updated the profile to dismiss and wait for both overlays before dispatching another target. node --check bench/tab-storm.mjs passes; rerunning the full local profile now.

Local run finding (2026-10-07): after warm-up, the first measured storm could not find the Money Tab because the Search or Settings overlay from the previous target remained open and made the Tab Bar unavailable. The failure reproduced in `dispatchTarget` at the Money `getByRole("tab")` lookup after all warm-up targets completed. I updated the profile to dismiss and wait for both overlays before dispatching another target. `node --check bench/tab-storm.mjs` passes; rerunning the full local profile now.
Author
Owner

Second local attempt (2026-10-07): fixture setup and Photos readiness passed, and the profile entered storm 1. The storm then timed out while Playwright requested a bounding box for the Money Tab (bench/tab-storm.mjs:1011, from runOneStorm). This is a second occurrence after the Search/Settings overlay cleanup, so the profile now captures the Tab Bar DOM geometry, selected/disabled/hidden state and a screenshot when a pointer target is missing. node --check and git diff --check pass. Retrying locally with that evidence enabled.

Second local attempt (2026-10-07): fixture setup and Photos readiness passed, and the profile entered storm 1. The storm then timed out while Playwright requested a bounding box for the Money Tab (`bench/tab-storm.mjs:1011`, from `runOneStorm`). This is a second occurrence after the Search/Settings overlay cleanup, so the profile now captures the Tab Bar DOM geometry, selected/disabled/hidden state and a screenshot when a pointer target is missing. `node --check` and `git diff --check` pass. Retrying locally with that evidence enabled.
Author
Owner

Third local attempt (2026-10-07, host load 35.29/38.17/38.38) passed fixture setup and Photos Index readiness, then failed when the Money Tab could not be resolved during storm 1. Captured DOM state shows data-capsule-state="tabs", all eight Tab buttons mounted, but the Tab Bar is under an aria-hidden ancestor. The screenshot artifacts/tabstorm-1338/money-tab-lookup-failure.png shows the real Journal composer open over Calendar, so the Tab Bar is correctly covered; Search and Settings are closed. I extended the profile to record the last keyboard events and to dismiss and report an unexpected active modal before dispatching the next Tab. node --check and git diff --check pass.

Third local attempt (2026-10-07, host load 35.29/38.17/38.38) passed fixture setup and Photos Index readiness, then failed when the Money Tab could not be resolved during storm 1. Captured DOM state shows `data-capsule-state="tabs"`, all eight Tab buttons mounted, but the Tab Bar is under an `aria-hidden` ancestor. The screenshot `artifacts/tabstorm-1338/money-tab-lookup-failure.png` shows the real Journal composer open over Calendar, so the Tab Bar is correctly covered; Search and Settings are closed. I extended the profile to record the last keyboard events and to dismiss and report an unexpected active modal before dispatching the next Tab. `node --check` and `git diff --check` pass.
Author
Owner

Fourth local attempt (2026-10-07, load 31.66/35.17/37.15) made progress past the first hidden Tab Bar by detecting and closing the Composer dialog. It then failed at Photos with path=/settings/tabs/calendar, all eight Tabs mounted but under an aria-hidden ancestor, and the Settings surface still visible; screenshot: artifacts/tabstorm-1338/photos-tab-lookup-failure.png. This confirms that the profile can encounter stacked Composer and Settings surfaces. The modal cleanup now closes active surfaces from topmost to bottommost and leaves only the requested Search/Settings surface open. openSettings also checks for an already-open surface before toggling.

Fourth local attempt (2026-10-07, load 31.66/35.17/37.15) made progress past the first hidden Tab Bar by detecting and closing the Composer dialog. It then failed at Photos with `path=/settings/tabs/calendar`, all eight Tabs mounted but under an `aria-hidden` ancestor, and the Settings surface still visible; screenshot: `artifacts/tabstorm-1338/photos-tab-lookup-failure.png`. This confirms that the profile can encounter stacked Composer and Settings surfaces. The modal cleanup now closes active surfaces from topmost to bottommost and leaves only the requested Search/Settings surface open. `openSettings` also checks for an already-open surface before toggling.
Author
Owner

Fifth local attempt (2026-10-07, load 52.53/45.64/40.96) passed Photos Index readiness, then timed out in shortcut destination verification: the selected Tab did not paint its seeded content within 15 seconds. The failure occurred in discoverModeShortcuts after the selected-mode assertion, so this is distinct from the hidden-Tab overlay failures. I raised this content wait to 30 seconds for the loaded local host and added selected mode, URL, route text, modal state and screenshot evidence on failure.

Fifth local attempt (2026-10-07, load 52.53/45.64/40.96) passed Photos Index readiness, then timed out in shortcut destination verification: the selected Tab did not paint its seeded content within 15 seconds. The failure occurred in `discoverModeShortcuts` after the selected-mode assertion, so this is distinct from the hidden-Tab overlay failures. I raised this content wait to 30 seconds for the loaded local host and added selected mode, URL, route text, modal state and screenshot evidence on failure.
Author
Owner

Sixth local attempt (2026-10-07, load 34.27/39.31/40.05) passed fixture readiness, closed the unexpected Composer and progressed to Photos. It then timed out in Playwright scrollIntoViewIfNeeded: the Tab did not become stable while the shared Tab Bar morph was running. This is a profile-side animation stability wait before input dispatch. I replaced it with one instant DOM scroll so the benchmark does not wait for motion. No product code changed.

Sixth local attempt (2026-10-07, load 34.27/39.31/40.05) passed fixture readiness, closed the unexpected Composer and progressed to Photos. It then timed out in Playwright `scrollIntoViewIfNeeded`: the Tab did not become stable while the shared Tab Bar morph was running. This is a profile-side animation stability wait before input dispatch. I replaced it with one instant DOM scroll so the benchmark does not wait for motion. No product code changed.
Author
Owner

Local memory finding for #1338 (2026-10-07): the forced-GC heap grew from 27,084,472 bytes after storm 1 to 33,130,784 bytes after storm 3 (+22.3%); DOM nodes grew from 12,344 to 26,205 (+112.3%). The event-listener count fell from 1,087 to 970 (-10.8%), so this is not an accumulating listener count.

The after-storm-1 and after-storm-3 snapshots show retained, detached Calendar page trees increasing from 2 #calendar-panel nodes to 10. The strong retaining path reaches the Calendar route component scope in _app/immutable/nodes/16.BctjSTmS.js, then its .agenda DOM. A retained CalendarDay.items array contains 1,133 rows through the component's displayAgenda derived value at apps/web/src/routes/calendar/[view]/[date]/+page.svelte:344. The path runs through the SvelteKit client page-data reaction. Heap snapshots and the diagnostic trace are in ignored artifacts/tabstorm-1338/local-native-after-storm-{1,3}.heapsnapshot and local-native-budget-failure-trace.json.

This local host was heavily loaded, so I will confirm the memory and DOM result on the perf VM before treating its size as a product regression. The VM run is queued behind the current lock holder. The trace also shows Chrome's Network.enable inspector retaining 10,016 NetworkResourcesData::ResourceData entries (+2.88 MB), which is profile instrumentation overhead and not an app retention issue.

Local memory finding for #1338 (2026-10-07): the forced-GC heap grew from 27,084,472 bytes after storm 1 to 33,130,784 bytes after storm 3 (+22.3%); DOM nodes grew from 12,344 to 26,205 (+112.3%). The event-listener count fell from 1,087 to 970 (-10.8%), so this is not an accumulating listener count. The after-storm-1 and after-storm-3 snapshots show retained, detached Calendar page trees increasing from 2 `#calendar-panel` nodes to 10. The strong retaining path reaches the Calendar route component scope in `_app/immutable/nodes/16.BctjSTmS.js`, then its `.agenda` DOM. A retained `CalendarDay.items` array contains 1,133 rows through the component's `displayAgenda` derived value at `apps/web/src/routes/calendar/[view]/[date]/+page.svelte:344`. The path runs through the SvelteKit client page-data reaction. Heap snapshots and the diagnostic trace are in ignored `artifacts/tabstorm-1338/local-native-after-storm-{1,3}.heapsnapshot` and `local-native-budget-failure-trace.json`. This local host was heavily loaded, so I will confirm the memory and DOM result on the perf VM before treating its size as a product regression. The VM run is queued behind the current lock holder. The trace also shows Chrome's `Network.enable` inspector retaining 10,016 `NetworkResourcesData::ResourceData` entries (+2.88 MB), which is profile instrumentation overhead and not an app retention issue.
Author
Owner

Filed #1356: Tabs: release detached Calendar page views after Tab switches. The local forced-GC snapshots show detached Calendar panels increasing from 2 after storm 1 to 10 after storm 3, with the retained route context reaching Calendar displayAgenda and its CalendarDay.items rows. I will append the perf-VM confirmation and measurements after the queued run.

Filed [#1356: Tabs: release detached Calendar page views after Tab switches](https://git.kayg.org/kayg/calternal/issues/1356). The local forced-GC snapshots show detached Calendar panels increasing from 2 after storm 1 to 10 after storm 3, with the retained route context reaching Calendar `displayAgenda` and its `CalendarDay.items` rows. I will append the perf-VM confirmation and measurements after the queued run.
Author
Owner

Local request finding for #1338: each storm left requests from a previously selected Tab in flight (max 20, 7 and 3). Per-view peak concurrency was Files 30, Photos 19/19/20, Calendar up to 14, and Tasks up to 13; those views also had few or no aborted requests. The ledger includes all HTTP resources except WebSockets, including image subresources. The Photos and Files counts therefore include thumbnail image loads from PhotoTimeline.svelte's lazy <img> and the shared packages/ui/src/components/files/FileThumb.svelte, as well as API reads.

The post-storm diagnostic trace ended with a pending /api/v1/notes/tasks read. The source path is TaskIndex.#read in apps/web/src/lib/tasks/index.svelte.ts → listAllTasks in apps/web/src/lib/tasks/api.ts; that read has no AbortSignal. This is a concrete request source, but the diagnostic trace includes extra view switches after the measured storms. I will use the perf-VM result to confirm which requests need their own follow-up.

Local request finding for #1338: each storm left requests from a previously selected Tab in flight (max 20, 7 and 3). Per-view peak concurrency was Files 30, Photos 19/19/20, Calendar up to 14, and Tasks up to 13; those views also had few or no aborted requests. The ledger includes all HTTP resources except WebSockets, including image subresources. The Photos and Files counts therefore include thumbnail image loads from `PhotoTimeline.svelte`'s lazy `<img>` and the shared `packages/ui/src/components/files/FileThumb.svelte`, as well as API reads. The post-storm diagnostic trace ended with a pending `/api/v1/notes/tasks` read. The source path is `TaskIndex.#read` in `apps/web/src/lib/tasks/index.svelte.ts` → `listAllTasks` in `apps/web/src/lib/tasks/api.ts`; that read has no `AbortSignal`. This is a concrete request source, but the diagnostic trace includes extra view switches after the measured storms. I will use the perf-VM result to confirm which requests need their own follow-up.
Author
Owner

Tab storm results

Photos fixture readiness is fixed: Files adopted all 500 seeded Photos, Photos Index became ready and the real timeline content was visible before each storm. The production build completed three 120-second storms on one macOS-emulated Chromium page with 4× CPU throttle and three without. Each storm included the phone touch phase.

Perf VM: forced-GC memory and server RSS

Metric CPU4 storm 1 CPU4 storm 2 CPU4 storm 3 S1→S3 Native storm 1 Native storm 2 Native storm 3 S1→S3
V8 heap 18.03 MB 24.45 MB 24.86 MB +37.9% 23.35 MB 26.34 MB 36.00 MB +54.2%
Renderer RSS 667.9 MB 877.0 MB 971.4 MB +45.4% 918.4 MB 1,247.8 MB 1,599.3 MB +74.1%
DOM nodes 6,894 8,300 10,037 +45.6% 8,457 12,098 14,933 +76.6%
Event listeners 751 955 890 +18.5% 1,003 1,904 1,919 +91.3%
Server RSS after 1 s 309.7 MB 372.6 MB 386.2 MB +24.7% 540.5 MB 584.1 MB 604.9 MB +11.9%

Both conditions exceeded the ±10% V8 heap and renderer RSS budgets. Main thread did not return to idle within 1 second after any storm.

Perf VM: requests by selected Tab

Each cell is total / aborted / max in-flight across all three storms. This includes API, image and other non-WebSocket requests.

Tab CPU4 Native
Calendar 211 / 2 / 13 321 / 1 / 14
Tasks 249 / 1 / 4 520 / 1 / 8
Notes 528 / 4 / 5 757 / 1 / 5
Files 735 / 2 / 25 1,073 / 5 / 25
Photos 1,373 / 3 / 20 1,044 / 1 / 25
Mail 758 / 7 / 9 796 / 25 / 8
Money 815 / 1 / 12 832 / 0 / 5
Settings 307 / 4 / 9 284 / 1 / 5
Search 330 / 1 / 4 490 / 3 / 1

Maximum requests still in flight from a Tab after leaving it were 24/25/20 across CPU4 storms and 25/6/3 across native storms.

Perf VM: switching and CPU

Condition Final Tab and input→second paint Correct target by 1 s Main-thread busy p50/p95/max Long-task duration p50/p95/max Switch long-task max p50/p95/max
CPU4 storm 1 Photos, 1,212.9 ms Yes 86.79% / 100% / 100% 140 / 844 / 1,720 ms 0 / 405 / 899 ms
CPU4 storm 2 Files, 719.8 ms Yes 86.79% / 100% / 100% 140 / 844 / 1,720 ms 0 / 405 / 899 ms
CPU4 storm 3 Files, 1,057.3 ms Yes 86.79% / 100% / 100% 140 / 844 / 1,720 ms 0 / 405 / 899 ms
Native storm 1 Tasks, 109.6 ms Yes 29.47% / 100% / 100% 51 / 171 / 323 ms 0 / 111 / 313 ms
Native storm 2 Tasks, 213.8 ms Yes 29.47% / 100% / 100% 51 / 171 / 323 ms 0 / 111 / 313 ms
Native storm 3 Notes, 184.5 ms Yes 29.47% / 100% / 100% 51 / 171 / 323 ms 0 / 111 / 313 ms

The strict 100 ms content assertion failed after all six storms; the correct Tab marker appeared by the next one-second sample. No wrong-Tab content was observed after target paint. The supplementary warm sweep measured only Calendar→Tasks and Calendar→Notes (two samples each), then timed out after 15 seconds at Calendar→Files. Warm p50/p95 were 1,373.6/1,995 ms under CPU4 and 323.1/389.2 ms native; the pair sweep is incomplete.

The CPU4 perf-run lock load at start was 0.03 1.18 2.60, sampled one-minute load p50/p95/max 5.75/6.50/6.79. Native start load was 6.04 5.28 4.08, sampled p50/p95/max 5.10/5.71/5.82. Native started on a loaded host.

Local run

The local 3×120-second no-throttle run had startup load 37.00 / 37.06 / 38.94 and one-minute load p50/p95/max 44.34 / 50.23 / 50.92. CPU and long-task values are indicative only. Forced-GC heap grew 22.3% from storm 1 to storm 3; renderer RSS grew 58.8%, DOM nodes 112.3%, listeners fell 10.8%, and server RSS grew 24.1%. All three targets were visible within one second. Storms 2 and 3 missed the 100 ms content assertion.

Findings and follow-ups

  • Local Calendar snapshot path: SvelteKit page-data reaction → Calendar route scope → #calendar-panel → .agenda → CalendarDay.items via displayAgenda in apps/web/src/routes/calendar/[view]/[date]/+page.svelte:344. The VM showed 0→1 detached Calendar panels, a smaller increase than local 2→10. #1356
  • Detached Mail roots grew 5→15 with CPU4 and 9→28 native. Retainer: (GC roots) → Global handles → NativeContext → Window.fetch → module context → SourceTextModule → Cell → #s.reactions[] → reaction.fn → closure context → detached Mail <main>. Source component is apps/web/src/lib/mail/MailView.svelte; production V8 does not name the generated callback. #1375
  • The CPU4 trace sent 131 GET /api/v1/notes/tasks?limit=100&cursor=0 requests in its 15.8-second diagnostic interval. Audit TaskIndex.#watch() / TaskIndex.#read() in apps/web/src/lib/tasks/index.svelte.ts and listAllTasks() in apps/web/src/lib/tasks/api.ts. #1376
  • The CPU4 trace showed 24 /api/v1/files/thumb/:hash image requests. Sources are FileThumb in packages/ui/src/components/files/FileThumb.svelte and tile <img> in apps/web/src/lib/photos/PhotoTimeline.svelte. They had completed by trace end. #1377
  • Server RSS rose monotonically, but the browser trace does not identify a Rust allocation retainer. #1378
  • Four randomized key trails opened unexpected Composer dialogs (two per condition). The harness dismissed them; the profile cannot prove whether a create request followed. #1380

VM snapshots and traces remain in ignored artifacts/tabstorm-1338/; they are not committed. Raw CSV rows and the summary JSON are committed under docs/perf/runs/2026-10-07-1338-tabstorm*.

## Tab storm results Photos fixture readiness is fixed: Files adopted all 500 seeded Photos, Photos Index became ready and the real timeline content was visible before each storm. The production build completed three 120-second storms on one macOS-emulated Chromium page with 4× CPU throttle and three without. Each storm included the phone touch phase. ### Perf VM: forced-GC memory and server RSS | Metric | CPU4 storm 1 | CPU4 storm 2 | CPU4 storm 3 | S1→S3 | Native storm 1 | Native storm 2 | Native storm 3 | S1→S3 | | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | | V8 heap | 18.03 MB | 24.45 MB | 24.86 MB | +37.9% | 23.35 MB | 26.34 MB | 36.00 MB | +54.2% | | Renderer RSS | 667.9 MB | 877.0 MB | 971.4 MB | +45.4% | 918.4 MB | 1,247.8 MB | 1,599.3 MB | +74.1% | | DOM nodes | 6,894 | 8,300 | 10,037 | +45.6% | 8,457 | 12,098 | 14,933 | +76.6% | | Event listeners | 751 | 955 | 890 | +18.5% | 1,003 | 1,904 | 1,919 | +91.3% | | Server RSS after 1 s | 309.7 MB | 372.6 MB | 386.2 MB | +24.7% | 540.5 MB | 584.1 MB | 604.9 MB | +11.9% | Both conditions exceeded the ±10% V8 heap and renderer RSS budgets. Main thread did not return to idle within 1 second after any storm. ### Perf VM: requests by selected Tab Each cell is total / aborted / max in-flight across all three storms. This includes API, image and other non-WebSocket requests. | Tab | CPU4 | Native | | --- | ---: | ---: | | Calendar | 211 / 2 / 13 | 321 / 1 / 14 | | Tasks | 249 / 1 / 4 | 520 / 1 / 8 | | Notes | 528 / 4 / 5 | 757 / 1 / 5 | | Files | 735 / 2 / 25 | 1,073 / 5 / 25 | | Photos | 1,373 / 3 / 20 | 1,044 / 1 / 25 | | Mail | 758 / 7 / 9 | 796 / 25 / 8 | | Money | 815 / 1 / 12 | 832 / 0 / 5 | | Settings | 307 / 4 / 9 | 284 / 1 / 5 | | Search | 330 / 1 / 4 | 490 / 3 / 1 | Maximum requests still in flight from a Tab after leaving it were 24/25/20 across CPU4 storms and 25/6/3 across native storms. ### Perf VM: switching and CPU | Condition | Final Tab and input→second paint | Correct target by 1 s | Main-thread busy p50/p95/max | Long-task duration p50/p95/max | Switch long-task max p50/p95/max | | --- | --- | --- | --- | --- | --- | | CPU4 storm 1 | Photos, 1,212.9 ms | Yes | 86.79% / 100% / 100% | 140 / 844 / 1,720 ms | 0 / 405 / 899 ms | | CPU4 storm 2 | Files, 719.8 ms | Yes | 86.79% / 100% / 100% | 140 / 844 / 1,720 ms | 0 / 405 / 899 ms | | CPU4 storm 3 | Files, 1,057.3 ms | Yes | 86.79% / 100% / 100% | 140 / 844 / 1,720 ms | 0 / 405 / 899 ms | | Native storm 1 | Tasks, 109.6 ms | Yes | 29.47% / 100% / 100% | 51 / 171 / 323 ms | 0 / 111 / 313 ms | | Native storm 2 | Tasks, 213.8 ms | Yes | 29.47% / 100% / 100% | 51 / 171 / 323 ms | 0 / 111 / 313 ms | | Native storm 3 | Notes, 184.5 ms | Yes | 29.47% / 100% / 100% | 51 / 171 / 323 ms | 0 / 111 / 313 ms | The strict 100 ms content assertion failed after all six storms; the correct Tab marker appeared by the next one-second sample. No wrong-Tab content was observed after target paint. The supplementary warm sweep measured only Calendar→Tasks and Calendar→Notes (two samples each), then timed out after 15 seconds at Calendar→Files. Warm p50/p95 were 1,373.6/1,995 ms under CPU4 and 323.1/389.2 ms native; the pair sweep is incomplete. The CPU4 `perf-run` lock load at start was `0.03 1.18 2.60`, sampled one-minute load p50/p95/max 5.75/6.50/6.79. Native start load was `6.04 5.28 4.08`, sampled p50/p95/max 5.10/5.71/5.82. Native started on a loaded host. ### Local run The local 3×120-second no-throttle run had startup load `37.00 / 37.06 / 38.94` and one-minute load p50/p95/max `44.34 / 50.23 / 50.92`. CPU and long-task values are indicative only. Forced-GC heap grew 22.3% from storm 1 to storm 3; renderer RSS grew 58.8%, DOM nodes 112.3%, listeners fell 10.8%, and server RSS grew 24.1%. All three targets were visible within one second. Storms 2 and 3 missed the 100 ms content assertion. ### Findings and follow-ups - Local Calendar snapshot path: SvelteKit page-data reaction → Calendar route scope → `#calendar-panel` → `.agenda` → `CalendarDay.items` via `displayAgenda` in `apps/web/src/routes/calendar/[view]/[date]/+page.svelte:344`. The VM showed 0→1 detached Calendar panels, a smaller increase than local 2→10. [#1356](https://git.kayg.org/kayg/calternal/issues/1356) - Detached Mail roots grew 5→15 with CPU4 and 9→28 native. Retainer: `(GC roots) → Global handles → NativeContext → Window.fetch → module context → SourceTextModule → Cell → #s.reactions[] → reaction.fn → closure context → detached Mail <main>`. Source component is `apps/web/src/lib/mail/MailView.svelte`; production V8 does not name the generated callback. [#1375](https://git.kayg.org/kayg/calternal/issues/1375) - The CPU4 trace sent 131 `GET /api/v1/notes/tasks?limit=100&cursor=0` requests in its 15.8-second diagnostic interval. Audit `TaskIndex.#watch()` / `TaskIndex.#read()` in `apps/web/src/lib/tasks/index.svelte.ts` and `listAllTasks()` in `apps/web/src/lib/tasks/api.ts`. [#1376](https://git.kayg.org/kayg/calternal/issues/1376) - The CPU4 trace showed 24 `/api/v1/files/thumb/:hash` image requests. Sources are `FileThumb` in `packages/ui/src/components/files/FileThumb.svelte` and tile `<img>` in `apps/web/src/lib/photos/PhotoTimeline.svelte`. They had completed by trace end. [#1377](https://git.kayg.org/kayg/calternal/issues/1377) - Server RSS rose monotonically, but the browser trace does not identify a Rust allocation retainer. [#1378](https://git.kayg.org/kayg/calternal/issues/1378) - Four randomized key trails opened unexpected Composer dialogs (two per condition). The harness dismissed them; the profile cannot prove whether a create request followed. [#1380](https://git.kayg.org/kayg/calternal/issues/1380) VM snapshots and traces remain in ignored `artifacts/tabstorm-1338/`; they are not committed. Raw CSV rows and the summary JSON are committed under `docs/perf/runs/2026-10-07-1338-tabstorm*`.
Author
Owner

Correction to the CPU table in my previous comment: its repeated CPU and long-task p50/p95/max values are condition-level summaries across 359 one-second samples, not separate per-storm measurements. Read them once per condition:

Condition Renderer CPU p50/p95/max Long-task duration p50/p95/max Switch long-task max p50/p95/max Idle within 1 s after storms
CPU4 112.99% / 131.99% / 181.51% 140 / 844 / 1,720 ms 0 / 405 / 899 ms No, all three
Native 37.00% / 87.99% / 153.98% 51 / 171 / 323 ms 0 / 111 / 313 ms No, all three
Correction to the CPU table in my previous comment: its repeated CPU and long-task p50/p95/max values are **condition-level** summaries across 359 one-second samples, not separate per-storm measurements. Read them once per condition: | Condition | Renderer CPU p50/p95/max | Long-task duration p50/p95/max | Switch long-task max p50/p95/max | Idle within 1 s after storms | | --- | --- | --- | --- | --- | | CPU4 | 112.99% / 131.99% / 181.51% | 140 / 844 / 1,720 ms | 0 / 405 / 899 ms | No, all three | | Native | 37.00% / 87.99% / 153.98% | 51 / 171 / 323 ms | 0 / 111 / 313 ms | No, all three |
Author
Owner

Finished

Branch: job/tabstorm-1338
HEAD: 12077fd3db2fe299cd957a8e9a5f0efb71e07915

Photos fixture readiness now waits for Files to adopt the 500 Photos and for the real Photos timeline to become ready. The local 3×120-second run and both perf-VM conditions completed. Result tables and raw CSV/JSON are committed in docs/perf/2026-10-07-1338-tabstorm.md and docs/perf/runs/2026-10-07-1338-tabstorm*. Heap snapshots and Chrome traces remain in ignored artifacts/tabstorm-1338/ and are not committed.

Follow-ups: Calendar retention #1356; Mail retention #1375; repeated Task reads #1376; Files/Photos thumbnail concurrency #1377; server RSS investigation #1378; unexpected Composer dialogs #1380. The tables and evidence are also on #1233.

Gates

bun run check exited 0. Verbatim summary output:

perf-lint: PASS; 0 violations; 22544 scoped exceptions
User browser caches use userStorage; only documented device/public-link exceptions remain.
Glass alpha, blur and backdrop-filter roles use packages/ui/src/tokens.css.
Text sizes and UI shape values use shared role tokens.
Keyboard focus rings use the shared focus tokens.
UI transitions and animation options use shared motion tokens or documented exceptions.
Loading svelte-check in workspace: /home/kayg/Developer/calternal-wt/tabstorm-1338/apps/web
Getting Svelte diagnostics...

/home/kayg/Developer/calternal-wt/tabstorm-1338/apps/web/../../packages/ui/src/components/calendar/AttachmentDeck.svelte:1055:3
Warn: Do not use empty rulesets (css)
  }
  .attachment-row:focus {

svelte-check found 0 errors and 1 warning in 1 file

bun run test exited 1. Verbatim Vitest summary:

Failed Tests 1
FAIL |component| src/lib/components/analytics/widgets/StatRow.svelte.test.ts > StatRow summary cards > toggles all four cards together and remembers the mode
Error: Test timed out in 5000ms.

Test Files  1 failed | 286 passed (287)
Tests       1 failed | 2035 passed (2036)
Duration    1619.03s
error: script "test" exited with code 1

The failed test is in an untouched Analytics component. No existing test expectation was changed. Run cd apps/web && bun run test in the merge round; it must show the 287 files and 2,036 tests passing.

Cleanup completed: removed apps/web/build and apps/web/.svelte-kit; cargo clean reported Removed 9259 files, 3.4GiB total. No push or deploy was made.

## Finished Branch: `job/tabstorm-1338` HEAD: `12077fd3db2fe299cd957a8e9a5f0efb71e07915` Photos fixture readiness now waits for Files to adopt the 500 Photos and for the real Photos timeline to become ready. The local 3×120-second run and both perf-VM conditions completed. Result tables and raw CSV/JSON are committed in `docs/perf/2026-10-07-1338-tabstorm.md` and `docs/perf/runs/2026-10-07-1338-tabstorm*`. Heap snapshots and Chrome traces remain in ignored `artifacts/tabstorm-1338/` and are not committed. Follow-ups: Calendar retention #1356; Mail retention #1375; repeated Task reads #1376; Files/Photos thumbnail concurrency #1377; server RSS investigation #1378; unexpected Composer dialogs #1380. The tables and evidence are also on #1233. ### Gates `bun run check` exited 0. Verbatim summary output: ```text perf-lint: PASS; 0 violations; 22544 scoped exceptions User browser caches use userStorage; only documented device/public-link exceptions remain. Glass alpha, blur and backdrop-filter roles use packages/ui/src/tokens.css. Text sizes and UI shape values use shared role tokens. Keyboard focus rings use the shared focus tokens. UI transitions and animation options use shared motion tokens or documented exceptions. Loading svelte-check in workspace: /home/kayg/Developer/calternal-wt/tabstorm-1338/apps/web Getting Svelte diagnostics... /home/kayg/Developer/calternal-wt/tabstorm-1338/apps/web/../../packages/ui/src/components/calendar/AttachmentDeck.svelte:1055:3 Warn: Do not use empty rulesets (css) } .attachment-row:focus { svelte-check found 0 errors and 1 warning in 1 file ``` `bun run test` exited 1. Verbatim Vitest summary: ```text Failed Tests 1 FAIL |component| src/lib/components/analytics/widgets/StatRow.svelte.test.ts > StatRow summary cards > toggles all four cards together and remembers the mode Error: Test timed out in 5000ms. Test Files 1 failed | 286 passed (287) Tests 1 failed | 2035 passed (2036) Duration 1619.03s error: script "test" exited with code 1 ``` The failed test is in an untouched Analytics component. No existing test expectation was changed. Run `cd apps/web && bun run test` in the merge round; it must show the 287 files and 2,036 tests passing. Cleanup completed: removed `apps/web/build` and `apps/web/.svelte-kit`; `cargo clean` reported `Removed 9259 files, 3.4GiB total`. No push or deploy was made.
Commenting is not possible because the repository is archived.
No labels
No milestone
No project
No assignees
1 participant
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-v0#1338
No description provided.