perf(api): issue summary counts concurrently#1418
Merged
Merged
Conversation
`SummaryService.getSummary` backs the dashboard, the first page a user sees after logging in. It issued 20 count queries, each awaited before the next was sent, even though none of them depends on another. Total latency was therefore the sum of 20 round trips rather than the slowest one. Issue them concurrently with `Promise.all`. Measured against a real MongoDB replica set with 5,000 subjects / 50,400 sessions / 204,800 instrument records, median of 9 runs: GET /v1/summary 278 ms -> 69 ms (4.0x) GET /v1/summary?groupId=<g> 385 ms -> 88 ms (4.4x) The response is unchanged: both versions were diffed against the same database and are identical once the trend timestamps are normalised. The only behavioural difference is that `Date.now()` is now read before the counts run rather than after, so trend timestamps no longer drift by however long the first five queries took. All three trends already shared one reading; they now share one taken at a consistent point. Adds a spec for the module, which had none. The concurrency test holds every count open until all 20 have been called, so it fails (by timing out) against the sequential implementation rather than passing vacuously. Refs #1409 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bfen9VQemZanJLXinJ6MMG
Contributor
There was a problem hiding this comment.
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
joshunrau
approved these changes
Jul 23, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #1409.
SummaryService.getSummarybacks the dashboard — the first page a user sees after logging in. It issued 20 count queries, eachawaited before the next was sent, even though none of them depends on another. Latency was therefore the sum of 20 round trips rather than the slowest one.This issues them concurrently with
Promise.all.Measured effect
Against a real MongoDB 8 replica set with a database provisioned through the app's own
POST /v1/setupand grown to 5,000 subjects / 50,400 sessions / 204,800 instrument records (112 MiB). Median of 9 runs, warm cache, same database for both branches:mainGET /v1/summaryGET /v1/summary?groupId=<g>The response is unchanged
Both versions were run against the same database and their payloads diffed. Once the trend timestamps are normalised (they are wall-clock values, so they differ between any two runs), the responses are byte-identical:
Behavioural note
One thing did change, deliberately:
Date.now()is now read before the counts run rather than after the first five complete. Previously the trend timestamps were offset by however long those queries happened to take — ~600 ms on the instance above, and growing with the size of the deployment. All three trends already shared a single reading; they now share one taken at a consistent point. The trend buckets are 30 days wide, so this is not observable in the dashboard.Tests
The summary module had no spec; this adds one (6 tests) covering the counts, the trend shape and ordering, timestamp/value pairing, group scoping, and concurrency.
The concurrency test is the one that matters, and it is written so it cannot pass vacuously: it holds every mocked count open until all 20 have been called. If any query waits on the result of another, that number is never reached and the test times out. Verified against the previous implementation:
and against this branch:
Checks
tsc --noEmitonapps/api— cleaneslint src/summary— cleanprettier --check src/summary— cleanvitest --project api— 105 passed, 1 failedThe one failure is
instrument-records.service.spec.ts > upload > should create records with pending set, which fails identically on pristinemainand is unrelated to this change.Scope
Deliberately limited to the parallelisation, which #1409 identifies as the high-value part. The other items in that issue are left for separate PRs:
instrumentsService.count()being implemented asfind().lengthis Performance: instrument bundles are fetched from MongoDB on every hot-path request #1408's territory. On this instance the instrument load measured ~30 ms of the endpoint against ~590 ms for the sequential counts, so it is a much smaller term.🤖 Generated with Claude Code
https://claude.ai/code/session_01Bfen9VQemZanJLXinJ6MMG