Tasks: 12-30 s to open Board/List/Matrix with 1,000 Tasks #1286

Closed
opened 2026-10-07 00:46:09 +00:00 by kayg · 24 comments
Owner

Tasks Tab takes 12 to 30 seconds to become ready with 1,000 Tasks (bash-tasks #1215 measurement, release server, production SPA, 4x CPU throttle, loaded host):

| Width | Board | List | Matrix |
| 390 px | 12.4 s | 29.2 s | 24.0 s |
| 1440 px | 17.8 s | 22.2 s | 30.6 s |

A 24-client burst against the API returned p50 544 ms / p95 837 ms. Even allowing for host load and throttle, this is orders of magnitude over docs/DESIGN.md §59 budgets. Profile and fix:

  1. Reproduce on the perf VM through perf-run (netbird ssh root@10.69.69.63; perf-run calternal tasksperf <min> -- cmd; build here, copy binaries; no builds on that box), without CPU throttle and with 4x throttle, 1,000 and 5,000 Tasks. Use the corrected profile apps/web/e2e/tasks-perf.mjs from branch job/bash-tasks (merge it first).
  2. Split server vs client time: API latency of the Tasks list endpoint (and any N+1 per-Task calls), JSON size, then main-thread time (Chrome trace: scripting, layout, long tasks). Look for O(N^2) derivations, per-row effects, unvirtualized lists, per-card date formatting, re-sorting on every change.
  3. Fix the top causes: virtualize long lists, memoize derived groupings, batch API, index server queries, stream or paginate if needed. Optimistic UI stays.
  4. Targets on the perf VM without throttle: Board/List/Matrix ready under 1 s at 1,000 Tasks, under 2 s at 5,000; API p95 under 100 ms for the list. Report before/after medians over 3 interleaved runs, and add a Tasks entry to docs/perf/baseline.json.
    Gates verbatim: cargo fmt/clippy/test for crates you touch, bun run check, bun run test.
Tasks Tab takes 12 to 30 seconds to become ready with 1,000 Tasks (bash-tasks #1215 measurement, release server, production SPA, 4x CPU throttle, loaded host): | Width | Board | List | Matrix | | 390 px | 12.4 s | 29.2 s | 24.0 s | | 1440 px | 17.8 s | 22.2 s | 30.6 s | A 24-client burst against the API returned p50 544 ms / p95 837 ms. Even allowing for host load and throttle, this is orders of magnitude over docs/DESIGN.md §59 budgets. Profile and fix: 1. Reproduce on the perf VM through perf-run (netbird ssh root@10.69.69.63; `perf-run calternal tasksperf <min> -- cmd`; build here, copy binaries; no builds on that box), without CPU throttle and with 4x throttle, 1,000 and 5,000 Tasks. Use the corrected profile apps/web/e2e/tasks-perf.mjs from branch job/bash-tasks (merge it first). 2. Split server vs client time: API latency of the Tasks list endpoint (and any N+1 per-Task calls), JSON size, then main-thread time (Chrome trace: scripting, layout, long tasks). Look for O(N^2) derivations, per-row effects, unvirtualized lists, per-card date formatting, re-sorting on every change. 3. Fix the top causes: virtualize long lists, memoize derived groupings, batch API, index server queries, stream or paginate if needed. Optimistic UI stays. 4. Targets on the perf VM without throttle: Board/List/Matrix ready under 1 s at 1,000 Tasks, under 2 s at 5,000; API p95 under 100 ms for the list. Report before/after medians over 3 interleaved runs, and add a Tasks entry to docs/perf/baseline.json. Gates verbatim: cargo fmt/clippy/test for crates you touch, bun run check, bun run test.
Author
Owner

Starting #1286 on job/tasksperf-1286, based on origin/dev at 3601719b80a39f712daf07b37879b22929dfffa0. I will merge job/bash-tasks first to use its corrected Tasks profile, then reproduce and profile the current Tasks path on perf-test before changing code.

Starting #1286 on `job/tasksperf-1286`, based on `origin/dev` at `3601719b80a39f712daf07b37879b22929dfffa0`. I will merge `job/bash-tasks` first to use its corrected Tasks profile, then reproduce and profile the current Tasks path on perf-test before changing code.
Author
Owner

The first perf-VM profile attempt completed the 1,000-Task API seed but failed before route sampling: Playwright under Bun does not expose Browser.process(). I changed the sampler to read the Chromium browser PID through CDP SystemInfo.getProcessInfo; a separate perf-run smoke returned a browser PID successfully. The 1,000-Task baseline is now rerunning with unthrottled and 4x routes interleaved over one Home.

The first perf-VM profile attempt completed the 1,000-Task API seed but failed before route sampling: Playwright under Bun does not expose `Browser.process()`. I changed the sampler to read the Chromium browser PID through CDP `SystemInfo.getProcessInfo`; a separate `perf-run` smoke returned a browser PID successfully. The 1,000-Task baseline is now rerunning with unthrottled and 4x routes interleaved over one Home.
Author
Owner

Perf VM baseline on the release server and production SPA, 4-core host, 1,000 Tasks, three interleaved samples per route:

  • Task list API: 10 requests per load, 499,291 decoded bytes total. Across routes, the slowest measured API p95 was 57.5 ms, under the 100 ms target; there is no per-Task API fanout.
  • Unthrottled readiness p50: 1.4–2.0 s for Board, 2.1–3.0 s for List, and 2.7–3.8 s for Matrix (390/1440 px).
  • At 4x throttle, readiness p50: 2.1–3.3 s for Board, 9.0–10.6 s for List, and 10.4–12.1 s for Matrix.
  • Chrome traces show client work is the main cause. At 0x, List/Matrix spend about 1.6–2.1 s scripting; 4x Matrix spends 2.3 s styling and 1.1–2.0 s layout. List/Matrix mount all 1,000 rows/cards; Board already mounts only 7 visible cards.
  • The profile recorded host load average 0.8 before and 5.63 after the run. The host stayed below load 6 while sampling.

The initial run and CDP PID fix are recorded in the profile branch. Next I am measuring the 5,000-Task baseline, then I will target the client render path and compare again.

Perf VM baseline on the release server and production SPA, 4-core host, 1,000 Tasks, three interleaved samples per route: - Task list API: 10 requests per load, 499,291 decoded bytes total. Across routes, the slowest measured API p95 was 57.5 ms, under the 100 ms target; there is no per-Task API fanout. - Unthrottled readiness p50: 1.4–2.0 s for Board, 2.1–3.0 s for List, and 2.7–3.8 s for Matrix (390/1440 px). - At 4x throttle, readiness p50: 2.1–3.3 s for Board, 9.0–10.6 s for List, and 10.4–12.1 s for Matrix. - Chrome traces show client work is the main cause. At 0x, List/Matrix spend about 1.6–2.1 s scripting; 4x Matrix spends 2.3 s styling and 1.1–2.0 s layout. List/Matrix mount all 1,000 rows/cards; Board already mounts only 7 visible cards. - The profile recorded host load average 0.8 before and 5.63 after the run. The host stayed below load 6 while sampling. The initial run and CDP PID fix are recorded in the profile branch. Next I am measuring the 5,000-Task baseline, then I will target the client render path and compare again.
Author
Owner

Client render fix committed as ae3d8a0fa (perf(tasks): virtualize list and matrix views). The List observes only mounted rows with one ResizeObserver, batches row-height changes, and carries Tab/Shift+Tab across virtual boundaries. The Matrix now groups all Tasks in one pass and virtualizes fixed-height card rows against one shared scroll viewport; List table rows and Matrix list items expose their full count and mounted position. Board keeps Index order when it has no secondary grouping and computes each grouped label once. Task API pages after the first are fetched in bounded groups of three and appended in cursor order.

Focused regression run: bunx vitest run src/lib/tasks/TaskList.svelte.test.ts src/lib/tasks/TaskBoard.svelte.test.ts src/lib/tasks/TaskMatrix.svelte.test.ts src/lib/tasks/task-virtual.test.ts --maxWorkers=2 — 4 files and 13 tests passed, no Svelte warnings.

Implementation choices not fixed by DESIGN: List and Matrix use a 72dvh/48rem maximum internal scroll viewport; List starts with 72 px estimated rows, 8-row overscan and then uses measured heights; Matrix uses the existing Board's 152 px Task-card extent and 3-row overscan. These choices keep one scroll region per view and make keyboard traversal move to the adjacent Task at a virtual boundary.

Client render fix committed as ae3d8a0fa (`perf(tasks): virtualize list and matrix views`). The List observes only mounted rows with one ResizeObserver, batches row-height changes, and carries Tab/Shift+Tab across virtual boundaries. The Matrix now groups all Tasks in one pass and virtualizes fixed-height card rows against one shared scroll viewport; List table rows and Matrix list items expose their full count and mounted position. Board keeps Index order when it has no secondary grouping and computes each grouped label once. Task API pages after the first are fetched in bounded groups of three and appended in cursor order. Focused regression run: `bunx vitest run src/lib/tasks/TaskList.svelte.test.ts src/lib/tasks/TaskBoard.svelte.test.ts src/lib/tasks/TaskMatrix.svelte.test.ts src/lib/tasks/task-virtual.test.ts --maxWorkers=2` — 4 files and 13 tests passed, no Svelte warnings. Implementation choices not fixed by DESIGN: List and Matrix use a 72dvh/48rem maximum internal scroll viewport; List starts with 72 px estimated rows, 8-row overscan and then uses measured heights; Matrix uses the existing Board's 152 px Task-card extent and 3-row overscan. These choices keep one scroll region per view and make keyboard traversal move to the adjacent Task at a virtual boundary.
Author
Owner

Follow-up committed as 6197ce63e (fix(tasks): preserve virtual scroll focus and geometry). The List's one shared row observer also measures the header, so spacer offsets stay correct; the table uses stable column widths while rows enter and leave the window. Keyboard focus moves to the labeled scroll region before a manually scrolled row is unmounted, then Tab enters the first visible Task. Matrix uses the same focus handoff when a card leaves its shared window.

The focused Task suite passed again after this change: 4 files, 13 tests, no Svelte warnings. bun run build completed successfully. I am keeping the perf VM's baseline SPA unchanged until the 5,000-Task before profile releases its lock.

Follow-up committed as 6197ce63e (`fix(tasks): preserve virtual scroll focus and geometry`). The List's one shared row observer also measures the header, so spacer offsets stay correct; the table uses stable column widths while rows enter and leave the window. Keyboard focus moves to the labeled scroll region before a manually scrolled row is unmounted, then Tab enters the first visible Task. Matrix uses the same focus handoff when a card leaves its shared window. The focused Task suite passed again after this change: 4 files, 13 tests, no Svelte warnings. `bun run build` completed successfully. I am keeping the perf VM's baseline SPA unchanged until the 5,000-Task before profile releases its lock.
Author
Owner

5,000-Task profile setup finding (#1286): the original fixture made 5,000 separate Task API writes. On the perf VM it reached 4,842 Notes after 60 minutes and did not begin route timing; the perf-run cap ended it. Load was 1.6–3.5 during most of the run and the server remained alive, so the cap exposed fixture setup cost rather than a route result.

I changed the profile fixture to write batches of 500 uniquely anchored checkbox Tasks through the real Notes API, with at most three independent Note writes in flight. The profile now checks that the final Task API page contains the requested number of rows before measuring. The data still comes from the production Notes API and its Task index; the profile does not write files directly. I will use this same fixture path for both before and after runs.

5,000-Task profile setup finding (#1286): the original fixture made 5,000 separate Task API writes. On the perf VM it reached 4,842 Notes after 60 minutes and did not begin route timing; the `perf-run` cap ended it. Load was 1.6–3.5 during most of the run and the server remained alive, so the cap exposed fixture setup cost rather than a route result. I changed the profile fixture to write batches of 500 uniquely anchored checkbox Tasks through the real Notes API, with at most three independent Note writes in flight. The profile now checks that the final Task API page contains the requested number of rows before measuring. The data still comes from the production Notes API and its Task index; the profile does not write files directly. I will use this same fixture path for both before and after runs.
Author
Owner

Profile finding (#1286): after batched seeding created the full 5,000-Task index, the old-SPA Board route exceeded Playwright's default 30 s page.goto(..., waitUntil: 'load') timeout before the profile reached its readiness selector. The browser-process sampler then failed during outer cleanup because the route error had closed its measured process. The run produced no valid route timing.

I updated the profile to wait for DOM content, then the first visible Task, with a 180 s route cap. Collector shutdown is now memoized and runs in finally, so a failed navigation cannot leak samplers or hide the route error. Next run will check whether the old Board can reach its first Task within that bound.

Profile finding (#1286): after batched seeding created the full 5,000-Task index, the old-SPA Board route exceeded Playwright's default 30 s `page.goto(..., waitUntil: 'load')` timeout before the profile reached its readiness selector. The browser-process sampler then failed during outer cleanup because the route error had closed its measured process. The run produced no valid route timing. I updated the profile to wait for DOM content, then the first visible Task, with a 180 s route cap. Collector shutdown is now memoized and runs in `finally`, so a failed navigation cannot leak samplers or hide the route error. Next run will check whether the old Board can reach its first Task within that bound.
Author
Owner

5,000-Task baseline finding (#1286): after 35 of 36 interleaved route samples, the final 4x/1440 Matrix sample had not shown its first Task within 180 s and the run stopped at that per-route limit. The remaining run allowance was still available. This is consistent with the old Matrix rendering the full 5,000-card set on the main thread.

I extended the per-route bound to 360 s. The rerun will record all three samples if the route completes; the after profile uses the same bound and should show the effect of the virtual window.

5,000-Task baseline finding (#1286): after 35 of 36 interleaved route samples, the final 4x/1440 Matrix sample had not shown its first Task within 180 s and the run stopped at that per-route limit. The remaining run allowance was still available. This is consistent with the old Matrix rendering the full 5,000-card set on the main thread. I extended the per-route bound to 360 s. The rerun will record all three samples if the route completes; the after profile uses the same bound and should show the effect of the virtual window.
Author
Owner

5,000-Task baseline finding (#1286): while the old 4x/390 List route was rendering, the Chromium renderer reached about 4.0 GB RSS and 123% CPU on the four-core perf VM; the server was below 600 MB RSS. The first visible row had not appeared yet. This points to browser-side row construction as the dominant cost for this case.

5,000-Task baseline finding (#1286): while the old 4x/390 List route was rendering, the Chromium renderer reached about 4.0 GB RSS and 123% CPU on the four-core perf VM; the server was below 600 MB RSS. The first visible row had not appeared yet. This points to browser-side row construction as the dominant cost for this case.
Author
Owner

5,000-Task baseline finding (#1286): with the six-minute route bound, the interleaved profile reached the third unthrottled 1440 Matrix sample and Chromium's renderer crashed. The renderer RSS was about 6.8 GB immediately before the failure. No route JSON was written because the runner only emits its final aggregate after every sample.

The high-water memory accumulated across full-page route loads in one renderer. I am adding out-of-sample cleanup for the unloaded document and its JS heap, then will rerun the profile so each route measures a fresh document instead of inheriting dead Task DOMs.

5,000-Task baseline finding (#1286): with the six-minute route bound, the interleaved profile reached the third unthrottled 1440 Matrix sample and Chromium's renderer crashed. The renderer RSS was about 6.8 GB immediately before the failure. No route JSON was written because the runner only emits its final aggregate after every sample. The high-water memory accumulated across full-page route loads in one renderer. I am adding out-of-sample cleanup for the unloaded document and its JS heap, then will rerun the profile so each route measures a fresh document instead of inheriting dead Task DOMs.
Author
Owner

Before baseline, 1,000 Tasks (#1286): the completed 3-round perf-VM profile used the same batched Notes API fixture as the after profile. Host load average was 1.06/2.08/2.28 before and 3.67/2.77/2.52 after; CPU count was 4.

Route readiness p50 (390 px / 1440 px):

  • Unthrottled Board: 1.07 / 1.45 s; List: 2.18 / 2.59 s; Matrix: 2.51 / 3.10 s.
  • 4x throttle Board: 1.73 / 1.95 s; List: 8.54 / 9.28 s; Matrix: 9.33 / 10.45 s.

The Task-list endpoint used 10 page requests and 472,291 decoded bytes per route sample (one initial 390 px Board sample recorded 20 requests); route-sample p95 stayed below 62 ms. No Task detail or mutation calls appeared. The 24-client, 12-second burst completed 4,242 successful reads with p50 66.4 ms and p95 87.7 ms. List/Matrix mounted all 1,000 Tasks; their median Chrome scripting was about 1.0–1.1 s unthrottled and 6.5–6.6 s at 4x.

Before baseline, 1,000 Tasks (#1286): the completed 3-round perf-VM profile used the same batched Notes API fixture as the after profile. Host load average was 1.06/2.08/2.28 before and 3.67/2.77/2.52 after; CPU count was 4. Route readiness p50 (390 px / 1440 px): - Unthrottled Board: 1.07 / 1.45 s; List: 2.18 / 2.59 s; Matrix: 2.51 / 3.10 s. - 4x throttle Board: 1.73 / 1.95 s; List: 8.54 / 9.28 s; Matrix: 9.33 / 10.45 s. The Task-list endpoint used 10 page requests and 472,291 decoded bytes per route sample (one initial 390 px Board sample recorded 20 requests); route-sample p95 stayed below 62 ms. No Task detail or mutation calls appeared. The 24-client, 12-second burst completed 4,242 successful reads with p50 66.4 ms and p95 87.7 ms. List/Matrix mounted all 1,000 Tasks; their median Chrome scripting was about 1.0–1.1 s unthrottled and 6.5–6.6 s at 4x.
Author
Owner

Before baseline, 5,000 Tasks (#1286): the full 3-round perf-VM profile completed after adding between-route heap cleanup. Host load average was 2.32/2.52/2.45 before and 3.60/2.84/2.64 after; CPU count was 4.

Route readiness p50 (390 px / 1440 px):

  • Unthrottled Board: 1.23 / 2.08 s; List: 9.74 / 10.12 s; Matrix: 13.71 / 14.39 s.
  • 4x throttle Board: 2.62 / 3.40 s; List: 42.38 / 43.99 s; Matrix: 67.33 / 74.04 s.

Each route read 50 Task-list pages (2,361,491 decoded bytes). Task-list request p95 was below 28 ms and no Task detail or mutation calls appeared. The 24-client, 12-second burst completed 1,905 successful reads with p50 146.1 ms and p95 215.8 ms. On unthrottled List/Matrix, median Chrome scripting was 7.8 s; at 4x it was about 36–37 s. This separates the route delay from the list API: most of the readiness cost is client rendering.

Before baseline, 5,000 Tasks (#1286): the full 3-round perf-VM profile completed after adding between-route heap cleanup. Host load average was 2.32/2.52/2.45 before and 3.60/2.84/2.64 after; CPU count was 4. Route readiness p50 (390 px / 1440 px): - Unthrottled Board: 1.23 / 2.08 s; List: 9.74 / 10.12 s; Matrix: 13.71 / 14.39 s. - 4x throttle Board: 2.62 / 3.40 s; List: 42.38 / 43.99 s; Matrix: 67.33 / 74.04 s. Each route read 50 Task-list pages (2,361,491 decoded bytes). Task-list request p95 was below 28 ms and no Task detail or mutation calls appeared. The 24-client, 12-second burst completed 1,905 successful reads with p50 146.1 ms and p95 215.8 ms. On unthrottled List/Matrix, median Chrome scripting was 7.8 s; at 4x it was about 36–37 s. This separates the route delay from the list API: most of the readiness cost is client rendering.
Author
Owner

The 5,000-Task after profile found 7–49 /api/v1/notes/tasks Resource Timing entries across routes, although the fixture's last-page assertion and full-projection wait require 50 page reads. The earlier 5,000-Task profile recorded 50 entries per route. I am increasing the browser timing buffer and will use that corrected collector for final API numbers.

The 5,000-Task after profile found 7–49 `/api/v1/notes/tasks` Resource Timing entries across routes, although the fixture's last-page assertion and full-projection wait require 50 page reads. The earlier 5,000-Task profile recorded 50 entries per route. I am increasing the browser timing buffer and will use that corrected collector for final API numbers.
Author
Owner

The corrected 5,000-Task profile retained all list timing entries and recorded 2.36 MB across 52 successful list GETs. Across the 3 unthrottled samples, route readiness medians were 759–817 ms at 390 px and 1.41–1.48 s at 1440 px. The maximum list-page p95 across those routes was 93.7 ms. The 24-client, 12-second 5,000-Task burst remained at 226.7 ms p95.

At 1,000 Tasks, phone medians are 741–797 ms; desktop medians are 1.21–1.35 s. The desktop 1-second readiness target remains unmet. A selected-layout dynamic import experiment kept the 816 KB shared startup dependency graph and worsened the six-route unthrottled median, so I reverted it.

The corrected 5,000-Task profile retained all list timing entries and recorded 2.36 MB across 52 successful list GETs. Across the 3 unthrottled samples, route readiness medians were 759–817 ms at 390 px and 1.41–1.48 s at 1440 px. The maximum list-page p95 across those routes was 93.7 ms. The 24-client, 12-second 5,000-Task burst remained at 226.7 ms p95. At 1,000 Tasks, phone medians are 741–797 ms; desktop medians are 1.21–1.35 s. The desktop 1-second readiness target remains unmet. A selected-layout dynamic import experiment kept the 816 KB shared startup dependency graph and worsened the six-route unthrottled median, so I reverted it.
Author
Owner

Completed: Tasks readiness profile and fix

Head: 6521d6d583fe8c087c22f498e5c465f4b8dab940 (perf(tasks): record Tasks UI latency baseline). No push, deploy, merge, or issue close was performed.

Built

  • Virtualized Task Board lanes, List rows with variable-height geometry, and Matrix cards. Rendering stays bounded, keyboard traversal crosses virtual windows, and Board grouping/date formatting avoids repeated work.
  • Task Index now exposes the first real 100-Task page as soon as it arrives, then reads remaining pages with bounded concurrency of three while preserving cursor order. Optimistic Status writes and revision fences remain in place.
  • Extended the real-server production-SPA profile to measure API request count, p95 and decoded JSON bytes, browser scripting/style/layout/long tasks, CPU/RSS, burst behavior, and interleaved unthrottled/4x route readiness. Increased the resource timing buffer so the 5,000-Task profile retains every list request.
  • Added focused tests and the Tasks UI record in docs/perf/baseline.json.

Perf VM measurements

All readiness values are p50 milliseconds from three interleaved route samples on the perf VM, release server and production SPA. 390px and 1440px are unthrottled; 4x throttle values are also recorded in the baseline.

Tasks Width Before Board/List/Matrix After Board/List/Matrix
1,000 390px 1,066 / 2,180 / 2,509 741 / 797 / 769
1,000 1440px 1,453 / 2,588 / 3,095 1,345 / 1,338 / 1,205
5,000 390px 1,234 / 9,737 / 13,711 759 / 804 / 817
5,000 1440px 2,080 / 10,123 / 14,388 1,411 / 1,446 / 1,477

At 5,000 Tasks, the maximum unthrottled list-page p95 was 93.7 ms over 52 successful GETs and 2,361,553 decoded bytes. At 1,000 Tasks it was 75.6 ms over 10 GETs and 472,291 bytes. The 24-client, 12-second 5,000-Task burst was p50 174.1 ms / p95 226.7 ms, with server mean CPU 90.16% and peak RSS 598,859,776 bytes.

Known gaps: the 1,000-Task desktop routes remain above the 1-second readiness target (1.205–1.345 s). The 5,000-Task burst p95 remains above 100 ms. At 4x throttle, the 1,000-Task 390px List median is 1,989 ms and 5,000-Task route medians exceed 2 seconds. Normal list-page p95 stays below 100 ms through 5,000 Tasks. Desktop main-thread p50 at 1,000 Tasks was 141–194 ms scripting, 34–38 ms style, and 17–25 ms layout; the longest sampled tasks were 330–341 ms.

Files

  • Task UI and projection: apps/web/src/lib/tasks/TaskBoard.svelte, TaskCard.svelte, TaskColumn.svelte, TaskList.svelte, TaskMatrix.svelte, TasksSidebar.svelte, TasksWorkspace.svelte, api.ts, index.svelte.ts, and new task-virtual.ts.
  • Focused coverage and real profile: apps/web/src/lib/tasks/api.test.ts, TaskList.svelte.test.ts, TaskMatrix.svelte.test.ts, task-virtual.test.ts, apps/web/e2e/tasks-perf.mjs, apps/web/e2e/task-checkbox-1229.mjs.
  • Shared interactions and docs: apps/web/src/lib/components/SidebarLinks.svelte, TagRow.svelte, TagRow.svelte.test.ts, apps/web/src/lib/notes/NoteView.svelte, bench/tasks-ui/README.md, docs/perf/baseline.json, contracts/perf/adoption-1058.json, contracts/perf/exceptions.json, contracts/perf/ratchet.json.

Shared visible controls continue to use @calternal/ui Card, Pill, Checkbox, StatusPill, and TagPill, plus the shared ItemActionMenu and SidebarLinks components.

Decisions, UX gaps, and cost

  • The design does not set a page-fetch concurrency or a variable-height estimate. This change uses three concurrent 100-row reads, a 152px estimate for unmeasured List rows, and three rows of overscan. The first real page is shown while later pages load.
  • UX gaps closed: large List/Matrix DOMs are bounded; the initial Tasks view no longer waits for the full projection; keyboard traversal is retained across virtual windows; existing optimistic Status and Undo behavior remains. Focused Task coverage passed.
  • UX gaps left: 1,000-Task desktop readiness and the 5,000-Task burst p95 above remain. Screenshots below cover all three widths, all three layouts, light/dark and macOS shortcut rendering; visual review is for the orchestrator.
  • Expected client cost is O(N) to assemble the bounded full projection and group Tasks once, followed by O(visible rows + overscan) mounted DOM per virtual view. The first 100-Task page is available before remaining pages finish.

Gates (verbatim output)

cargo fmt --check — exit 0, no output.

No Rust source or crate changed, so Rust clippy and crate tests were not applicable.

Focused Vitest output:

 Test Files  5 passed (5)
      Tests  15 passed (15)
   Start at  07:45:33
   Duration  50.09s (transform 75%, import 9%, environment 7%, tests 6%, setup 2%)

bun run check — exit 0; final output:

Loading svelte-check in workspace: /home/kayg/Developer/calternal-wt/tasksperf-1286/apps/web
Getting Svelte diagnostics...

/home/kayg/Developer/calternal-wt/tasksperf-1286/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 — failed with three unrelated 5-second timeouts; no expectations were changed:

 FAIL  |unit| src/lib/actions/magnetic.test.ts > magnetic pill items > delegates the shared pointer drift to each pill item, not the group shell
Error: Test timed out in 5000ms.
 FAIL  |component| src/lib/files/UploadToast.svelte.test.ts > shows a refused name reason in the collapsed toast without a Retry action
Error: Test timed out in 5000ms.
 FAIL  |component| src/routes/settings/admin/UsersGroup.svelte.test.ts > Admin Users Licence limit > shows the exact count and blocks adding or re-enabling at the limit
Error: Test timed out in 5000ms.
Test Files  3 failed | 286 passed (289)
Tests       3 failed | 2038 passed (2041)
Start at  07:52:54
Duration  1231.43s (transform 34%, import 23%, environment 23%, tests 15%, setup 4%)
error: script "test" exited with code 1

git diff --check passed. cargo clean removed 9,259 files / 3.4 GiB, and apps/web/build plus .svelte-kit/output were removed.

Screenshot set

All screenshots emulate macOS. Widths are 390, 820, and 1440 px; each set includes Board, List and Matrix in light and dark themes.

## Completed: Tasks readiness profile and fix Head: `6521d6d583fe8c087c22f498e5c465f4b8dab940` (`perf(tasks): record Tasks UI latency baseline`). No push, deploy, merge, or issue close was performed. ### Built - Virtualized Task Board lanes, List rows with variable-height geometry, and Matrix cards. Rendering stays bounded, keyboard traversal crosses virtual windows, and Board grouping/date formatting avoids repeated work. - Task Index now exposes the first real 100-Task page as soon as it arrives, then reads remaining pages with bounded concurrency of three while preserving cursor order. Optimistic Status writes and revision fences remain in place. - Extended the real-server production-SPA profile to measure API request count, p95 and decoded JSON bytes, browser scripting/style/layout/long tasks, CPU/RSS, burst behavior, and interleaved unthrottled/4x route readiness. Increased the resource timing buffer so the 5,000-Task profile retains every list request. - Added focused tests and the Tasks UI record in `docs/perf/baseline.json`. ### Perf VM measurements All readiness values are p50 milliseconds from three interleaved route samples on the perf VM, release server and production SPA. `390px` and `1440px` are unthrottled; 4x throttle values are also recorded in the baseline. | Tasks | Width | Before Board/List/Matrix | After Board/List/Matrix | |---:|---:|---:|---:| | 1,000 | 390px | 1,066 / 2,180 / 2,509 | 741 / 797 / 769 | | 1,000 | 1440px | 1,453 / 2,588 / 3,095 | 1,345 / 1,338 / 1,205 | | 5,000 | 390px | 1,234 / 9,737 / 13,711 | 759 / 804 / 817 | | 5,000 | 1440px | 2,080 / 10,123 / 14,388 | 1,411 / 1,446 / 1,477 | At 5,000 Tasks, the maximum unthrottled list-page p95 was 93.7 ms over 52 successful GETs and 2,361,553 decoded bytes. At 1,000 Tasks it was 75.6 ms over 10 GETs and 472,291 bytes. The 24-client, 12-second 5,000-Task burst was p50 174.1 ms / p95 226.7 ms, with server mean CPU 90.16% and peak RSS 598,859,776 bytes. Known gaps: the 1,000-Task desktop routes remain above the 1-second readiness target (1.205–1.345 s). The 5,000-Task burst p95 remains above 100 ms. At 4x throttle, the 1,000-Task 390px List median is 1,989 ms and 5,000-Task route medians exceed 2 seconds. Normal list-page p95 stays below 100 ms through 5,000 Tasks. Desktop main-thread p50 at 1,000 Tasks was 141–194 ms scripting, 34–38 ms style, and 17–25 ms layout; the longest sampled tasks were 330–341 ms. ### Files - Task UI and projection: `apps/web/src/lib/tasks/TaskBoard.svelte`, `TaskCard.svelte`, `TaskColumn.svelte`, `TaskList.svelte`, `TaskMatrix.svelte`, `TasksSidebar.svelte`, `TasksWorkspace.svelte`, `api.ts`, `index.svelte.ts`, and new `task-virtual.ts`. - Focused coverage and real profile: `apps/web/src/lib/tasks/api.test.ts`, `TaskList.svelte.test.ts`, `TaskMatrix.svelte.test.ts`, `task-virtual.test.ts`, `apps/web/e2e/tasks-perf.mjs`, `apps/web/e2e/task-checkbox-1229.mjs`. - Shared interactions and docs: `apps/web/src/lib/components/SidebarLinks.svelte`, `TagRow.svelte`, `TagRow.svelte.test.ts`, `apps/web/src/lib/notes/NoteView.svelte`, `bench/tasks-ui/README.md`, `docs/perf/baseline.json`, `contracts/perf/adoption-1058.json`, `contracts/perf/exceptions.json`, `contracts/perf/ratchet.json`. Shared visible controls continue to use `@calternal/ui` Card, Pill, Checkbox, StatusPill, and TagPill, plus the shared ItemActionMenu and SidebarLinks components. ### Decisions, UX gaps, and cost - The design does not set a page-fetch concurrency or a variable-height estimate. This change uses three concurrent 100-row reads, a 152px estimate for unmeasured List rows, and three rows of overscan. The first real page is shown while later pages load. - UX gaps closed: large List/Matrix DOMs are bounded; the initial Tasks view no longer waits for the full projection; keyboard traversal is retained across virtual windows; existing optimistic Status and Undo behavior remains. Focused Task coverage passed. - UX gaps left: 1,000-Task desktop readiness and the 5,000-Task burst p95 above remain. Screenshots below cover all three widths, all three layouts, light/dark and macOS shortcut rendering; visual review is for the orchestrator. - Expected client cost is O(N) to assemble the bounded full projection and group Tasks once, followed by O(visible rows + overscan) mounted DOM per virtual view. The first 100-Task page is available before remaining pages finish. ### Gates (verbatim output) `cargo fmt --check` — exit 0, no output. No Rust source or crate changed, so Rust clippy and crate tests were not applicable. Focused Vitest output: ```text Test Files 5 passed (5) Tests 15 passed (15) Start at 07:45:33 Duration 50.09s (transform 75%, import 9%, environment 7%, tests 6%, setup 2%) ``` `bun run check` — exit 0; final output: ```text Loading svelte-check in workspace: /home/kayg/Developer/calternal-wt/tasksperf-1286/apps/web Getting Svelte diagnostics... /home/kayg/Developer/calternal-wt/tasksperf-1286/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` — failed with three unrelated 5-second timeouts; no expectations were changed: ```text FAIL |unit| src/lib/actions/magnetic.test.ts > magnetic pill items > delegates the shared pointer drift to each pill item, not the group shell Error: Test timed out in 5000ms. FAIL |component| src/lib/files/UploadToast.svelte.test.ts > shows a refused name reason in the collapsed toast without a Retry action Error: Test timed out in 5000ms. FAIL |component| src/routes/settings/admin/UsersGroup.svelte.test.ts > Admin Users Licence limit > shows the exact count and blocks adding or re-enabling at the limit Error: Test timed out in 5000ms. Test Files 3 failed | 286 passed (289) Tests 3 failed | 2038 passed (2041) Start at 07:52:54 Duration 1231.43s (transform 34%, import 23%, environment 23%, tests 15%, setup 4%) error: script "test" exited with code 1 ``` `git diff --check` passed. `cargo clean` removed 9,259 files / 3.4 GiB, and `apps/web/build` plus `.svelte-kit/output` were removed. ### Screenshot set All screenshots emulate macOS. Widths are 390, 820, and 1440 px; each set includes Board, List and Matrix in light and dark themes. - 1440 dark: [Board](https://git.kayg.org/attachments/a479a4da-b558-438b-b51d-b4d0245a61f9), [List](https://git.kayg.org/attachments/26349d2a-cb94-4b99-a334-24add5389860), [Matrix](https://git.kayg.org/attachments/2dc56834-226f-4693-9ba7-c8bb8519c388) - 1440 light: [Board](https://git.kayg.org/attachments/d3eea396-737c-4fc9-9daa-b62eee7b7294), [List](https://git.kayg.org/attachments/28c63df0-873f-487b-a9da-33c611782494), [Matrix](https://git.kayg.org/attachments/5652caae-a831-46a5-bd0e-d00f6ec07fcd) - 820 dark: [Board](https://git.kayg.org/attachments/829e26e1-2757-4ea4-88ed-fbde81e3f2b5), [List](https://git.kayg.org/attachments/8751403f-81ee-4ca5-9280-d900a52b73d3), [Matrix](https://git.kayg.org/attachments/49296b60-2dcb-4865-be0f-a9df7b6abc2d) - 820 light: [Board](https://git.kayg.org/attachments/b50d3e45-77b4-4e6a-85ab-99b474c7128f), [List](https://git.kayg.org/attachments/fa48841c-0430-4014-8ab0-851da2e133f7), [Matrix](https://git.kayg.org/attachments/745c2f9b-71bd-4a60-a818-1157dae45cf4) - 390 dark: [Board](https://git.kayg.org/attachments/bf61cacf-824f-4784-a1b0-721830ff41ef), [List](https://git.kayg.org/attachments/9b5083d9-6541-4a3b-af00-00ccb14d87de), [Matrix](https://git.kayg.org/attachments/55d666df-e682-45d2-aa13-32a52c5e0c97) - 390 light: [Board](https://git.kayg.org/attachments/7d8b3511-c003-4779-a0cb-cfe82d1a4da0), [List](https://git.kayg.org/attachments/20013d6f-1b92-4809-8949-ce7965af6811), [Matrix](https://git.kayg.org/attachments/6fbd4380-d6ec-4bbd-8d71-5cd0207ef917)
Author
Owner

Continuing #1286 on job/tasksperf-1286, initial head 6521d6d583fe8c087c22f498e5c465f4b8dab940. I fetched origin and merged origin/dev at 3601719b80a39f712daf07b37879b22929dfffa0; it was already included.

I am investigating the screenshot regressions before changing code: virtual row geometry for variable-height cards, totals while later pages load, and Board spacing. I will add the requested browser regressions, rerun the Tasks profile and capture the requested screenshot set.

Continuing #1286 on `job/tasksperf-1286`, initial head `6521d6d583fe8c087c22f498e5c465f4b8dab940`. I fetched `origin` and merged `origin/dev` at `3601719b80a39f712daf07b37879b22929dfffa0`; it was already included. I am investigating the screenshot regressions before changing code: virtual row geometry for variable-height cards, totals while later pages load, and Board spacing. I will add the requested browser regressions, rerun the Tasks profile and capture the requested screenshot set.
Author
Owner

Finding for #1286: TaskPage in crates/plugins/notes/src/tasks_api.rs and the generated TaskPage type expose only items and next_cursor; there is no total in the first response. TaskIndex.#read publishes that first page to taskIndex.tasks, while Board lane counts and the Tasks sidebar Tag counts are derived from the current array length. This makes the visible 100 a loaded-page count. I will hide those counts until TaskIndex.status is ready, then expose the complete projection's count.

The Matrix and Board both use fixed 152px row extents (TaskMatrix.svelte and TaskColumn.svelte). TaskCard content can be taller when metadata and TagPills wrap. List already observes mounted row heights and clears its height cache when its container width changes. I am adding the browser regression before changing the Matrix and Board geometry.

Finding for #1286: `TaskPage` in `crates/plugins/notes/src/tasks_api.rs` and the generated `TaskPage` type expose only `items` and `next_cursor`; there is no total in the first response. `TaskIndex.#read` publishes that first page to `taskIndex.tasks`, while Board lane counts and the Tasks sidebar Tag counts are derived from the current array length. This makes the visible `100` a loaded-page count. I will hide those counts until `TaskIndex.status` is `ready`, then expose the complete projection's count. The Matrix and Board both use fixed `152px` row extents (`TaskMatrix.svelte` and `TaskColumn.svelte`). TaskCard content can be taller when metadata and TagPills wrap. List already observes mounted row heights and clears its height cache when its container width changes. I am adding the browser regression before changing the Matrix and Board geometry.
Author
Owner

The new production Playwright regression reproduced both findings against a real local server and the production SPA, with the macOS browser platform emulated and 1,000 real Tasks seeded through the Notes API.

  • While later pages were held in flight, Board and the #review sidebar Tag both showed 100.
  • The visible Task card bounding boxes intersected in Board and Matrix at 390, 820 and 1440 px. The first 390 px sample measured 33 px of Board card overlap and 83 px of Matrix card overlap. The List rectangle check did not report an intersection in this run.

The test now records these regressions before the UI changes. I am fixing Matrix and Board geometry and hiding partial counts until the complete projection is ready.

The new production Playwright regression reproduced both findings against a real local server and the production SPA, with the macOS browser platform emulated and 1,000 real Tasks seeded through the Notes API. - While later pages were held in flight, Board and the `#review` sidebar Tag both showed `100`. - The visible Task card bounding boxes intersected in Board and Matrix at 390, 820 and 1440 px. The first 390 px sample measured 33 px of Board card overlap and 83 px of Matrix card overlap. The List rectangle check did not report an intersection in this run. The test now records these regressions before the UI changes. I am fixing Matrix and Board geometry and hiding partial counts until the complete projection is ready.
Author
Owner

Completed

Branch: job/tasksperf-1286
Head: 1f9ed1c5ed6920810dec0a7b072064b33c6ffe10

Changes

  • Board, List, and Matrix virtual rows now use batched ResizeObserver measurements. Heights are cached by Task ID and rendered width. The 152px estimate is used only before a row is measured. Matrix keyboard traversal uses the same measured offsets, and the Board keeps its 8px card spacing.
  • Board, sidebar Tag, and ARIA counts stay unknown while Task pages are still loading. They show exact totals when the Task Index is ready.
  • Added test:e2e:tasksperf-1286: it creates 1,000 real Notes-backed Tasks, holds later pages to check partial counts, and checks visible Task row/card intersections in Board, List, and Matrix at 390/820/1440 in light and dark with macOS platform emulation.
  • Refreshed 38 exact Task performance pins. The perf ledger reports 22,514 used pins, 0 remaining, and no added ratchet entries.

Touched files: TaskColumn.svelte, TaskMatrix.svelte, TaskList.svelte, TaskBoard.svelte, TasksSidebar.svelte, TasksWorkspace.svelte, task-virtual.ts, task-virtual.test.ts, apps/web/e2e/tasksperf-1286.mjs, apps/web/package.json, contracts/perf/adoption-1058.json, contracts/perf/exceptions.json, and contracts/perf/ratchet.json.

UX gaps closed

  • Wrapped titles and multiple Tags no longer overlap adjacent virtual rows at any tested width.
  • Partial page counts are hidden during loading; complete counts are exact.
  • The Board card spacing is restored.
  • The added geometry checks and ARIA set sizes follow measured row positions and count readiness.

UX gaps left: none found in this regression scope. The visual screenshots are for the orchestrator's review; I did not judge their visual quality.

Screenshots: artifacts/tasksperf-1286/after-fix/ (18 width/layout/theme screenshots plus 1440-light-matrix-long-titles.png; 19 files total).

Verification

bun run check — exit 0:

perf-lint: PASS; 0 violations; 22514 scoped exceptions
svelte-check found 0 errors and 1 warning in 1 file

The warning is the existing empty CSS ruleset at packages/ui/src/components/calendar/AttachmentDeck.svelte:1055.

bun run build — exit 0:

Compressed 917 static variants; saved 21506320 bytes.

bun run test — exit 1. Under shared-host load, 10 tests in 9 files timed out at Vitest's 5s limit:

 Test Files  9 failed | 280 passed (289)
      Tests  10 failed | 2031 passed (2041)
error: script "test" exited with code 1

Isolated rerun of the 9 failed files:

 Test Files  9 passed (9)
      Tests  71 passed (71)

After the final width-aware change, one existing Matrix keyboard test also timed out in a combined focused run; rerunning TaskMatrix.svelte.test.ts alone passed both tests, and task-virtual.test.ts passed in the combined run.

Production-build Playwright run: PASS, no count or geometry failures and no browser runtime errors. The run captured all 19 screenshots above.

Performance profile

Repeated the 1,000- and 5,000-Task profile with 3 samples per route, Board/List/Matrix at 390px and 1440px, 0x and 4x CPU throttling, and a 24-client/12-second burst. The perf VM was held by tabstorm-1338, so this was measured locally using the debug server and production SPA.

Local unthrottled route-ready p50 ranges were 0.913–2.200s at 1,000 Tasks and 0.938–2.460s at 5,000 Tasks. At 4x throttle they were 3.294–7.140s and 4.031–6.382s. Burst results: 1,000 Tasks: 686 requests, p50 376.7ms, p95 795.4ms, all HTTP 200, peak RSS 610,680,832 bytes. 5,000 Tasks: 639 requests, p50 461.8ms, p95 601.7ms, all HTTP 200, peak RSS 661,438,464 bytes.

The 12-core local host load was 25.05/30.41/37.51 before and 29.58/29.18/35.65 after the 1,000-Task run; it was 27.20/28.64/35.30 before and 24.92/29.27/34.01 after the 5,000-Task run. These debug-server results under high load are diagnostic and cannot establish a direct comparison with the clean perf-VM baseline in docs/perf/baseline.json. The new geometry work costs O(N) to rebuild prefix offsets after a batch of measurements; ResizeObserver observes mounted rows only and batches measurements per frame. Profiles exercised N=1,000 and N=5,000.

Decisions and known gaps

  • For variable card heights, cache the border-box height per stable Task ID and width; use the existing 152px estimate until measurement. This keeps offsets width-specific without layout reads in loops.
  • Hide counts until all Task pages are loaded because the current client projection does not expose a trusted total count in its ready state.
  • The full web test run needs one clean rerun in the merge round. The local perf run also needs a perf-VM rerun when its lock is free to make a reliable comparison with the baseline.
  • A failed initial local Playwright attempt printed a short-lived session cookie for the throwaway test account in its tool log. The value is not reproduced here; the test server was shut down during teardown.

cargo clean completed: Removed 8647 files, 7.3GiB total. No Rust files changed. No screenshots or build output were committed.

## Completed Branch: `job/tasksperf-1286` Head: `1f9ed1c5ed6920810dec0a7b072064b33c6ffe10` ### Changes - Board, List, and Matrix virtual rows now use batched ResizeObserver measurements. Heights are cached by Task ID and rendered width. The 152px estimate is used only before a row is measured. Matrix keyboard traversal uses the same measured offsets, and the Board keeps its 8px card spacing. - Board, sidebar Tag, and ARIA counts stay unknown while Task pages are still loading. They show exact totals when the Task Index is ready. - Added `test:e2e:tasksperf-1286`: it creates 1,000 real Notes-backed Tasks, holds later pages to check partial counts, and checks visible Task row/card intersections in Board, List, and Matrix at 390/820/1440 in light and dark with macOS platform emulation. - Refreshed 38 exact Task performance pins. The perf ledger reports 22,514 used pins, 0 remaining, and no added ratchet entries. Touched files: `TaskColumn.svelte`, `TaskMatrix.svelte`, `TaskList.svelte`, `TaskBoard.svelte`, `TasksSidebar.svelte`, `TasksWorkspace.svelte`, `task-virtual.ts`, `task-virtual.test.ts`, `apps/web/e2e/tasksperf-1286.mjs`, `apps/web/package.json`, `contracts/perf/adoption-1058.json`, `contracts/perf/exceptions.json`, and `contracts/perf/ratchet.json`. ### UX gaps closed - Wrapped titles and multiple Tags no longer overlap adjacent virtual rows at any tested width. - Partial page counts are hidden during loading; complete counts are exact. - The Board card spacing is restored. - The added geometry checks and ARIA set sizes follow measured row positions and count readiness. UX gaps left: none found in this regression scope. The visual screenshots are for the orchestrator's review; I did not judge their visual quality. Screenshots: `artifacts/tasksperf-1286/after-fix/` (18 width/layout/theme screenshots plus `1440-light-matrix-long-titles.png`; 19 files total). ### Verification `bun run check` — exit 0: ``` perf-lint: PASS; 0 violations; 22514 scoped exceptions svelte-check found 0 errors and 1 warning in 1 file ``` The warning is the existing empty CSS ruleset at `packages/ui/src/components/calendar/AttachmentDeck.svelte:1055`. `bun run build` — exit 0: ``` Compressed 917 static variants; saved 21506320 bytes. ``` `bun run test` — exit 1. Under shared-host load, 10 tests in 9 files timed out at Vitest's 5s limit: ``` Test Files 9 failed | 280 passed (289) Tests 10 failed | 2031 passed (2041) error: script "test" exited with code 1 ``` Isolated rerun of the 9 failed files: ``` Test Files 9 passed (9) Tests 71 passed (71) ``` After the final width-aware change, one existing Matrix keyboard test also timed out in a combined focused run; rerunning `TaskMatrix.svelte.test.ts` alone passed both tests, and `task-virtual.test.ts` passed in the combined run. Production-build Playwright run: `PASS`, no count or geometry failures and no browser runtime errors. The run captured all 19 screenshots above. ### Performance profile Repeated the 1,000- and 5,000-Task profile with 3 samples per route, Board/List/Matrix at 390px and 1440px, 0x and 4x CPU throttling, and a 24-client/12-second burst. The perf VM was held by `tabstorm-1338`, so this was measured locally using the debug server and production SPA. Local unthrottled route-ready p50 ranges were 0.913–2.200s at 1,000 Tasks and 0.938–2.460s at 5,000 Tasks. At 4x throttle they were 3.294–7.140s and 4.031–6.382s. Burst results: 1,000 Tasks: 686 requests, p50 376.7ms, p95 795.4ms, all HTTP 200, peak RSS 610,680,832 bytes. 5,000 Tasks: 639 requests, p50 461.8ms, p95 601.7ms, all HTTP 200, peak RSS 661,438,464 bytes. The 12-core local host load was 25.05/30.41/37.51 before and 29.58/29.18/35.65 after the 1,000-Task run; it was 27.20/28.64/35.30 before and 24.92/29.27/34.01 after the 5,000-Task run. These debug-server results under high load are diagnostic and cannot establish a direct comparison with the clean perf-VM baseline in `docs/perf/baseline.json`. The new geometry work costs O(N) to rebuild prefix offsets after a batch of measurements; ResizeObserver observes mounted rows only and batches measurements per frame. Profiles exercised N=1,000 and N=5,000. ### Decisions and known gaps - For variable card heights, cache the border-box height per stable Task ID and width; use the existing 152px estimate until measurement. This keeps offsets width-specific without layout reads in loops. - Hide counts until all Task pages are loaded because the current client projection does not expose a trusted total count in its ready state. - The full web test run needs one clean rerun in the merge round. The local perf run also needs a perf-VM rerun when its lock is free to make a reliable comparison with the baseline. - A failed initial local Playwright attempt printed a short-lived session cookie for the throwaway test account in its tool log. The value is not reproduced here; the test server was shut down during teardown. `cargo clean` completed: `Removed 8647 files, 7.3GiB total`. No Rust files changed. No screenshots or build output were committed.
Author
Owner

Starting #1286 on branch job/tasksperf-1286. Base is origin/dev at 3601719b80; current branch head is 1f9ed1c5ed. I am capturing the origin/dev Tasks layouts at 390/820/1440 before applying the reported layout fixes, then I will add geometry regressions and capture after-fix2 evidence.

Starting #1286 on branch job/tasksperf-1286. Base is origin/dev at 3601719b80a39f712daf07b37879b22929dfffa0; current branch head is 1f9ed1c5ed6920810dec0a7b072064b33c6ffe10. I am capturing the origin/dev Tasks layouts at 390/820/1440 before applying the reported layout fixes, then I will add geometry regressions and capture after-fix2 evidence.
Author
Owner

Baseline finding on origin/dev at 3601719b80: the 390px light List Tag overflow is pre-existing. In the production-build capture with 1,000 indexed Tasks, #area/operations-support ends at x=301.2 while its Tags cell ends at x=203.9 (97.3px outside); #project/performance-validation ends at x=342.0 (138.1px outside). The baseline screenshot is artifacts/tasksperf-1286/origin-dev/390-light-list.png.

Baseline finding on origin/dev at 3601719b80a39f712daf07b37879b22929dfffa0: the 390px light List Tag overflow is pre-existing. In the production-build capture with 1,000 indexed Tasks, `#area/operations-support` ends at x=301.2 while its Tags cell ends at x=203.9 (97.3px outside); `#project/performance-validation` ends at x=342.0 (138.1px outside). The baseline screenshot is `artifacts/tasksperf-1286/origin-dev/390-light-list.png`.
Author
Owner

Baseline Matrix findings on origin/dev at 3601719b80, with 1,000 indexed Tasks and the long-title fixture:

  • At 1440px after horizontal scroll, the first header cell and sticky first body cell differ by 17px on both edges; the other four header/body column edges match exactly. At 390px and 820px the first-column mismatch equals the Matrix scroll offset (650px and 245px in the light captures). The header/body desynchronisation is pre-existing. See artifacts/tasksperf-1286/origin-dev/1440-light-matrix-long-titles.png.
  • In the same 1440px Matrix capture, #area/operations-support ends 38.9px beyond its Task Card. That chip overflow is pre-existing. The 390px and 820px Matrix captures show the same overrun.
  • The matching origin/dev Board capture had zero Tag chip overflows at 390/820/1440. The reported Board chip overflow is therefore introduced by the job branch.
Baseline Matrix findings on origin/dev at 3601719b80a39f712daf07b37879b22929dfffa0, with 1,000 indexed Tasks and the long-title fixture: - At 1440px after horizontal scroll, the first header cell and sticky first body cell differ by 17px on both edges; the other four header/body column edges match exactly. At 390px and 820px the first-column mismatch equals the Matrix scroll offset (650px and 245px in the light captures). The header/body desynchronisation is pre-existing. See `artifacts/tasksperf-1286/origin-dev/1440-light-matrix-long-titles.png`. - In the same 1440px Matrix capture, `#area/operations-support` ends 38.9px beyond its Task Card. That chip overflow is pre-existing. The 390px and 820px Matrix captures show the same overrun. - The matching origin/dev Board capture had zero Tag chip overflows at 390/820/1440. The reported Board chip overflow is therefore introduced by the job branch.
Author
Owner

Tasks geometry fixes — #1286

Branch: job/tasksperf-1286
Base: origin/dev at 0419170fe (merged as 6110805e9)
Head: 98dc46d0514465d30f107f1dd20d1ea6981659ad

Built

  • Matrix header and body now use one horizontal scroll origin. The column edges remain aligned after horizontal scrolling.
  • Task Tags stay within their Board and Matrix cards and List cells. Visible labels ellipsize; the complete Tag stays in the accessible name and tooltip; the +N chip remains.
  • The phone List keeps its existing compact register layout and bounds the Tags cell so chips cannot overlap Status or row actions.
  • The Playwright regression checks Tag-chip containment, Matrix header/body edges (within 1 px) after horizontal scroll at 390, 820 and 1440 px, and phone viewport overflow.

Origin/dev comparison

The Matrix header/body desync, Matrix Tag spill and 390 px List overlap were present on origin/dev. The Board Tag spill appeared on this task branch. All four cases are fixed.

Screenshots

42 macOS-emulated production screenshots are attached below and remain in artifacts/tasksperf-1286/after-fix2/ in the worktree. They cover Board, List and Matrix at 390/820/1440 px in light and dark, plus many-long-Tag variants at all sizes/themes and long-title variants at 1440 px in both themes.

Standard views

Many long Tags

Long titles

Files

packages/ui/src/components/TagPill.svelte, packages/ui/README.md; Task views in apps/web/src/lib/tasks/ (Board, Card, Column, List, Matrix, Sidebar, Workspace, API and virtualizer modules/tests); Tag rows and sidebar components/tests; apps/web/src/lib/notes/NoteView.svelte; apps/web/e2e/task-checkbox-1229.mjs, apps/web/e2e/tasks-perf.mjs, apps/web/e2e/tasksperf-1286.mjs; apps/web/package.json; bench/tasks-ui/README.md; contracts/perf/adoption-1058.json, exceptions.json, ratchet.json, registry.json; docs/perf/baseline.json.

Gates

bun run check — exit 0. Exact result lines:

perf-lint: PASS; 0 violations; 22545 scoped exceptions
svelte-check found 0 errors and 1 warning in 1 file

The one warning is the existing empty .attachment-row:focus rule at packages/ui/src/components/calendar/AttachmentDeck.svelte:1055.

bun run test — exit 1 after 664.04s. 288 test files and 2,043 tests passed; two tests timed out at 5 seconds: NoteList.svelte.test.ts (Notes Delete/Undo) and AppPasswordsGroup.svelte.test.ts (device choice). Both files passed alone:

bunx vitest run src/lib/components/NoteList.svelte.test.ts --maxWorkers=1
Test Files 1 passed (1)
Tests 3 passed (3)
Duration 29.74s
bunx vitest run src/routes/settings/account/AppPasswordsGroup.svelte.test.ts --maxWorkers=1
Test Files 1 passed (1)
Tests 3 passed (3)
Duration 32.14s

Focused production Playwright run: PASS, zero geometry failures, 42 screenshots. node scripts/design-rules.test.mjs output:

design rules: no new shared primitive overrides or hand-rolled buttons (origin/dev baseline)

Performance and decisions

The visual fix adds CSS ellipsis and shares the Matrix scroll container. Its work is proportional to the Tags already rendered (O(k) for k visible chips); it adds no API requests, layout reads or observers. The existing #1286 profile in docs/perf/baseline.json covers 1,000 and 5,000 Tasks, including phone/desktop readiness and a 24-client burst. I did not rerun it because perf-run status showed the shared VM held by calternal tagperf for a 20-minute run; I did not start a competing measurement.

Decision: keep the DESIGN-mandated compact phone List and bound its Tags cell. No new product behavior was needed. The shared TagPill owns visible truncation, full accessible naming and the native tooltip.

UX gaps closed: Matrix columns now stay aligned; long Tag chips stay inside cards/cells; the phone List no longer spills; full Tag names remain available to screen readers and through the tooltip; +N remains visible. UX gaps left: none found in this scope.

Known verification gap: the two full-suite timeouts above passed in isolated reruns. The check warning is in merged AttachmentDeck.svelte, outside the Task files. No Rust code changed. No push or deploy was performed; the only merge was origin/dev into the job branch.

# Tasks geometry fixes — #1286 **Branch:** `job/tasksperf-1286` **Base:** `origin/dev` at `0419170fe` (merged as `6110805e9`) **Head:** `98dc46d0514465d30f107f1dd20d1ea6981659ad` ## Built - Matrix header and body now use one horizontal scroll origin. The column edges remain aligned after horizontal scrolling. - Task Tags stay within their Board and Matrix cards and List cells. Visible labels ellipsize; the complete Tag stays in the accessible name and tooltip; the `+N` chip remains. - The phone List keeps its existing compact register layout and bounds the Tags cell so chips cannot overlap Status or row actions. - The Playwright regression checks Tag-chip containment, Matrix header/body edges (within 1 px) after horizontal scroll at 390, 820 and 1440 px, and phone viewport overflow. ## Origin/dev comparison The Matrix header/body desync, Matrix Tag spill and 390 px List overlap were present on `origin/dev`. The Board Tag spill appeared on this task branch. All four cases are fixed. ## Screenshots 42 macOS-emulated production screenshots are attached below and remain in `artifacts/tasksperf-1286/after-fix2/` in the worktree. They cover Board, List and Matrix at 390/820/1440 px in light and dark, plus many-long-Tag variants at all sizes/themes and long-title variants at 1440 px in both themes. ### Standard views - **390 light:** [Board](https://git.kayg.org/attachments/7bffdb63-678d-4ce3-973e-339be8019601) · [List](https://git.kayg.org/attachments/7da046e8-17d8-42ad-b0f5-2d8f862c036c) · [Matrix](https://git.kayg.org/attachments/40ecafc2-dc7a-4e3a-86ba-b7f7d10da5dc) - **390 dark:** [Board](https://git.kayg.org/attachments/9a72ba1c-b817-4afa-b52c-4a9c2f34e241) · [List](https://git.kayg.org/attachments/639407ba-28e8-44e5-9e69-cabbb4bd5ab9) · [Matrix](https://git.kayg.org/attachments/ac0773ff-5774-4d1f-8b9b-707a89bbd4ef) - **820 light:** [Board](https://git.kayg.org/attachments/9bfe2098-6812-46d4-8007-9c19a697a584) · [List](https://git.kayg.org/attachments/8b9c576c-4cb8-46bf-b141-f682304c69bf) · [Matrix](https://git.kayg.org/attachments/1652b33e-324c-48ae-ad34-d347281ca504) - **820 dark:** [Board](https://git.kayg.org/attachments/9838d44e-a358-4ab8-bf95-ea2050bed599) · [List](https://git.kayg.org/attachments/7870f733-e182-40bf-ae45-c429cfcf3128) · [Matrix](https://git.kayg.org/attachments/0dba1fd5-7392-4fbe-880c-52a6fcdb5daa) - **1440 light:** [Board](https://git.kayg.org/attachments/6c483f83-01d0-43e3-9eac-d9af39437863) · [List](https://git.kayg.org/attachments/607566e7-4066-4f09-9096-0c43ab11b8c2) · [Matrix](https://git.kayg.org/attachments/44995fa2-0ecd-43fa-b0fd-450b6c944fda) - **1440 dark:** [Board](https://git.kayg.org/attachments/855a2f23-f073-4ccd-a46d-fa17c7efeec7) · [List](https://git.kayg.org/attachments/f1c2956b-77a9-4c55-9ece-4f7f724f4cd7) · [Matrix](https://git.kayg.org/attachments/ad3addb2-ebcf-4800-9b04-f6fc62bed5c2) ### Many long Tags - **390 light:** [Board](https://git.kayg.org/attachments/ffa403c3-537b-48ab-a1c6-d8b5b8ffc4c3) · [List](https://git.kayg.org/attachments/3af239c5-d7df-4369-99e0-6268c6b0dc3c) · [Matrix](https://git.kayg.org/attachments/a729a1b1-fbbf-4f20-b0d3-8a2911b442d6) - **390 dark:** [Board](https://git.kayg.org/attachments/7facea64-8bd4-436e-9f8a-1361cbf31236) · [List](https://git.kayg.org/attachments/7a926d87-56b9-4874-a4f2-de8bfca1dc4c) · [Matrix](https://git.kayg.org/attachments/67950129-55d0-4266-b901-ee2bf13c502d) - **820 light:** [Board](https://git.kayg.org/attachments/38858d93-684e-4b82-9118-ce4156c70781) · [List](https://git.kayg.org/attachments/71f2e76a-6a82-4664-9ae2-cbff0ca36e46) · [Matrix](https://git.kayg.org/attachments/c8c26268-5d11-4ef2-ba2e-9caad6478a1b) - **820 dark:** [Board](https://git.kayg.org/attachments/15e103f0-45f5-42b6-ad11-cdd2f18c3ea1) · [List](https://git.kayg.org/attachments/27b572ad-bf91-498f-b592-34c8a77205d9) · [Matrix](https://git.kayg.org/attachments/0906dd9e-65cc-4a45-8e69-4b7f7f9381a2) - **1440 light:** [Board](https://git.kayg.org/attachments/2fc9b39c-364d-45a8-a81a-b3533e336085) · [List](https://git.kayg.org/attachments/71972ea5-bf07-4202-bdff-9c3d2e21898a) · [Matrix](https://git.kayg.org/attachments/86409c93-c8f3-4453-a52b-ad4dcb6936a0) - **1440 dark:** [Board](https://git.kayg.org/attachments/7b6b44b5-a09b-47d4-bad1-3358183e03d0) · [List](https://git.kayg.org/attachments/91531adf-dfd8-48a5-b68f-e6ed8511322c) · [Matrix](https://git.kayg.org/attachments/e27aab81-d15e-4759-940b-74cacf411f4e) ### Long titles - **1440 light:** [Board](https://git.kayg.org/attachments/bb422e47-9b09-40f1-9482-ab07e7784154) · [List](https://git.kayg.org/attachments/209991d3-5e7c-4297-bc14-7ade4aabe886) · [Matrix](https://git.kayg.org/attachments/d73e33de-4326-42e9-a490-8209135a0e51) - **1440 dark:** [Board](https://git.kayg.org/attachments/30c879c3-38ae-4e46-ae82-48e2281f6056) · [List](https://git.kayg.org/attachments/b36bf17f-214e-4375-a130-7c241b479aa1) · [Matrix](https://git.kayg.org/attachments/79460e5b-250f-41f5-944b-fb1ee0ae19cc) ## Files `packages/ui/src/components/TagPill.svelte`, `packages/ui/README.md`; Task views in `apps/web/src/lib/tasks/` (Board, Card, Column, List, Matrix, Sidebar, Workspace, API and virtualizer modules/tests); Tag rows and sidebar components/tests; `apps/web/src/lib/notes/NoteView.svelte`; `apps/web/e2e/task-checkbox-1229.mjs`, `apps/web/e2e/tasks-perf.mjs`, `apps/web/e2e/tasksperf-1286.mjs`; `apps/web/package.json`; `bench/tasks-ui/README.md`; `contracts/perf/adoption-1058.json`, `exceptions.json`, `ratchet.json`, `registry.json`; `docs/perf/baseline.json`. ## Gates `bun run check` — **exit 0**. Exact result lines: ``` perf-lint: PASS; 0 violations; 22545 scoped exceptions svelte-check found 0 errors and 1 warning in 1 file ``` The one warning is the existing empty `.attachment-row:focus` rule at `packages/ui/src/components/calendar/AttachmentDeck.svelte:1055`. `bun run test` — **exit 1 after 664.04s**. 288 test files and 2,043 tests passed; two tests timed out at 5 seconds: `NoteList.svelte.test.ts` (Notes Delete/Undo) and `AppPasswordsGroup.svelte.test.ts` (device choice). Both files passed alone: ``` bunx vitest run src/lib/components/NoteList.svelte.test.ts --maxWorkers=1 Test Files 1 passed (1) Tests 3 passed (3) Duration 29.74s ``` ``` bunx vitest run src/routes/settings/account/AppPasswordsGroup.svelte.test.ts --maxWorkers=1 Test Files 1 passed (1) Tests 3 passed (3) Duration 32.14s ``` Focused production Playwright run: **PASS**, zero geometry failures, 42 screenshots. `node scripts/design-rules.test.mjs` output: ``` design rules: no new shared primitive overrides or hand-rolled buttons (origin/dev baseline) ``` ## Performance and decisions The visual fix adds CSS ellipsis and shares the Matrix scroll container. Its work is proportional to the Tags already rendered (`O(k)` for `k` visible chips); it adds no API requests, layout reads or observers. The existing #1286 profile in `docs/perf/baseline.json` covers 1,000 and 5,000 Tasks, including phone/desktop readiness and a 24-client burst. I did not rerun it because `perf-run status` showed the shared VM held by `calternal tagperf` for a 20-minute run; I did not start a competing measurement. Decision: keep the DESIGN-mandated compact phone List and bound its Tags cell. No new product behavior was needed. The shared `TagPill` owns visible truncation, full accessible naming and the native tooltip. **UX gaps closed:** Matrix columns now stay aligned; long Tag chips stay inside cards/cells; the phone List no longer spills; full Tag names remain available to screen readers and through the tooltip; `+N` remains visible. **UX gaps left:** none found in this scope. **Known verification gap:** the two full-suite timeouts above passed in isolated reruns. The check warning is in merged `AttachmentDeck.svelte`, outside the Task files. No Rust code changed. No push or deploy was performed; the only merge was `origin/dev` into the job branch.
Author
Owner

Deployed to calternal.cloud in merge round 34 (d395b6e25), 2026-10-08 05:03 IST. Backup: /srv/calternal/backup-pre-m34-20261008-0132. Production healthy after 36 s; served build matches.

Deployed to calternal.cloud in merge round 34 (d395b6e25), 2026-10-08 05:03 IST. Backup: /srv/calternal/backup-pre-m34-20261008-0132. Production healthy after 36 s; served build matches.
kayg closed this issue 2026-10-07 23:54:28 +00:00
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#1286
No description provided.