Repository navigation
Conversation
computeBuckets() rescanned the whole event list once per day, and cloned it
once per day on top of that:
const dayEvents = events.filter(...) // full scan
const lastBeforeDay = [...events] // full clone
.filter((e) => e.timestamp < dayStartSec) // full scan
.at(-1); // ...to read one element
With days up to 365 (the schema's max) that is three full traversals and one
full array allocation per day - the clone existing only to fetch its last
element. A third array pair was allocated per day to build `allStatuses` just
to run `.every()` over it.
The events already arrive ordered by timestamp ascending, and the day windows
advance monotonically, so one forward cursor yields each day's slice and the
carried-in status without any rescanning. The `allStatuses` check is done in
place with an early exit.
getBatchedStatusHistory() separately grouped events with
`events.filter(...)` and `lastKnownEvents.find(...)` per entity, i.e.
O(entities x events). That is now a single grouping pass into Maps.
This is synchronous work on the request path, so it is time the process
cannot serve anything else.
Measured (median of repeated runs):
computeBuckets, one entity
days events before after speedup
30 500 0.63ms 0.12ms 5.1x
90 2000 4.35ms 0.13ms 33.2x
365 5000 17.78ms 0.49ms 36.4x
365 20000 122.92ms 1.38ms 89.2x
batched grouping (300 events/entity)
entities events before after speedup
10 3000 0.29ms 0.33ms 0.9x
50 15000 6.60ms 0.20ms 33.2x
200 60000 108.86ms 1.27ms 85.4x
The 10-entity grouping row is a wash - Map construction costs about what the
scan did at that size; the win starts around 50 entities.
Output is unchanged: the new implementation was fuzzed against the original
over 3006 generated workloads (varying days, event counts, event density,
prior status and timezone offset) plus explicit edge cases, with byte
identical results.
Adds server/lib/statusHistory.test.ts covering the properties the cursor has
to preserve - notably that a day inherits the status of the newest event from
any earlier day, which is the part a naive cursor gets wrong.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
aithal007
requested review from
miloschwartz and
oschwartz10612
as code owners
September 22, 2026 12:51
Contributor
There was a problem hiding this comment.
Copilot review overview
🟢 Approval recommended
The optimized implementations preserve ordering and carry-forward semantics, and the added tests cover the key regression cases.
Review effort: Lite
Findings: None
What changed in this PR
Optimizes status-history bucket computation and batched entity grouping while preserving existing output behavior.
Changes:
- Replaces per-day event rescans with a forward cursor.
- Avoids temporary status arrays during unknown-status checks.
- Groups batched events and prior states in linear passes.
- Adds focused regression tests for carry-forward behavior.
| File | Description |
|---|---|
server/lib/statusHistory.ts |
Implements cursor-based bucketing and map-based grouping. |
server/lib/statusHistory.test.ts |
Adds computeBuckets behavior tests. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
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.
Community Contribution License Agreement
By creating this pull request, I grant the project maintainers an unlimited,
perpetual license to use, modify, and redistribute these contributions under any terms they
choose, including both the AGPLv3 and the Fossorial Commercial license terms. I
represent that I have the right to grant this license for all contributed content.
AI Disclosure
Claude Code (Anthropic) was used to write the rewrite, the tests and the benchmarks, and to fuzz the new implementation against the old one. I directed the work and verified the results myself — including catching a carry-forward bug in my own first attempt, and confirming the new tests actually fail when that bug is reintroduced.
Description
Summary
computeBuckets()inserver/lib/statusHistory.tsbuilds one bucket per day for the status-history endpoints. For each day it rescanned the entire event list — and cloned it:That's three full traversals and one full array allocation per day, with
daysgoing up to 365 (the schema's own max). The clone exists solely to read its last element. A further pair of arrays was allocated per day to buildallStatusesjust to call.every()on it.Separately,
getBatchedStatusHistory()grouped events per entity withevents.filter(...)andlastKnownEvents.find(...)inside a loop over entities — O(entities × events).What changed
The events already arrive ordered by
timestampascending (the queries feeding this use.orderBy(asc(statusHistory.timestamp))), and the day windows advance monotonically. So a single forward cursor yields each day's slice and the carried-in status, with no rescanning and no per-day clone. TheallStatusescheck became an in-place scan with an early exit, and the batched grouping became a single pass intoMaps.No behaviour change — this is purely how the same result is computed.
Impact
This is synchronous work on the request path, so it's time the process can't serve anything else.
computeBuckets, single entity (median of repeated runs):Batched grouping (300 events per entity):
Honest note: the 10-entity grouping row is a wash — building the
Maps costs about what the scan did at that size. The win starts around 50 entities. ThecomputeBucketsimprovement applies at every size.Org dashboard end-to-end (N entities × 90 days × 300 events each,
computeBucketsper entity): 10 entities 9.1 ms → 4.2 ms, 50 entities 42.1 ms → 11.3 ms, 200 entities 163.4 ms → 41.9 ms.Verification
Output equivalence — 3,006 generated workloads, zero differences. I kept a verbatim copy of the original implementation and fuzzed the two against each other over randomized combinations of
days(1–90), event count (0–200), event density,priorStatus, andtzOffsetMinutes(including-720/+840extremes), comparing fullJSON.stringifyoutput. Plus explicit edge cases: empty events, empty events with a prior status, all-unknownevents, a single event,days=1, anddays=365with sparse events.New tests (
server/lib/statusHistory.test.ts) pin the properties the cursor must preserve — most importantly that a day inherits the status of the newest event from any earlier day. That's the part a naive cursor gets wrong: my own first attempt only carried forward events from before the current day and not the current day's own trailing event. I verified the test catches it by reintroducing that bug — it fails withExpected: bad, Actual: no_data.How to test?
npx tsx server/lib/statusHistory.test.ts— all assertions pass.npx tsc --noEmit— clean.npx eslint server/lib/statusHistory.ts— clean.🤖 Generated with Claude Code