Calendar horizontal scrolling became slow after merge round 37 #1579

Open
opened 2026-10-11 16:31:41 +00:00 by kayg · 29 comments
Owner

Owner report (2026-10-11): horizontal scrolling in Calendar is very slow on production now; it was fast two days ago.

Production builds since then: merge37 ac7fda1a8 (deployed Oct 10 21:39 IST: ui-calendar, ui-chrome, intlfmt-1461 merges) and merge38 4ebeba0b8 (Oct 11 02:02 IST). Last build before the merge37 UI merges: 1f9500fba.

Suspect (unproven): 4b479c564 "Drop the end time chip in narrow grid lanes" adds container-type: inline-size to .b-time-row in every Event block (GridColumn.svelte ~1439), which makes every block a size container; other merge37 Calendar commits add @container rules (Month cells, ModeHeader).

Done when: the regressing commit is identified by bisection with numbers, the fix restores horizontal scroll to the 1f9500fba level (no visible change to narrow-lane chips), and a bench guard covers horizontal Week/Day scroll.

Owner report (2026-10-11): horizontal scrolling in Calendar is very slow on production now; it was fast two days ago. Production builds since then: merge37 ac7fda1a8 (deployed Oct 10 21:39 IST: ui-calendar, ui-chrome, intlfmt-1461 merges) and merge38 4ebeba0b8 (Oct 11 02:02 IST). Last build before the merge37 UI merges: 1f9500fba. Suspect (unproven): 4b479c564 "Drop the end time chip in narrow grid lanes" adds `container-type: inline-size` to `.b-time-row` in every Event block (GridColumn.svelte ~1439), which makes every block a size container; other merge37 Calendar commits add @container rules (Month cells, ModeHeader). Done when: the regressing commit is identified by bisection with numbers, the fix restores horizontal scroll to the 1f9500fba level (no visible change to narrow-lane chips), and a bench guard covers horizontal Week/Day scroll.
Author
Owner

Started investigation on branch job/hscroll-1579, based at production revision 4ebeba0b8e13b06223bc2b24ee7a398a0933bda0. I will compare it with 1f9500fba, reproduce the scroll regression on the perf VM, bisect with the existing scroll bench, then make and verify the smallest fix.

Started investigation on branch `job/hscroll-1579`, based at production revision `4ebeba0b8e13b06223bc2b24ee7a398a0933bda0`. I will compare it with `1f9500fba`, reproduce the scroll regression on the perf VM, bisect with the existing scroll bench, then make and verify the smallest fix.
Author
Owner

Measurement setup finding (not a scroll result): both attempts crashed Chromium during registerOwner before any scroll sample. The profile used calendar-perf.mjs, which opens /login before passkey setup; the shared harness documents that this starts a conditional passkey request and starts setup at /readyz. The profile now uses the shared authenticator and registerOwner helpers, with a regression guard. No performance conclusion yet.

Measurement setup finding (not a scroll result): both attempts crashed Chromium during `registerOwner` before any scroll sample. The profile used `calendar-perf.mjs`, which opens `/login` before passkey setup; the shared harness documents that this starts a conditional passkey request and starts setup at `/readyz`. The profile now uses the shared `authenticator` and `registerOwner` helpers, with a regression guard. No performance conclusion yet.
Author
Owner

Profiler finding (not the Calendar regression cause yet): the first real Week scroll profile reached all four viewports, then failed while serializing newly_revealed_dates; DOM elements cannot cross Playwright's page.evaluate result boundary. I now copy the dates to strings inside the page and added a guard. Reproduction before the fix: TypeError: undefined is not an object (evaluating 'column.dataset.date') at calendar-scroll-1362.mjs:385. After the fix, node --check apps/web/e2e/calendar-scroll-1362.mjs, the focused 1579 test (6/6), and git diff --check pass. Commit: 538e1ab33.

Profiler finding (not the Calendar regression cause yet): the first real Week scroll profile reached all four viewports, then failed while serializing `newly_revealed_dates`; DOM elements cannot cross Playwright's `page.evaluate` result boundary. I now copy the dates to strings inside the page and added a guard. Reproduction before the fix: `TypeError: undefined is not an object (evaluating 'column.dataset.date')` at calendar-scroll-1362.mjs:385. After the fix, `node --check apps/web/e2e/calendar-scroll-1362.mjs`, the focused 1579 test (6/6), and `git diff --check` pass. Commit: 538e1ab33.
Author
Owner

Measurement finding (first comparison, before the interleaved bisect): at 1440 px in Week, forward trackpad-style wheel, the bad build is 33% slower on p95 frame interval and drops 61% more frames. Three samples per build, 336 real Events over 84 days, 4x CPU throttle, same release server and Chromium build: good 1f9500fba p95 423.28 ms / 152 dropped frames / 354.03 ms style recalc / 374.48 ms layout; bad 4ebeba0b8 p95 564.11 ms / 245 dropped frames / 538.13 ms style recalc / 453.24 ms layout. The 390 px cases did not regress in this first run. I’m bisecting from the first Calendar merge and will verify with interleaved samples.

Measurement finding (first comparison, before the interleaved bisect): at 1440 px in Week, forward trackpad-style wheel, the bad build is 33% slower on p95 frame interval and drops 61% more frames. Three samples per build, 336 real Events over 84 days, 4x CPU throttle, same release server and Chromium build: good 1f9500fba p95 423.28 ms / 152 dropped frames / 354.03 ms style recalc / 374.48 ms layout; bad 4ebeba0b8 p95 564.11 ms / 245 dropped frames / 538.13 ms style recalc / 453.24 ms layout. The 390 px cases did not regress in this first run. I’m bisecting from the first Calendar merge and will verify with interleaved samples.
Author
Owner

Profiler-method finding: the initial trace-all frame comparison is not a reliable reproduction. In the matching untraced 1440 px Week forward-wheel profile (3 samples, 336 Events), good 1f9500fba had median sample p95 743.30 ms and 211 dropped frames; bad 4ebeba0b8 had 530.03 ms and 255 dropped frames (+21% drops, lower p95). Trace-all therefore changed frame timing enough to reverse the p95 result. I will use untraced runs for frame/drop timing and a separate selected trace for style/layout, then check dark, Month and the larger fixture as requested.

Profiler-method finding: the initial trace-all frame comparison is not a reliable reproduction. In the matching untraced 1440 px Week forward-wheel profile (3 samples, 336 Events), good 1f9500fba had median sample p95 743.30 ms and 211 dropped frames; bad 4ebeba0b8 had 530.03 ms and 255 dropped frames (+21% drops, lower p95). Trace-all therefore changed frame timing enough to reverse the p95 result. I will use untraced runs for frame/drop timing and a separate selected trace for style/layout, then check dark, Month and the larger fixture as requested.
Author
Owner

#1579 final report

Branch/head: job/hscroll-1579 at 033229513396193b61cf289cb7b8ae3a87f9475b (base 4ebeba0b8). No push, merge or deploy.

Built

  • Removed container-type: inline-size from each Event time row. The end time chip now hides through the existing .block.cramped placement class, so cramped lanes do not overlap. The Event button aria-label keeps the complete interval.
  • Added the #1579 scroll profile, real CalDAV Event fixture, paint/frame/style/layout/long-task metrics, touch fallback inputs, screenshot captures and a regression guard against per-row size containers. Added an option to skip the unrelated Settings visit during profile setup.
  • Refreshed exact perf source pins; did not change contracts/perf/ratchet.json.

Performance

Measured on perf-test-69-63 with Chrome 153, the shared release server, 4x CPU throttle, and an 84-day fixture (336 real Events, 84 Tasks, 84 Logs). Five samples per build were run sequentially, not interleaved. Rightward 1440 px Week wheel medians:

Build frame p50 frame p95 dropped frames long tasks / total
good 1f9500fba 149.9 ms 676.72 ms 239 12 / 2,977 ms
bad 4ebeba0b8 133.3 ms 516.60 ms 218 12 / 2,323 ms
fix 3dbe81fa7 150.0 ms 568.35 ms 245 10 / 2,615 ms

The fix is within 10% of good on p95 and dropped frames. The bad build was faster in this five-sample run, so the owner-reported regression was not reproduced consistently. The first traced-all run overstated the regression; ordinary matched runs, dark mode, Month and the 1,680-Event fallback did not reach the 25% threshold. The larger one-sample Week run was +20.8% p95 and +23.0% dropped frames for bad vs good. I did not bisect because the reproduction gate was not met.

Newly revealed Events painted in 0 ms p95 in the rightward Week samples. A selected trace reported style/layout time for 1440 px Day and Week on the fix: Day 182.72/95.16 ms; Week 559.63/568.99 ms. Single selected traces were noisy (good Week 344.53/458.18 ms; bad Week 408.02/529.24 ms), so they do not establish the cause.

Files

packages/ui/src/components/calendar/GridColumn.svelte; apps/web/e2e/calendar-scroll-1362.mjs; apps/web/e2e/calendar-scroll-1579.test.mjs; apps/web/e2e/harness.mjs; bench/calendar-scroll-1579.mjs; contracts/perf/adoption-1058.json; contracts/perf/exceptions.json.

Gates

bun run check:

perf-lint: PASS; 0 violations; 22128 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.
Focus policy passes: 48 focus ring rules, 132 DOM focus calls and 2 data-input writers audited.
UI transitions and animation options use shared motion tokens or documented exceptions.
Loading svelte-check in workspace: /home/kayg/Developer/calternal-wt/hscroll-1579/apps/web
Getting Svelte diagnostics...
svelte-check found 0 errors and 0 warnings
exit=0

Focused Vitest: Test Files 2 passed (2); Tests 23 passed (23). Focused Node profile tests: # tests 9, # pass 9, # fail 0. node --check bench/calendar-scroll-1579.mjs and git diff --check passed. cargo clean removed 1 file (356 B); web build output was removed.

UX gaps and known limits

  • Closed: cramped lane time chips no longer overlap; the full interval remains in the accessible name.
  • Left: before/after screenshots cover Week at 390, 820 and 1440 px in light/dark, with real data and macOS platform emulation. Day screenshots were not captured. The 24 PNGs are in artifacts/hscroll-1579/week-before-after.zip; the available fj issue comment command has no file-upload option, so the archive is not attached to this issue.
  • The five samples per build were sequential rather than interleaved; performance results do not confirm the CSS rule as the cause. No Rust code changed. The full web suite was deferred per the merge-round verification rule.

Decisions not specified in DESIGN

  • Reuse the existing .block.cramped placement class instead of introducing another container or width measurement.
  • Keep full tracing opt-in because it changes measured scroll timing; collect a selected trace for style/layout diagnosis.
  • Use four overlapping real Events per day for the normal profile and a separate 20-per-day larger-fixture fallback.

For the merge round: run cd apps/web && bun run test -- --maxWorkers=2 for the full web suite.

## #1579 final report **Branch/head:** `job/hscroll-1579` at `033229513396193b61cf289cb7b8ae3a87f9475b` (base `4ebeba0b8`). No push, merge or deploy. ### Built - Removed `container-type: inline-size` from each Event time row. The end time chip now hides through the existing `.block.cramped` placement class, so cramped lanes do not overlap. The Event button `aria-label` keeps the complete interval. - Added the #1579 scroll profile, real CalDAV Event fixture, paint/frame/style/layout/long-task metrics, touch fallback inputs, screenshot captures and a regression guard against per-row size containers. Added an option to skip the unrelated Settings visit during profile setup. - Refreshed exact perf source pins; did not change `contracts/perf/ratchet.json`. ### Performance Measured on perf-test-69-63 with Chrome 153, the shared release server, 4x CPU throttle, and an 84-day fixture (336 real Events, 84 Tasks, 84 Logs). Five samples per build were run sequentially, not interleaved. Rightward 1440 px Week wheel medians: | Build | frame p50 | frame p95 | dropped frames | long tasks / total | |---|---:|---:|---:|---:| | good `1f9500fba` | 149.9 ms | 676.72 ms | 239 | 12 / 2,977 ms | | bad `4ebeba0b8` | 133.3 ms | 516.60 ms | 218 | 12 / 2,323 ms | | fix `3dbe81fa7` | 150.0 ms | 568.35 ms | 245 | 10 / 2,615 ms | The fix is within 10% of good on p95 and dropped frames. The bad build was faster in this five-sample run, so the owner-reported regression was not reproduced consistently. The first traced-all run overstated the regression; ordinary matched runs, dark mode, Month and the 1,680-Event fallback did not reach the 25% threshold. The larger one-sample Week run was +20.8% p95 and +23.0% dropped frames for bad vs good. I did not bisect because the reproduction gate was not met. Newly revealed Events painted in 0 ms p95 in the rightward Week samples. A selected trace reported style/layout time for 1440 px Day and Week on the fix: Day 182.72/95.16 ms; Week 559.63/568.99 ms. Single selected traces were noisy (good Week 344.53/458.18 ms; bad Week 408.02/529.24 ms), so they do not establish the cause. ### Files `packages/ui/src/components/calendar/GridColumn.svelte`; `apps/web/e2e/calendar-scroll-1362.mjs`; `apps/web/e2e/calendar-scroll-1579.test.mjs`; `apps/web/e2e/harness.mjs`; `bench/calendar-scroll-1579.mjs`; `contracts/perf/adoption-1058.json`; `contracts/perf/exceptions.json`. ### Gates `bun run check`: ```text perf-lint: PASS; 0 violations; 22128 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. Focus policy passes: 48 focus ring rules, 132 DOM focus calls and 2 data-input writers audited. UI transitions and animation options use shared motion tokens or documented exceptions. Loading svelte-check in workspace: /home/kayg/Developer/calternal-wt/hscroll-1579/apps/web Getting Svelte diagnostics... svelte-check found 0 errors and 0 warnings exit=0 ``` Focused Vitest: `Test Files 2 passed (2)`; `Tests 23 passed (23)`. Focused Node profile tests: `# tests 9`, `# pass 9`, `# fail 0`. `node --check bench/calendar-scroll-1579.mjs` and `git diff --check` passed. `cargo clean` removed 1 file (356 B); web build output was removed. ### UX gaps and known limits - Closed: cramped lane time chips no longer overlap; the full interval remains in the accessible name. - Left: before/after screenshots cover Week at 390, 820 and 1440 px in light/dark, with real data and macOS platform emulation. Day screenshots were not captured. The 24 PNGs are in `artifacts/hscroll-1579/week-before-after.zip`; the available `fj issue comment` command has no file-upload option, so the archive is not attached to this issue. - The five samples per build were sequential rather than interleaved; performance results do not confirm the CSS rule as the cause. No Rust code changed. The full web suite was deferred per the merge-round verification rule. ### Decisions not specified in DESIGN - Reuse the existing `.block.cramped` placement class instead of introducing another container or width measurement. - Keep full tracing opt-in because it changes measured scroll timing; collect a selected trace for style/layout diagnosis. - Use four overlapping real Events per day for the normal profile and a separate 20-per-day larger-fixture fallback. **For the merge round:** run `cd apps/web && bun run test -- --maxWorkers=2` for the full web suite.
Author
Owner

Perf VM load averages recorded inside the perf-run lock: good five-sample run 2.08 / 3.73 / 3.93; fixed run 4.24 / 4.43 / 4.19; bad run 3.89 / 4.49 / 4.28. The fixed traced Day/Week profile started at 3.59 / 4.68 / 4.45.

Perf VM load averages recorded inside the perf-run lock: good five-sample run 2.08 / 3.73 / 3.93; fixed run 4.24 / 4.43 / 4.19; bad run 3.89 / 4.49 / 4.28. The fixed traced Day/Week profile started at 3.59 / 4.68 / 4.45.
Author
Owner

Started on branch job/hscroll-1579, based on 033229513396193b61cf289cb7b8ae3a87f9475b in the provided worktree. I am reading the Calendar scroll profile and will compare good 1f9500fba, bad 4ebeba0b8, and fix 033229513 on the locked perf VM, with WebKit and Chromium, before deciding whether to bisect merge37.

Started on branch `job/hscroll-1579`, based on `033229513396193b61cf289cb7b8ae3a87f9475b` in the provided worktree. I am reading the Calendar scroll profile and will compare good `1f9500fba`, bad `4ebeba0b8`, and fix `033229513` on the locked perf VM, with WebKit and Chromium, before deciding whether to bisect merge37.
Author
Owner

Measurement setup finding: the current profile only launches Playwright Chromium and uses five large wheel deltas (780 px each), so it does not exercise the requested WebKit path or trackpad-like input. The previous 5-sample 336-Event comparison also ran builds sequentially. I am extending the profile to run 6 px wheel events at 120 Hz with a decaying tail, and to interleave good/bad/fix within each round.

Decision rule for this retest: reproduce only if bad is at least 20% worse than good on p95 frame interval or style+layout time in either engine, with all seven paired rounds in that direction or median paired ratio at least 1.2. If reproduced, I will keep the fix only if it is within 10% of good; otherwise I will bisect merge37 Calendar/UI commits with the 1,680-Event WebKit profile.

Measurement setup finding: the current profile only launches Playwright Chromium and uses five large wheel deltas (780 px each), so it does not exercise the requested WebKit path or trackpad-like input. The previous 5-sample 336-Event comparison also ran builds sequentially. I am extending the profile to run 6 px wheel events at 120 Hz with a decaying tail, and to interleave good/bad/fix within each round. Decision rule for this retest: reproduce only if bad is at least 20% worse than good on p95 frame interval or style+layout time in either engine, with all seven paired rounds in that direction or median paired ratio at least 1.2. If reproduced, I will keep the fix only if it is within 10% of good; otherwise I will bisect merge37 Calendar/UI commits with the 1,680-Event WebKit profile.
Author
Owner

WebKit setup finding: the shared passkey helper needs BrowserContext.newCDPSession and WebAuthn.addVirtualAuthenticator, which Playwright WebKit does not expose. I changed the profile to register the owner and seed the real Instance through Chromium, then copy that signed session into fresh WebKit asset contexts. Only setup uses Chromium; the measured Week/Day scrolling and WebKit screenshots still use WebKit. The standalone WebKit 26.6 launch smoke passed under perf-run.

WebKit setup finding: the shared passkey helper needs `BrowserContext.newCDPSession` and `WebAuthn.addVirtualAuthenticator`, which Playwright WebKit does not expose. I changed the profile to register the owner and seed the real Instance through Chromium, then copy that signed session into fresh WebKit asset contexts. Only setup uses Chromium; the measured Week/Day scrolling and WebKit screenshots still use WebKit. The standalone WebKit 26.6 launch smoke passed under `perf-run`.
Author
Owner

Profile smoke finding: the first WebKit runner launch stopped before setup because build-matrix validation called existsSync without importing it. I added the import and a focused contract check; node --check and all 14 #1579 Node tests now pass. No scroll samples were recorded in that failed smoke.

Profile smoke finding: the first WebKit runner launch stopped before setup because build-matrix validation called `existsSync` without importing it. I added the import and a focused contract check; `node --check` and all 14 #1579 Node tests now pass. No scroll samples were recorded in that failed smoke.
Author
Owner

Profile smoke finding: server startup expected the shared prebuilt media-sandbox runtime, which is not part of the minimal scratch runner copy. The perf VM already has a complete runtime under another job's target directory, so I will use it read-only through CALTERNAL_MEDIA_SANDBOX_ROOT. This smoke exited before server startup and recorded no scroll samples.

Profile smoke finding: server startup expected the shared prebuilt media-sandbox runtime, which is not part of the minimal scratch runner copy. The perf VM already has a complete runtime under another job's target directory, so I will use it read-only through `CALTERNAL_MEDIA_SANDBOX_ROOT`. This smoke exited before server startup and recorded no scroll samples.
Author
Owner

Profile smoke finding: the app suppresses its one-time build-version request when the cloned signed session already has the checked marker. The smoke therefore timed out after Week content had rendered and before any scroll sample. I now call /api/v1/version directly after the real cards appear, and added a contract test for this retained-session case. The smoke also held its perf-run lock during the slow WebKit teardown; I stopped only that run's process groups and verified the lock returned to free.

Profile smoke finding: the app suppresses its one-time build-version request when the cloned signed session already has the checked marker. The smoke therefore timed out after Week content had rendered and before any scroll sample. I now call `/api/v1/version` directly after the real cards appear, and added a contract test for this retained-session case. The smoke also held its perf-run lock during the slow WebKit teardown; I stopped only that run's process groups and verified the lock returned to `free`.
Author
Owner

Smoke finding: the selected WebKit profile seeded its test server, then waitForCards timed out after 120 seconds at the first Week navigation. The page had no Calendar root or rendered card keys. The runner exited without writing samples; perf-run released the lock and no profile processes remain. I am adding page URL/title/body-state diagnostics before retrying.

Smoke finding: the selected WebKit profile seeded its test server, then `waitForCards` timed out after 120 seconds at the first Week navigation. The page had no Calendar root or rendered card keys. The runner exited without writing samples; perf-run released the lock and no profile processes remain. I am adding page URL/title/body-state diagnostics before retrying.
Author
Owner

Follow-up finding: after fixture setup, the WebKit page is at /login (Sign in · calternal) with no Calendar grid. The Chromium setup cookie is __Host-calternal_session; Linux WebKit does not send this Secure cookie over the harness's plain HTTP localhost origin. Existing mobile-focus.mjs handles this with a real session-cookie API route. I will use the same harness pattern for WebKit and serve the selected local production document so the server cannot redirect that document to login.

Follow-up finding: after fixture setup, the WebKit page is at `/login` (`Sign in · calternal`) with no Calendar grid. The Chromium setup cookie is `__Host-calternal_session`; Linux WebKit does not send this Secure cookie over the harness's plain HTTP localhost origin. Existing `mobile-focus.mjs` handles this with a real session-cookie API route. I will use the same harness pattern for WebKit and serve the selected local production document so the server cannot redirect that document to login.
Author
Owner

The WebKit session setup now uses the real __Host-calternal_session only on API calls, serves the selected production document locally, and checks authentication at /readyz before Calendar navigation. Commit 741d8ea50 passed the 16 focused profile tests. The WebKit 336-Event smoke now passes: 3 builds × 2 directions, Week at 1440 px; fixture first-range paint was 0 ms with 0 blank frames.

The WebKit session setup now uses the real `__Host-calternal_session` only on API calls, serves the selected production document locally, and checks authentication at `/readyz` before Calendar navigation. Commit `741d8ea50` passed the 16 focused profile tests. The WebKit 336-Event smoke now passes: 3 builds × 2 directions, Week at 1440 px; fixture first-range paint was 0 ms with 0 blank frames.
Author
Owner

Measurement finding: the WebKit profile uses 84 page.mouse.wheel calls per stroke, but a measured stroke took 13,969–18,176 ms (85–86 scroll events), about 5–6 calls/sec. This does not meet the requested 120 Hz input cadence and made the full profile take about 24 minutes. I am replacing per-call Playwright RPC input with the same page-scheduled sequence in both engines: 84 small deltaX WheelEvents at 120 Hz, plus matching scrollLeft increments and the decaying tail. The report will label these as synthetic events because OS trackpad acceleration is not modeled.

Measurement finding: the WebKit profile uses 84 `page.mouse.wheel` calls per stroke, but a measured stroke took 13,969–18,176 ms (85–86 scroll events), about 5–6 calls/sec. This does not meet the requested 120 Hz input cadence and made the full profile take about 24 minutes. I am replacing per-call Playwright RPC input with the same page-scheduled sequence in both engines: 84 small deltaX WheelEvents at 120 Hz, plus matching scrollLeft increments and the decaying tail. The report will label these as synthetic events because OS trackpad acceleration is not modeled.
Author
Owner

The 120 Hz page-scheduled input smoke passes on WebKit with all 3 builds and both directions (Week, 1440 px). It reports the realized cadence for every sample (83–113 Hz in this one-round smoke) and records the synthetic-event limitation. Focused profile tests pass: 17/17. The official seven-round profiles now use this same input path in both engines.

The 120 Hz page-scheduled input smoke passes on WebKit with all 3 builds and both directions (Week, 1440 px). It reports the realized cadence for every sample (83–113 Hz in this one-round smoke) and records the synthetic-event limitation. Focused profile tests pass: 17/17. The official seven-round profiles now use this same input path in both engines.
Author
Owner

Measurement finding: the page-side 120 Hz 336-Event WebKit run completed with 84 samples, but several leftward Week strokes began at the scroll edge and produced no frame samples (good: 0 frames in 5/7 rounds; bad: 0 in 3/7; fix: 0 in 3/7). These samples cannot support the paired comparison. I am centering the fixture scroll surface before each measured stroke and will record/assert the available horizontal range so both directions move.

Measurement finding: the page-side 120 Hz 336-Event WebKit run completed with 84 samples, but several leftward Week strokes began at the scroll edge and produced no frame samples (good: 0 frames in 5/7 rounds; bad: 0 in 3/7; fix: 0 in 3/7). These samples cannot support the paired comparison. I am centering the fixture scroll surface before each measured stroke and will record/assert the available horizontal range so both directions move.
Author
Owner

Centered-scroll smoke finding: the profile generated all six Week samples, but shutdown reported one in-flight Tasks API route being fulfilled after the WebKit context was disposed. The selected window can request a large Tasks day payload when priming the centered range. I am tracking routed API work and draining it before measurement/context shutdown; this keeps fixture loading out of the measured stroke and prevents a late route callback from failing cleanup.

Centered-scroll smoke finding: the profile generated all six Week samples, but shutdown reported one in-flight Tasks API route being fulfilled after the WebKit context was disposed. The selected window can request a large Tasks day payload when priming the centered range. I am tracking routed API work and draining it before measurement/context shutdown; this keeps fixture loading out of the measured stroke and prevents a late route callback from failing cleanup.
Author
Owner

Centered WebKit smoke on perf-test-69-63, using the 336-Event fixture and Week 1440, completed for good 1f9500fba, bad 4ebeba0b8, and fix 033229513.

  • Both directions started at scrollLeft=136729 of 273467, so neither stroke was pinned to an edge.
  • All six strokes produced frame samples; the fast-prefetch probe passed with 0 blank frames and 0 ms first-new-range paint latency.
  • Scripted wheel input measured 76–115 Hz under renderer load. This smoke has too few frame intervals for p95 comparison; the seven-round profiles are in progress.
  • Route callbacks drained before samples and WebKit context teardown completed without the prior disposed-context error.
Centered WebKit smoke on `perf-test-69-63`, using the 336-Event fixture and Week 1440, completed for good `1f9500fba`, bad `4ebeba0b8`, and fix `033229513`. - Both directions started at `scrollLeft=136729` of `273467`, so neither stroke was pinned to an edge. - All six strokes produced frame samples; the fast-prefetch probe passed with 0 blank frames and 0 ms first-new-range paint latency. - Scripted wheel input measured 76–115 Hz under renderer load. This smoke has too few frame intervals for p95 comparison; the seven-round profiles are in progress. - Route callbacks drained before samples and WebKit context teardown completed without the prior disposed-context error.
Author
Owner

The first seven-round WebKit 336 Event profile completed, but it is not suitable for the regression decision. Week strokes recorded only 1–5 animation-frame intervals per 650 ms sample (some Day strokes recorded none); per-stroke frame p95 reached 1,233 ms across the builds, and actual scripted input cadence fell to 75–120 Hz. This indicates background-frame throttling or renderer saturation, so those values are retained as exploratory evidence only.

I added page.bringToFront() before each selected build is measured (aff92990b) and am rerunning the short smoke. I will only use the full matrix if the smoke has enough frame intervals for paired comparisons.

The first seven-round WebKit 336 Event profile completed, but it is not suitable for the regression decision. Week strokes recorded only 1–5 animation-frame intervals per 650 ms sample (some Day strokes recorded none); per-stroke frame p95 reached 1,233 ms across the builds, and actual scripted input cadence fell to 75–120 Hz. This indicates background-frame throttling or renderer saturation, so those values are retained as exploratory evidence only. I added `page.bringToFront()` before each selected build is measured (`aff92990b`) and am rerunning the short smoke. I will only use the full matrix if the smoke has enough frame intervals for paired comparisons.
Author
Owner

The isolated WebKit 1,680-Event matrix completed: seven paired Day/Week rounds for both directions, and the fast-prefetch probe passed with 0 blank frames. Week is not frame-quality data (0–3 RAF intervals in most 650 ms strokes).

The requested threshold flags Day-left: the median of seven paired bad/good p95 frame ratios is 1.221. The fix/good median is 1.308, outside the 10% band; the dropped-frame ratio median is 1.000. Day-left frame sample counts range from 1 to 26, so the p95 signal is noisy, but I am following the threshold and running the requested merge37 bisect. Chromium 1,680 is in progress.

The isolated WebKit 1,680-Event matrix completed: seven paired Day/Week rounds for both directions, and the fast-prefetch probe passed with 0 blank frames. Week is not frame-quality data (0–3 RAF intervals in most 650 ms strokes). The requested threshold flags Day-left: the median of seven paired bad/good p95 frame ratios is 1.221. The fix/good median is 1.308, outside the 10% band; the dropped-frame ratio median is 1.000. Day-left frame sample counts range from 1 to 26, so the p95 signal is noisy, but I am following the threshold and running the requested merge37 bisect. Chromium 1,680 is in progress.
Author
Owner

The isolated Chromium 1,680-Event matrix completed (84 paired strokes; prefetch probe passed with 0 blank frames). Median bad/good p95 frame ratios were 0.955–0.967 for Day and Week-left, and 0.918 for Week-right; median dropped-frame ratios were 0.954–1.066. Frame p95 therefore did not cross 1.20.

The separate Week trace measured style+layout at 76.73 ms versus 51.86 ms for good (1.48×); the fix measured 40.99 ms (0.79× good). Day trace totals were lower in bad and fix than good. The WebKit Day-left p95 signal and this single Chromium Week trace point in different directions, so I am bisecting the merge37 Calendar/UI changes at the requested 1,680-Event WebKit case before calling the regression cause.

The isolated Chromium 1,680-Event matrix completed (84 paired strokes; prefetch probe passed with 0 blank frames). Median bad/good p95 frame ratios were 0.955–0.967 for Day and Week-left, and 0.918 for Week-right; median dropped-frame ratios were 0.954–1.066. Frame p95 therefore did not cross 1.20. The separate Week trace measured style+layout at 76.73 ms versus 51.86 ms for good (1.48×); the fix measured 40.99 ms (0.79× good). Day trace totals were lower in bad and fix than good. The WebKit Day-left p95 signal and this single Chromium Week trace point in different directions, so I am bisecting the merge37 Calendar/UI changes at the requested 1,680-Event WebKit case before calling the regression cause.
Author
Owner

Bisect checkpoint 273b605b5 (the Calendar UI merge) did not reproduce the Day p95 signal at 1,680 Events. Against good, the candidate's median paired p95 ratios were 1.013 right and 1.085 left; no direction reached 1.20. The candidate also includes the narrow-lane UI rules from that merge.

The next relevant checkpoint is 7d310ded6, which merges the Intl/Calendar performance branch and includes the shallow route-load guard from 0d2f38f33. There is no crates/plugins/calendar source delta between good and bad; this checkpoint tests the front-end range-load/cache path against the same release server.

Bisect checkpoint `273b605b5` (the Calendar UI merge) did not reproduce the Day p95 signal at 1,680 Events. Against good, the candidate's median paired p95 ratios were 1.013 right and 1.085 left; no direction reached 1.20. The candidate also includes the narrow-lane UI rules from that merge. The next relevant checkpoint is `7d310ded6`, which merges the Intl/Calendar performance branch and includes the shallow route-load guard from `0d2f38f33`. There is no `crates/plugins/calendar` source delta between good and bad; this checkpoint tests the front-end range-load/cache path against the same release server.
Author
Owner

The next WebKit checkpoint, 7d310ded6, also gives mixed results. In its seven-round Day-left comparison, the checkpoint/good median p95 ratio is 1.473, while bad/good is 1.007 and fix/good is 1.032 in the same interleaved run. The Calendar UI-only checkpoint 273b605b5 was 1.085 Day-left and 1.013 Day-right. This does not isolate a regression in the current production bundle.

Chromium 1,680 frame p95 and dropped-frame medians also stayed below 1.20; only the single separate Week trace was high (bad/good style+layout 1.48x; fix/good 0.79x). With WebKit's Week frame samples mostly 0–3 per stroke and contradictory paired reruns, the perf VM does not give a stable reproduction. The Calendar range route source is unchanged between good and bad; the shallow route-load and Intl formatter front-end paths remain candidates for a macOS WebKit retest.

The next WebKit checkpoint, `7d310ded6`, also gives mixed results. In its seven-round Day-left comparison, the checkpoint/good median p95 ratio is 1.473, while bad/good is 1.007 and fix/good is 1.032 in the same interleaved run. The Calendar UI-only checkpoint `273b605b5` was 1.085 Day-left and 1.013 Day-right. This does not isolate a regression in the current production bundle. Chromium 1,680 frame p95 and dropped-frame medians also stayed below 1.20; only the single separate Week trace was high (bad/good style+layout 1.48x; fix/good 0.79x). With WebKit's Week frame samples mostly 0–3 per stroke and contradictory paired reruns, the perf VM does not give a stable reproduction. The Calendar range route source is unchanged between good and bad; the shallow route-load and Intl formatter front-end paths remain candidates for a macOS WebKit retest.
Author
Owner

WebKit 1680-Event, Day 1440, exact container-commit probe (4b479c564): seven interleaved rounds under perf-run (load average at lock acquisition 0.11, 0.94, 2.27). Median paired p95 ratios were current bad/good 1.352, existing fix/good 1.348, and exact 4b479c564/good 1.034; bad/good dropped-frame ratio was 0.953. The bad profile had only 3–47 RAF samples per stroke. The current bad build crossed the requested 1.20 threshold again, but the exact commit did not explain it and the fix was not within 10% of good. Together with the earlier UI-only and Intl checkpoint retests, the perf VM result is inconsistent and does not isolate a culprit. I will preserve the existing 4b479c564 narrow-lane behavior fix and report the WebKit limitation.

WebKit 1680-Event, Day 1440, exact container-commit probe (`4b479c564`): seven interleaved rounds under `perf-run` (load average at lock acquisition `0.11, 0.94, 2.27`). Median paired p95 ratios were current `bad/good 1.352`, existing `fix/good 1.348`, and exact `4b479c564/good 1.034`; bad/good dropped-frame ratio was `0.953`. The bad profile had only 3–47 RAF samples per stroke. The current bad build crossed the requested 1.20 threshold again, but the exact commit did not explain it and the fix was not within 10% of good. Together with the earlier UI-only and Intl checkpoint retests, the perf VM result is inconsistent and does not isolate a culprit. I will preserve the existing `4b479c564` narrow-lane behavior fix and report the WebKit limitation.
Author
Owner

#1579 final report

Branch: job/hscroll-1579
Head: bc228f4ef (includes one required merge from origin/dev).
No push, deploy, or issue close was performed.

Decision rule and result

I applied the requested rule: a regression is a paired bad/good p95 frame or style-plus-layout ratio of at least 1.20, with seven interleaved rounds. Several individual profile medians crossed that threshold. The follow-up runs did not agree, and they did not isolate a code change. The threshold hits are signals; this perf VM cannot give a stable cause.

Engine and fixture bad/good p95 Dropped-frame ratio fix/good p95 Evidence
Chromium, 336 Events 1.284 Week-left; other Day/Week directions 0.866–1.055 0.969–1.049 1.000 Week-left One 7-pair p95 crossing, without a corresponding dropped-frame increase.
Chromium, 1,680 Events All Day/Week directions 0.787–0.967 0.954–1.066 0.919–1.166 No p95 crossing. The separate Week trace measured 57.15 good, 86.47 bad and 41.39 fix ms of style+layout per scroll-second (1.51× bad/good); this is one trace sample.
WebKit, 336 Events 1.205 Day-left; 1.241 Week-right 0.982 Day-left; 1.157 Week-right 0.883 Day-left; 0.996 Week-right The affected strokes had only 2–24 RAF intervals.
WebKit, 1,680 Events Day-left was 1.221 initially, 1.006 and 1.007 in checkpoint reruns, then 1.352 in the exact-commit run. Initial Week-left/right were 0.972/0.991. 1.000 initially; 0.953 in the exact-commit run Day-left fix was 1.308, 0.992, 1.032, then 1.348 In the exact-commit run, 4b479c564/good was 1.034. The current bad profile had only 3–47 RAF intervals per stroke. WebKit does not expose the trace and long-task metrics used here.

The 1,680-Event Chromium p50 frame intervals ranged from 83–192 ms; total observed long tasks over the Day/Week trackpad samples were 19 good, 68 bad and 69 fix. WebKit p50 values varied from 16–1,176 ms because many strokes had very few frame intervals. These software-rendered results do not represent macOS Safari timing. The perf runner used 120 Hz synthetic pixel-wheel strokes with a decaying tail; script-created WheelEvents need an explicit scrollLeft update because they do not invoke the browser's default scroll action.

The exact 4b479c564 build did not isolate the slowdown. Other merge37 candidates include the Day title/mode-header query (fce97b526), Month-cell queries (1714686e0, b3da41f73), shared Calendar Intl formatting (54aa28a5c), and shallow URL/range loading (801642d2a, 0d2f38f33). All browser bundles in this run used server revision 4ebeba0b8, so this A/B did not vary the deployed Calendar range API implementation. No source change under crates/plugins/calendar was present between the specified good and bad client revisions. The macOS VM was unavailable by owner instruction; a real Safari/macOS retest remains the main gap.

Build and UX

The existing Event fix removes the per-Event .b-time-row size container. It reuses the packed lane's .block.cramped state to hide the second time chip in a narrow lane, while keeping the full time range in the accessible name. This preserves the no-overlap behavior. The bench profile and regression contract cover seven interleaved builds, Day/Week scrolling and the WebKit heavy fixture.

The production Calendar screenshots use macOS platform emulation and include loaded Week data in light and dark at 390, 820 and 1440 px. Settled before/after captures are attached below. The worktree also has 24 local captures (mid-fling and settled) under ignored artifacts/hscroll-1579/screenshots/.

Build 390 px 820 px 1440 px
Good 1f9500fba light · dark light · dark light · dark
Fix 033229513 light · dark light · dark light · dark

UX gaps closed: cramped Event lanes keep the end chip hidden without overlap; the complete time range remains accessible. UX gap left: the performance cause is not isolated on this headless Linux WebKit runner.

Files changed for #1579

  • apps/web/e2e/calendar-scroll-1362.mjs, apps/web/e2e/calendar-scroll-1579.test.mjs, apps/web/e2e/harness.mjs
  • bench/calendar-scroll-1579.mjs
  • packages/ui/src/components/calendar/GridColumn.svelte
  • contracts/perf/adoption-1058.json, contracts/perf/exceptions.json

Decisions not specified by DESIGN: the scroll profile uses a synthetic 120 Hz wheel cadence plus explicit scroll deltas for repeatability; the Calendar profile may skip the unrelated Settings screen during CalDAV setup, while the shared helper keeps its default verification enabled. No new product UI decision was introduced.

Gates

bun run check completed successfully. Verbatim terminal summary:

NOTE Bun skipped 1 lock package(s) not installed in this production environment
PASS production dependency licences: 759 locked package releases across apps/web, apps/docs and packages
perf-lint: PASS; 0 violations; 20383 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.
Focus policy passes: 48 focus ring rules, 133 DOM focus calls and 2 data-input writers audited.
UI transitions and animation options use shared motion tokens or documented exceptions.
svelte-check found 0 errors and 0 warnings

Focused regression gate: node --test apps/web/e2e/calendar-scroll-1579.test.mjs:

# tests 20
# suites 0
# pass 20
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 161.53166

The full web suite is deferred to the merge round per the verification rule: cd apps/web && bun run test -- --maxWorkers=2. cargo clean completed: Removed 1 file, 356B total.

Merge note: git diff --check --cached reported one incoming origin/dev trailing blank line in apps/web/scripts/design-rules-lib.mjs:106; I left that unrelated file unchanged.

## #1579 final report Branch: `job/hscroll-1579` Head: `bc228f4ef` (includes one required merge from `origin/dev`). No push, deploy, or issue close was performed. ### Decision rule and result I applied the requested rule: a regression is a paired `bad/good` p95 frame or style-plus-layout ratio of at least `1.20`, with seven interleaved rounds. Several individual profile medians crossed that threshold. The follow-up runs did not agree, and they did not isolate a code change. The threshold hits are signals; this perf VM cannot give a stable cause. | Engine and fixture | `bad/good` p95 | Dropped-frame ratio | `fix/good` p95 | Evidence | | --- | --- | --- | --- | --- | | Chromium, 336 Events | `1.284` Week-left; other Day/Week directions `0.866–1.055` | `0.969–1.049` | `1.000` Week-left | One 7-pair p95 crossing, without a corresponding dropped-frame increase. | | Chromium, 1,680 Events | All Day/Week directions `0.787–0.967` | `0.954–1.066` | `0.919–1.166` | No p95 crossing. The separate Week trace measured `57.15` good, `86.47` bad and `41.39` fix ms of style+layout per scroll-second (`1.51×` bad/good); this is one trace sample. | | WebKit, 336 Events | `1.205` Day-left; `1.241` Week-right | `0.982` Day-left; `1.157` Week-right | `0.883` Day-left; `0.996` Week-right | The affected strokes had only 2–24 RAF intervals. | | WebKit, 1,680 Events | Day-left was `1.221` initially, `1.006` and `1.007` in checkpoint reruns, then `1.352` in the exact-commit run. Initial Week-left/right were `0.972/0.991`. | `1.000` initially; `0.953` in the exact-commit run | Day-left fix was `1.308`, `0.992`, `1.032`, then `1.348` | In the exact-commit run, `4b479c564/good` was `1.034`. The current bad profile had only 3–47 RAF intervals per stroke. WebKit does not expose the trace and long-task metrics used here. | The 1,680-Event Chromium p50 frame intervals ranged from `83–192 ms`; total observed long tasks over the Day/Week trackpad samples were 19 good, 68 bad and 69 fix. WebKit p50 values varied from `16–1,176 ms` because many strokes had very few frame intervals. These software-rendered results do not represent macOS Safari timing. The perf runner used 120 Hz synthetic pixel-wheel strokes with a decaying tail; script-created WheelEvents need an explicit `scrollLeft` update because they do not invoke the browser's default scroll action. The exact `4b479c564` build did not isolate the slowdown. Other merge37 candidates include the Day title/mode-header query (`fce97b526`), Month-cell queries (`1714686e0`, `b3da41f73`), shared Calendar Intl formatting (`54aa28a5c`), and shallow URL/range loading (`801642d2a`, `0d2f38f33`). All browser bundles in this run used server revision `4ebeba0b8`, so this A/B did not vary the deployed Calendar range API implementation. No source change under `crates/plugins/calendar` was present between the specified good and bad client revisions. The macOS VM was unavailable by owner instruction; a real Safari/macOS retest remains the main gap. ### Build and UX The existing Event fix removes the per-Event `.b-time-row` size container. It reuses the packed lane's `.block.cramped` state to hide the second time chip in a narrow lane, while keeping the full time range in the accessible name. This preserves the no-overlap behavior. The bench profile and regression contract cover seven interleaved builds, Day/Week scrolling and the WebKit heavy fixture. The production Calendar screenshots use macOS platform emulation and include loaded Week data in light and dark at 390, 820 and 1440 px. Settled before/after captures are attached below. The worktree also has 24 local captures (mid-fling and settled) under ignored `artifacts/hscroll-1579/screenshots/`. | Build | 390 px | 820 px | 1440 px | | --- | --- | --- | --- | | Good `1f9500fba` | [light](https://git.kayg.org/attachments/94a79202-4011-4b44-b107-d8217b145aa1) · [dark](https://git.kayg.org/attachments/3be67ead-d57e-4ce1-8f07-7c0b6bd17752) | [light](https://git.kayg.org/attachments/609383b6-b5b6-497b-b24b-f1e3ae5493ec) · [dark](https://git.kayg.org/attachments/870d2d55-2101-4043-bde1-a2cc58151d31) | [light](https://git.kayg.org/attachments/002bdfef-f029-488e-b578-7be41cf1fa70) · [dark](https://git.kayg.org/attachments/e142fc2b-9b3c-4997-b903-18f8801cd586) | | Fix `033229513` | [light](https://git.kayg.org/attachments/9e5f00b4-8480-4464-a2f0-16ec17222a51) · [dark](https://git.kayg.org/attachments/0182a47f-cfa4-4ad9-8f06-23942d77529b) | [light](https://git.kayg.org/attachments/51608ba6-3a75-423c-82e2-3099a0188a2b) · [dark](https://git.kayg.org/attachments/1f87c8c9-bbc0-4cfa-a0ae-0c18afcf7152) | [light](https://git.kayg.org/attachments/beb733a9-2a21-4c56-91b0-e8203036b8d8) · [dark](https://git.kayg.org/attachments/7fa2413d-de68-4909-b693-1e68275ae9b0) | UX gaps closed: cramped Event lanes keep the end chip hidden without overlap; the complete time range remains accessible. UX gap left: the performance cause is not isolated on this headless Linux WebKit runner. ### Files changed for #1579 - `apps/web/e2e/calendar-scroll-1362.mjs`, `apps/web/e2e/calendar-scroll-1579.test.mjs`, `apps/web/e2e/harness.mjs` - `bench/calendar-scroll-1579.mjs` - `packages/ui/src/components/calendar/GridColumn.svelte` - `contracts/perf/adoption-1058.json`, `contracts/perf/exceptions.json` Decisions not specified by DESIGN: the scroll profile uses a synthetic 120 Hz wheel cadence plus explicit scroll deltas for repeatability; the Calendar profile may skip the unrelated Settings screen during CalDAV setup, while the shared helper keeps its default verification enabled. No new product UI decision was introduced. ### Gates `bun run check` completed successfully. Verbatim terminal summary: ```text NOTE Bun skipped 1 lock package(s) not installed in this production environment PASS production dependency licences: 759 locked package releases across apps/web, apps/docs and packages perf-lint: PASS; 0 violations; 20383 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. Focus policy passes: 48 focus ring rules, 133 DOM focus calls and 2 data-input writers audited. UI transitions and animation options use shared motion tokens or documented exceptions. svelte-check found 0 errors and 0 warnings ``` Focused regression gate: `node --test apps/web/e2e/calendar-scroll-1579.test.mjs`: ```text # tests 20 # suites 0 # pass 20 # fail 0 # cancelled 0 # skipped 0 # todo 0 # duration_ms 161.53166 ``` The full web suite is deferred to the merge round per the verification rule: `cd apps/web && bun run test -- --maxWorkers=2`. `cargo clean` completed: `Removed 1 file, 356B total`. Merge note: `git diff --check --cached` reported one incoming origin/dev trailing blank line in `apps/web/scripts/design-rules-lib.mjs:106`; I left that unrelated file unchanged.
Author
Owner

Deployed in merge round 41 (build 8e8409629, 09:44 IST 2026-10-12): per-Event .b-time-row size container removed. Linux measurements were inconclusive; kept open until the owner confirms scrolling on their Mac.

Deployed in merge round 41 (build 8e8409629, 09:44 IST 2026-10-12): per-Event .b-time-row size container removed. Linux measurements were inconclusive; kept open until the owner confirms scrolling on their Mac.
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#1579
No description provided.