From 375ca12b058009765596574a2805e3eeebddd3fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 22 Oct 2026 05:38:36 +0200 Subject: [PATCH] perf(bench): add a large-account seed and a request benchmark MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit scripts/bench/seed-large-account.ts (moved from scripts/, extended) writes one synthetic account of ten years: 1,265,561 measurement rows across the main types and five sources (Apple Health dense spot rows, Withings blood pressure and scale, a weekly manual weight, three years of Oura nightly values, 90 days of Nightscout CGM), `stats:` and per-sample cumulative rows, nightly sleep stages, 3,020 workouts, 1,095 mood entries and three medications with 3,285 intake events. Everything goes in through set-based INSERT … SELECT generate_series; values and timestamps are functions of the row index and `--anchor`, so a run is reproducible. The rollup tier is folded with recomputeUserRollups, as the boot backfill does, and the tables are analysed. About 80 s on the Mac mini. scripts/bench/run.ts boots the production build once with every role (so the queues exist), then measures against a web-only process under the production heap ceiling: p50, p95 and max latency and SQL statements per request (pg_stat_statements, read once nothing is in flight and the count holds still) for /api/auth/me, the dashboard summary and snapshot, the pulse series over 30, 365 and 3650 days, the day view, insights health status and correlations, each cold (caches evicted by a one-row manual-sync ingest before every request) and warm, plus a 5,000-row batch ingest as ten requests of 500. It writes JSON and a markdown table. docs/contributing/benchmarks.md describes the setup against a throwaway Postgres in Docker with the production pg_stat_statements settings. --- docs/README.md | 3 + docs/contributing/benchmarks.md | 83 +++ scripts/README.md | 1 + scripts/bench/run.ts | 478 ++++++++++++++ scripts/bench/seed-large-account.ts | 659 +++++++++++++++++++ scripts/seed-large-account.ts | 296 --------- tests/integration/large-account-jobs.test.ts | 2 +- 7 files changed, 1225 insertions(+), 297 deletions(-) create mode 100644 docs/contributing/benchmarks.md create mode 100644 scripts/bench/run.ts create mode 100644 scripts/bench/seed-large-account.ts delete mode 100644 scripts/seed-large-account.ts diff --git a/docs/README.md b/docs/README.md index aabff21cc..5ade54148 100644 --- a/docs/README.md +++ b/docs/README.md @@ -59,6 +59,9 @@ notes, and machine-readable specs that ship alongside the source: - [`contributing/ui-standards.md`](./contributing/ui-standards.md) — the binding UI standards: card anatomy, spacing, text colour, tokens and which primitive to use where. Read before touching any surface. +- [`contributing/benchmarks.md`](./contributing/benchmarks.md) — the + large-account benchmark (latency and statements per request) and the + statement budgets on the hot routes. - [`ui-guidelines.md`](./ui-guidelines.md) — UI / a11y house style; where it disagrees with the UI standards, the standards win. diff --git a/docs/contributing/benchmarks.md b/docs/contributing/benchmarks.md new file mode 100644 index 000000000..62b16f33f --- /dev/null +++ b/docs/contributing/benchmarks.md @@ -0,0 +1,83 @@ +# Benchmarks + +Two tools measure the server's read and write paths. Use them before and after any change to a hot path, and put both numbers in the PR. + +- **`scripts/bench/run.ts`** measures latency (p50, p95, max) and SQL statements per request against a seeded account of about 1.26 million measurement rows, through the production build. It is a local tool, not a CI gate: latency depends on the machine. +- **`tests/integration/query-budget-hot-routes.test.ts`** holds a statement budget per hot route on a small fixture. Statement counts are exact on a fixed fixture, so this one is a gate: a change that adds a query per row, or a second read of something already loaded, fails it. + +## The large-account benchmark + +### 1. A throwaway Postgres with pg_stat_statements + +Never point the seed or the benchmark at a database that holds real data. They write under a synthetic user, and the seed's `--reset` deletes that user with everything it owns. + +```sh +docker run -d --name hl-bench-db --shm-size=256m \ + -e POSTGRES_USER=bench -e POSTGRES_PASSWORD=bench -e POSTGRES_DB=healthlog_bench \ + -p 55432:5432 postgres:16.15-alpine \ + postgres -c shared_preload_libraries=pg_stat_statements -c pg_stat_statements.track=top +``` + +The image and the two settings are the ones `docker-compose.yml` runs in production. + +### 2. Environment, migrations, seed + +```sh +export DATABASE_URL="postgresql://bench:bench@127.0.0.1:55432/healthlog_bench?schema=public" +export ENCRYPTION_KEYS='{"v1":"<64 hex>"}' ENCRYPTION_ACTIVE_KEY_ID=v1 +export API_TOKEN_HMAC_KEY="<64 hex>" + +pnpm db:migrate:deploy +pnpm dlx tsx scripts/bench/seed-large-account.ts --reset --anchor 2026-10-22 +``` + +Any throwaway key values work, as long as the seed, the benchmark and the server see the same ones. + +The seed writes ten years for one user: Apple Health dense rows (heart rate about every ten minutes, HRV, SpO2, respiratory rate, gait, audio), Withings blood pressure, pulse and scale, a weekly manual weight, three years of Oura resting heart rate and RMSSD, 90 days of Nightscout CGM, `stats:` and per-sample cumulative rows, nightly sleep stages, about 3,000 workouts, three years of mood and of three medications with their intake events. It then folds the rollup tier the way the boot backfill does and runs `ANALYZE`. It takes about 80 seconds on the Mac mini. + +The seed is deterministic for a given `--anchor`: values and timestamps are functions of the row index and that day. Without `--anchor` it anchors on today, which is what you want when the benchmark should see recent data. + +### 3. A production build + +```sh +pnpm build +``` + +### 4. Run + +```sh +pnpm dlx tsx scripts/bench/run.ts --out scripts/bench/results/