perf(measurements): fold each source before picking the canonical one

The live daily fold ranked every row by source before folding, which sorted
the whole window twice. A year of per-minute heart rate (525,600 rows) took
about 0.55 s per series on Postgres 16, with both sorts spilling to disk.

The fold now aggregates per source and bucket in one hashed pass and then
keeps each bucket's ladder-canonical source. Folding the canonical source's
rows is the same as folding that source's group, so the numbers do not
change; the same year reads in about 0.29 s. A new integration case pins the
canonical pick, mean, count and spread for a day with three sources.
This commit is contained in:
Marc-André Bombeck committed 2026-09-24 12:37:51 +02:00
1 parent e822154936
commit 70dba821be
2 files changed
+94 -30

No files matched your search

@@ -86,6 +86,12 @@ function byLocalDay(
const EXPECTED = { "2026-09-10": 1100, "2026-09-11": 10 };
/** The ladder puts Withings first for pulse, so its two readings are the day. */
const CANON_EXPECTED = [
{ day: "2026-09-10", value: 95, count: 2, min: 90, max: 100 },
{ day: "2026-09-11", value: 64, count: 1, min: 64, max: 64 },
];
describe("daily series buckets by the user's calendar day (#1026)", () => {
for (const tz of ["Europe/Berlin", "Asia/Kolkata", "America/New_York"]) {
it(`chart daily read agrees with the local day (${tz})`, async () => {
@@ -124,4 +130,58 @@ describe("daily series buckets by the user's calendar day (#1026)", () => {
expect(byLocalDay(rows, tz)).toEqual(EXPECTED);
});
}
it("collapses a day to its canonical source before folding it", async () => {
// Two sources on one day: only the ladder's pick is folded, and the
// mean, count and spread all come from that one source.
const prisma = getPrismaClient();
const user = await prisma.user.create({
data: {
username: "tz-canon",
email: "tz-canon@example.test",
role: "USER",
timezone: "Europe/Berlin",
},
});
const at = (h: number) => localHmAsUtc(DAY, "Europe/Berlin", h, 0);
await prisma.measurement.createMany({
data: [
{ value: 60, source: "APPLE_HEALTH" as const, measuredAt: at(8) },
{ value: 70, source: "APPLE_HEALTH" as const, measuredAt: at(9) },
{ value: 90, source: "WITHINGS" as const, measuredAt: at(10) },
{ value: 100, source: "WITHINGS" as const, measuredAt: at(12) },
{ value: 50, source: "MANUAL" as const, measuredAt: at(11) },
// The next day has one source only.
{
value: 64,
source: "WITHINGS" as const,
measuredAt: localHmAsUtc(NEXT_DAY, "Europe/Berlin", 7, 0),
},
].map((r) => ({
...r,
userId: user.id,
type: "PULSE" as const,
unit: "bpm",
})),
});
const rows = await readLiveBuckets({
userId: user.id,
type: "PULSE",
from: new Date("2026-09-05T00:00:00Z"),
to: new Date("2026-09-15T00:00:00Z"),
cap: 366,
priorityJson: null,
grain: "daily",
timeZone: "Europe/Berlin",
});
expect(
rows.map((r) => ({
day: userDayKey(new Date(r.measuredAt), "Europe/Berlin"),
value: r.value,
count: r.count,
min: r.minValue,
max: r.maxValue,
})),
).toEqual(CANON_EXPECTED);
});
});