Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
docs/performance/**/*.json linguist-generated=true
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ agent-audit-report.json
/scripts/*
!/scripts/revoke-legacy-health-tokens.mjs
!/scripts/benchmark-performance.ts
!/scripts/benchmark-browser.cjs
!/scripts/verify-duel-performance.cjs
!/scripts/verify-entry-performance.cjs
# scratch & local cache
/.scratch/
supabase/.temp/
40 changes: 40 additions & 0 deletions docs/performance.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,45 @@
# App loading and interaction performance

## 2026-10-12 follow-up

Compared production builds against `e7b284e` with Chrome 148 on the same machine. The local fixture has 730 prior days across 20 seeded Habits: 14,600 Check-ins, about 2.3 MB. These results measure local client work. The mobile scenario uses a 390×844 viewport and 4× CPU throttling; it does not represent a physical phone or hosted Supabase latency.

The first open of a deferred view waited about 300 ms after its JavaScript was available. The installed React DOM production runtime throttles Suspense fallback replacement. Deferred views now use a guarded import resource and stable external-store snapshots, keeping the deployment check before activation. Tab and Settings imports also warm on pointer entry or keyboard focus. The initial page still loads its existing frame and Today view.

The same loader also removes the remaining delay from default account entry and first-run onboarding. With a browser-local heading probe after document navigation, account entry falls from 379.7 to 80.6 ms and onboarding from 496.9 to 197.1 ms (five samples per flow, mock requests at 50 ms). Signup/sign-in switching and saving a first habit plan pass. The original locator-based timing was superseded because retry polling added a fixed delay to the observation.

First-open medians, five samples per view, document reload before every sample, HTTP cache retained, and two animation frames after content becomes ready:

| View | Before | After |
| --- | ---: | ---: |
| Progress | 331.3 ms | 53.6 ms |
| Duel | 331.3 ms | 44.8 ms |
| Settings | 334.8 ms | 41.3 ms |
| Activity | 333.4 ms | 40.2 ms |
| Attach Proof | 350.2 ms | 39.1 ms |
| Shortcuts | 352.9 ms | 48.3 ms |

Repeated interactions alternate build order, with 20 Check-in/undo pairs per build and scenario. Timing includes the real state update, one observed synchronous localStorage write, and two animation frames. The saved state is parsed after timing:

| Scenario | Action | Before | After |
| --- | --- | ---: | ---: |
| Desktop | Weekly Check-in | 48.6 ms | 41.4 ms |
| Desktop | Undo | 53.3 ms | 45.4 ms |
| Mobile viewport, 4× CPU | Weekly Check-in | 196.3 ms | 172.0 ms |
| Mobile viewport, 4× CPU | Undo | 149.8 ms | 123.1 ms |

Today streaks reuse the existing Check-in index, selected-day sums stay memoized, and badges are computed when the Trophy Cabinet mounts. Weekly Duel totals aggregate history once. Activity parses each timestamp once before sorting and skips search-string construction for an empty query, while retaining full-history search, counts, stable ties, and backdated records. Local persistence remains synchronous.

Production copies configured for a localhost Supabase mock verify the actual client, store effect, Phoenix subscriptions, and paginated reads. With 2,500 Check-ins, a burst of 20 realtime events drops from 140 requests to 7; exact-count requests drop from 140 to 5. Events during an active read produce one serialized follow-up. A 400-row server cap still returns all 2,500 records, with 11 requests and 5 exact counts per reload. These request counts describe the mock, not hosted latency. When a total is missing or nonnumeric, pagination continues through the empty page so a short server-capped response cannot truncate history.

That verification reproduced a read/write race on the original build: a read started before a local Check-in briefly replaced its optimistic state. The store now rejects reads when the pending-write queue changes during loading and requests a fresh read. The same browser reproduction keeps the Habit checked throughout. Initial loading, Check-in/undo writes, Activity counts, and isolation from local history also pass in the mock.

`npm run check` passes 165 tests, typecheck, and lint; `npm run build` passes. Browser checks cover photo upload and undo, proof gallery, historical calendar navigation and Today reset, shortcut focus trapping and cleanup, light/dark themes, offline Check-in/undo and warmed views, and persisted history after reload. The measured desktop and mobile viewports have no horizontal overflow. Deployment mismatch, recovery reload limits, unavailable version endpoints, offline activation, duplicate records, rest days, and streak caps have focused tests.

Reproduction commands and raw measurements are in [the October 12 evidence directory](performance/2026-10-12/README.md). Run `npx tsx scripts/benchmark-performance.ts` for calculation medians, including shared-index construction and all active Today streaks. The calculation benchmark is separate from the browser timings above.

## 2026-10-10 pass

Measured against main at `779dd85` on 2026-10-10, using production builds, Node 20.20.2 and Chrome on the same machine. The fixture contains 730 days for all 20 seeded habits, or 14,600 check-ins. These local measurements isolate client work; they do not measure authenticated Supabase latency or mobile hardware.

Run the calculation benchmark with `npx tsx scripts/benchmark-performance.ts`. Each result is the median of five runs after one warm-up. Before the change, the weekly check-in diff used the store's `find` for each rebalanced record. The new benchmark uses the store's positional comparison, since rebalancing preserves array order.
Expand Down
79 changes: 79 additions & 0 deletions docs/performance/2026-10-12/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# October 12 performance evidence

Baseline application source is `e7b284e`. Measurements use production builds and synthetic history in isolated browser contexts. The current source includes guarded deferred views, indexed streaks, coalesced Duel reloads, and rejection of reads that overlap local writes.

## Reproduce local browser measurements

Serve the baseline and current production builds on separate localhost ports, with Supabase environment variables unset. Keep the current checkout on its branch. For the baseline, extract a source snapshot with `git archive e7b284e`; run `npm ci`, `npm run build`, then `npm run start -- --port 43127` in that snapshot. Run the current build on port 43130. The scripts accept different URLs with `--baseline` and `--current`.

Install browser tooling outside the app's dependencies:

```sh
npm install --prefix .scratch/browser-tools playwright
.scratch/browser-tools/node_modules/.bin/playwright install chromium
```

Run from the repository root:

```sh
NODE_PATH="$PWD/.scratch/browser-tools/node_modules" node scripts/benchmark-browser.cjs --mode first-open --samples 5 --output .scratch/first-open.json
NODE_PATH="$PWD/.scratch/browser-tools/node_modules" node scripts/benchmark-browser.cjs --mode interactions --samples 20 --output .scratch/interactions.json
NODE_PATH="$PWD/.scratch/browser-tools/node_modules" node scripts/benchmark-browser.cjs --mode interactions --mobile --cpu 4 --samples 20 --output .scratch/interactions-mobile.json
NODE_PATH="$PWD/.scratch/browser-tools/node_modules" node scripts/benchmark-browser.cjs --mode offline --output .scratch/offline.json
```

Use `--chrome /path/to/chrome` to select an installed browser. This run used `/usr/bin/google-chrome`, version 148.0.7778.178. Timing ends after actual content or persistence and two animation frames. First-open samples reload the document but retain the HTTP cache. Interaction samples alternate build order and parse the saved state after timing. The mobile option changes the viewport; CPU throttling is synthetic.

## Reproduce Duel reload and write-race checks

Use separate baseline/current production snapshots configured for the mock below. They must not share the anonymous builds used above, because public environment variables are embedded during build:

```sh
NEXT_PUBLIC_SUPABASE_URL=http://127.0.0.1:54321 NEXT_PUBLIC_SUPABASE_ANON_KEY=mock-publishable-key npm run build
NEXT_PUBLIC_SUPABASE_URL=http://127.0.0.1:54321 NEXT_PUBLIC_SUPABASE_ANON_KEY=mock-publishable-key npm run start -- --port 43131
```

Serve the corresponding current snapshot on port 43132. No Supabase server is required: Playwright handles the localhost HTTP responses and Phoenix WebSocket protocol. The fixture contains 2,500 Check-ins; responses wait 50 ms. `npm test` in the repository root creates the compiled seed module used by the harness.

```sh
npm test
NODE_PATH="$PWD/.scratch/browser-tools/node_modules" node scripts/verify-duel-performance.cjs --page-cap 1000 --output .scratch/duel-reload.json
NODE_PATH="$PWD/.scratch/browser-tools/node_modules" node scripts/verify-duel-performance.cjs --page-cap 400 --output .scratch/duel-cap400.json
NODE_PATH="$PWD/.scratch/browser-tools/node_modules" node scripts/verify-duel-performance.cjs --race --output .scratch/duel-race.json
NODE_PATH="$PWD/.scratch/browser-tools/node_modules" node scripts/verify-duel-performance.cjs --loading-only --history-days 730 --samples 5 --page-cap 400 --output .scratch/duel-loading-cap400.json
NODE_PATH="$PWD/.scratch/browser-tools/node_modules" node scripts/verify-entry-performance.cjs --smoke --output .scratch/entry-performance.json
```

The reload checks assert full history, counts, filtered queries, twenty-event bursts, one follow-up for events during loading, actual Check-in/undo writes, and unchanged local history. The race check observes the Habit button while a read overlaps a local write: the baseline becomes unchecked temporarily; the current build remains checked. These tests verify client behavior and request amplification against a mock. They do not establish hosted database latency or server-side authorization.

Known-count history now loads in batches of at most three requests. Responses retain their requested order; a changed page size discards later speculative responses and resumes serial reads from the contiguous prefix. Unknown totals remain serial through an empty page. The loading-only scenario uses 14,600 records and five alternating samples in fresh browser contexts. Its browser-local probe ends when Daily Ritual and the Check-in control appear, followed by two animation frames; Activity then verifies the complete record count. This includes localhost JavaScript loading and rendering with 50 ms mock responses.

The entry check uses empty auth for the default account screen, then a synthetic account with an empty Duel for first-run onboarding. It measures the heading with a browser-local probe and two animation frames after document navigation. `--smoke` also switches signup/sign-in modes and saves a new habit plan through the mock. No real account is created.

## Recorded artifacts

- [First opens](first-open.json): five samples per view and build from `benchmark-browser.cjs`.
- [First opens with a mobile viewport and 4× CPU](first-open-mobile.json): five samples per view and build under synthetic throttling.
- [Repeated interactions](interactions.json): twenty alternating Check-in/undo pairs per build, desktop and synthetic mobile scenarios.
- [Offline](offline.json): local mutation/undo, warmed views, and persisted history after reconnect/reload.
- [Duel reloads](duel-reload.json): pagination and request counts with a 1,000-row page cap.
- [Duel reloads with a 400-row cap](duel-cap400.json): no history truncation with a lower server limit.
- [Read/write race](duel-race.json): the same UI reproduction on baseline and current source.
- [Account entry and onboarding](entry-performance.json): five navigation samples per flow and build, plus mode switching and a saved first plan.
- [Large authenticated history loading](duel-loading-cap400.json): five samples per build, full 14,600-record history, and a 400-row server cap.

Final source `69d22fe` passes 170 tests, type checking, lint, and anonymous/configured production builds. The final loading repeat measures medians of 2,365.4 ms before and 1,018.5 ms after. Final Duel fixtures also retain 2,500 records under both caps, reduce normal bursts from 140 to 7 requests and capped bursts from 220 to 11, and keep optimistic Check-ins checked during an overlapping read.

## Independent review and remaining work

[Standards](standards-review.md) and [Spec](spec-review.md) independently review source `69d22fe`. The [final trail review](trail-review.md) extends the [earlier trail review](trail-review-before-paging.md). Reports are retained with their original findings; the parent responses below explain the disposition.

No hard standards or demonstrated correctness failures were reported. Optional code cleanup remains deferred. Two independent deferred wrappers share the JavaScript module cache; moving a wrapper into the imported modal would need care to preserve its lazy boundary. Pointer/focus warming directly supports the authorized loading goal and retains the deployment guard. The removed Apple Health poll always received HTTP 410 at baseline and exited before changing state; functioning wearable paths remain intact.

Hosted database/RLS and physical mobile testing remain unavailable. Mobile measurements use emulation. In-flight read cancellation and further calculation proposals remain follow-up work; their suggested savings are unimplemented estimates. Existing offset pagination does not promise a consistent database snapshot during concurrent remote inserts or deletions.

The final trail reviewer checked the refreshed browser artifact but attributed its newer medians to historical row 53. That row records the earlier 2,385.8→1,034.4 ms sample; row 55 records the final 2,365.4→1,018.5 ms repeat. The append-only trail preserves both measurements. Documentation packaging and final handoff actions follow the source review boundary.

Earlier instrumentation that polled and parsed the entire stored state inside the timed interaction was superseded. The accepted interaction artifact observes one real storage write and parses its value after timing. The mock also exposes `Content-Range` to the browser and waits for quiescence across pagination gaps. These corrections are recorded in the decision trail.

The [decision trail](decisions.tsv) records accepted and abandoned work. Earlier diagnosis logs and quota-interrupted child reports remain local under `.scratch/performance-overnight`; the accepted measurements above and their reusable harnesses are committed here.
Loading
Loading