Merge w11/perf into release/v1.18.11

Serve achievements via SWR and precompute Berlin day-keys to kill the per-row
Intl cost; switch dashboard/summary to cachedSwr; lazy-load three charts that
bypassed the dynamic boundary.
This commit is contained in:
Marc-André Bombeck committed 2026-06-20 19:05:27 +02:00
commit 6975849bec
10 files changed
+357 -35

No files matched your search

+17
View File
@@ -484,10 +484,27 @@ export const caches = {
ttlMs: 60_000,
staleTtlMs: 600_000,
}),
/**
* v1.18.11 (W5 perf) — stale-while-revalidate window. The
* `AchievementUnlockNotifier` is mounted on every authenticated page and
* polls this endpoint every 2 minutes. Against a 60 s hard-TTL bucket
* that poll always landed on an expired entry, firing a cold ~400 ms
* rebuild on every session every 2 minutes. With a stale window the poll
* (and the inline `/achievements` page mount) serves the prior payload
* instantly while one coalesced background recompute refreshes it.
*
* SWR is safe here: the route persists `pendingUnlocks` OUTSIDE the
* cached factory (idempotent `createMany({ skipDuplicates: true })`), so
* serving a stale body never skips an unlock write. The 10-minute window
* mirrors the `medications` bucket; measurement / mood / medication /
* intake writes invalidate the `${userId}|` prefix so a user's own
* action still surfaces on the next read.
*/
achievements: new ServerCache<unknown>({
name: "achievements",
maxEntries: 1000,
ttlMs: 60_000,
staleTtlMs: 600_000,
}),
dashboardWidgets: new ServerCache<unknown>({
name: "dashboardWidgets",
@@ -7,6 +7,19 @@ import {
getHiddenMetrics,
getMoodMetrics,
} from "@/lib/gamification/expansion-metrics";
import { toBerlinDayKey } from "@/lib/gamification/achievements";
// Mirror of the module-private Berlin-hour derivation so the parity test can
// build a CORRECT precomputed `measurementHours` array.
const BERLIN_HOUR_FORMATTER = new Intl.DateTimeFormat("en-US", {
timeZone: "Europe/Berlin",
hour: "2-digit",
hour12: false,
});
function berlinHour(date: Date): number {
const parsed = Number.parseInt(BERLIN_HOUR_FORMATTER.format(date), 10);
return Number.isNaN(parsed) ? 0 : parsed;
}
describe("countMeasurementsByType", () => {
it("counts each type independently", () => {
@@ -298,3 +311,103 @@ describe("buildExpansionMetricValues — integration smoke", () => {
}
});
});
// v1.18.11 (W5 perf) — the achievements builder now precomputes each vitals
// row's Berlin day-key (and hour) ONCE and threads the parallel arrays into
// the engagement + hidden passes instead of re-running `Intl.DateTimeFormat`
// per row per pass. The optimisation must be a pure speed-up: supplying the
// (correct) precomputed arrays has to yield results byte-identical to the
// fallback path that derives them per row.
describe("achievements Intl precompute — byte-identical parity", () => {
// Spread across hours (incl. night-owl 02-04h / early-bird 04-06h Berlin),
// a leap day, and multiple distinct days so every counter is exercised.
const measurements = [
{ type: "WEIGHT", measuredAt: new Date("2024-02-29T03:30:00Z") }, // leap + night-owl
{ type: "PULSE", measuredAt: new Date("2026-04-01T03:10:00Z") }, // early-bird (05:10 Berlin DST)
{
type: "BLOOD_PRESSURE_SYS",
measuredAt: new Date("2026-04-01T09:00:00Z"),
},
{ type: "WEIGHT", measuredAt: new Date("2026-04-02T22:45:00Z") },
{ type: "PULSE", measuredAt: new Date("2026-04-03T12:00:00Z") },
];
const moodEntries = [
{
date: "2026-04-01",
score: 4,
moodLoggedAt: new Date("2026-04-01T20:00:00Z"),
},
];
const intakeEvents = [
{
scheduledFor: new Date("2026-04-02T07:00:00Z"),
takenAt: new Date("2026-04-02T07:05:00Z"),
skipped: false,
},
];
const auditEvents = [
{
action: "doctor-report.pdf.generate",
createdAt: new Date("2026-04-01T10:00:00Z"),
},
];
const measurementDayKeys = measurements.map((m) =>
toBerlinDayKey(m.measuredAt),
);
const measurementHours = measurements.map((m) => berlinHour(m.measuredAt));
it("getEngagementMetrics: precomputed day-keys match per-row derivation", () => {
const fallback = getEngagementMetrics({
measurements,
moodEntries,
intakeEvents,
});
const precomputed = getEngagementMetrics({
measurements,
moodEntries,
intakeEvents,
measurementDayKeys,
});
expect(precomputed).toEqual(fallback);
});
it("getHiddenMetrics: precomputed day-keys + hours match per-row derivation", () => {
const fallback = getHiddenMetrics({
measurements,
moodEntries,
intakeEvents,
auditEvents,
});
const precomputed = getHiddenMetrics({
measurements,
moodEntries,
intakeEvents,
auditEvents,
measurementDayKeys,
measurementHours,
});
expect(precomputed).toEqual(fallback);
// Sanity: the fixture exercises the offset-independent hidden counters
// (leap day = 2024-02-29 Berlin-local; doctor-report audit event).
expect(fallback.leapDayCount).toBe(1);
expect(fallback.doctorPdfCount).toBe(1);
});
it("buildExpansionMetricValues: precomputed day-keys match fallback", () => {
const fallback = buildExpansionMetricValues({
measurements,
moodEntries,
intakeEvents,
auditEvents,
});
const precomputed = buildExpansionMetricValues({
measurements,
moodEntries,
intakeEvents,
auditEvents,
measurementDayKeys,
});
expect(precomputed).toEqual(fallback);
});
});
+26 -3
View File
@@ -197,6 +197,11 @@ function getEventDaySeries(dates: Date[]): {
function getHealthGreenDaySeries(
measurements: MeasurementRecord[],
heightCm: number | null,
// v1.18.11 (W5 perf) — Berlin day-keys precomputed once per vitals row by
// the caller and passed in parallel to `measurements`. Falls back to a
// per-row `toBerlinDayKey` when absent so the standalone callers / tests
// keep working; the result is byte-identical either way.
dayKeys?: string[],
) {
const dayStats = new Map<
string,
@@ -208,8 +213,9 @@ function getHealthGreenDaySeries(
}
>();
for (const measurement of measurements) {
const dayKey = toBerlinDayKey(measurement.measuredAt);
for (let i = 0; i < measurements.length; i++) {
const measurement = measurements[i];
const dayKey = dayKeys?.[i] ?? toBerlinDayKey(measurement.measuredAt);
const bucket = dayStats.get(dayKey) ?? {
weight: [],
bpSys: [],
@@ -702,9 +708,20 @@ export async function buildAchievementsResult(
...passwordLoginDates,
]);
// v1.18.11 (W5 perf) — the vitals array runs through several independent
// full-array passes, each previously re-deriving a Berlin day-key per row
// via `Intl.DateTimeFormat.formatToParts` (~16.8k rows × ~3 passes on a
// power-user account dominated the cold build). Compute each row's day-key
// ONCE here and thread the parallel array into every consumer; the values
// are identical to the per-pass derivation, so the output is byte-identical.
const vitalsDayKeys = (measurements as MeasurementRecord[]).map((m) =>
toBerlinDayKey(m.measuredAt),
);
const healthSeries = getHealthGreenDaySeries(
measurements as MeasurementRecord[],
user.heightCm,
vitalsDayKeys,
);
const onTimeSeries = getOnTimePerfectDaySeries(
intakeEvents,
@@ -725,7 +742,9 @@ export async function buildAchievementsResult(
sleepMeasurements.map((m) => m.measuredAt),
);
const weeklyConsistency = getWeeklyConsistency(
measurements.map((m) => toBerlinDayKey(m.measuredAt)),
// v1.18.11 (W5 perf) — reuse the day-keys computed once above instead of
// a fresh per-row `toBerlinDayKey` pass over the full vitals array.
vitalsDayKeys,
MEASUREMENT_CONSISTENCY_MIN_DAYS_PER_WEEK,
MEASUREMENT_CONSISTENCY_TARGET_WEEKS,
);
@@ -743,6 +762,10 @@ export async function buildAchievementsResult(
moodEntries,
intakeEvents,
auditEvents,
// v1.18.11 (W5 perf) — reuse the vitals day-keys computed once above so
// the engagement + hidden passes don't re-walk the full vitals array
// through `Intl.DateTimeFormat` a second and third time.
measurementDayKeys: vitalsDayKeys,
});
const earnability = getEarnabilityFlags({
hasMedication: medications.length > 0,
+62 -19
View File
@@ -152,20 +152,34 @@ export function getEngagementMetrics(input: {
measurements: MeasurementRecord[];
moodEntries: MoodEntryRecord[];
intakeEvents: IntakeEventRecord[];
/**
* v1.18.11 (W5 perf) — Berlin day-keys for `measurements`, precomputed
* once by the caller and passed in parallel to the array. When present the
* measurement rows reuse these instead of re-deriving a `toBerlinDayKey`
* per row; mood / intake timestamps (far fewer) still derive inline. Falls
* back to per-row derivation when absent — byte-identical result either way.
*/
measurementDayKeys?: string[];
}): {
consistentMonthCount: number;
entryDayStreak: number;
weekendStreakCount: number;
} {
const allDates: Date[] = [];
for (const m of input.measurements) allDates.push(m.measuredAt);
for (const e of input.moodEntries) allDates.push(e.moodLoggedAt);
const dayKeyAccum: string[] = [];
for (let i = 0; i < input.measurements.length; i++) {
dayKeyAccum.push(
input.measurementDayKeys?.[i] ??
toBerlinDayKey(input.measurements[i].measuredAt),
);
}
for (const e of input.moodEntries)
dayKeyAccum.push(toBerlinDayKey(e.moodLoggedAt));
for (const i of input.intakeEvents) {
if (i.takenAt) allDates.push(i.takenAt);
else if (i.skipped) allDates.push(i.scheduledFor);
if (i.takenAt) dayKeyAccum.push(toBerlinDayKey(i.takenAt));
else if (i.skipped) dayKeyAccum.push(toBerlinDayKey(i.scheduledFor));
}
if (allDates.length === 0) {
if (dayKeyAccum.length === 0) {
return {
consistentMonthCount: 0,
entryDayStreak: 0,
@@ -173,9 +187,7 @@ export function getEngagementMetrics(input: {
};
}
const uniqueDays = Array.from(
new Set(allDates.map((d) => toBerlinDayKey(d))),
).sort();
const uniqueDays = Array.from(new Set(dayKeyAccum)).sort();
const entryDayStreak = calculateLongestStreak(uniqueDays);
// consistent-month: did any Berlin-local calendar month have ≥25
@@ -256,6 +268,16 @@ export function getHiddenMetrics(input: {
moodEntries: MoodEntryRecord[];
intakeEvents: IntakeEventRecord[];
auditEvents: AuditLogRecord[];
/**
* v1.18.11 (W5 perf) — Berlin day-keys + hours for `measurements`,
* precomputed once by the caller and passed in parallel to the array. When
* present the measurement rows reuse these instead of re-running
* `Intl.DateTimeFormat` per row for both the hour and the day-key; mood /
* intake timestamps (far fewer) still derive inline. Byte-identical to the
* per-row derivation either way.
*/
measurementDayKeys?: string[];
measurementHours?: number[];
}): {
nightOwlCount: number;
earlyBirdCount: number;
@@ -267,20 +289,25 @@ export function getHiddenMetrics(input: {
let earlyBird = 0;
let leapDay = 0;
const allTimestamps: Date[] = [];
for (const m of input.measurements) allTimestamps.push(m.measuredAt);
for (const e of input.moodEntries) allTimestamps.push(e.moodLoggedAt);
for (const i of input.intakeEvents) {
if (i.takenAt) allTimestamps.push(i.takenAt);
}
for (const ts of allTimestamps) {
const hour = berlinHour(ts);
const tally = (hour: number, dayKey: string): void => {
if (hour >= 2 && hour < 4) nightOwl += 1;
if (hour >= 4 && hour < 6) earlyBird += 1;
const dayKey = toBerlinDayKey(ts);
// Feb 29 — only valid in leap years.
if (dayKey.endsWith("-02-29")) leapDay += 1;
};
for (let i = 0; i < input.measurements.length; i++) {
const ts = input.measurements[i].measuredAt;
tally(
input.measurementHours?.[i] ?? berlinHour(ts),
input.measurementDayKeys?.[i] ?? toBerlinDayKey(ts),
);
}
for (const e of input.moodEntries) {
tally(berlinHour(e.moodLoggedAt), toBerlinDayKey(e.moodLoggedAt));
}
for (const i of input.intakeEvents) {
if (i.takenAt) tally(berlinHour(i.takenAt), toBerlinDayKey(i.takenAt));
}
let doctorPdfCount = 0;
@@ -353,19 +380,35 @@ export function buildExpansionMetricValues(input: {
moodEntries: MoodEntryRecord[];
intakeEvents: IntakeEventRecord[];
auditEvents: AuditLogRecord[];
/**
* v1.18.11 (W5 perf) — Berlin day-keys for `measurements`, precomputed once
* by the caller (the achievements builder already derives them for the
* green-day / weekly-consistency passes) and threaded into the engagement +
* hidden passes so the full vitals array is `Intl`-walked once, not ~3×.
*/
measurementDayKeys?: string[];
}): ExpansionMetrics {
const counts = countMeasurementsByType(input.measurements);
const mood = getMoodMetrics(input.moodEntries);
// The hidden pass also needs the per-row Berlin hour; derive it once here
// (only when the caller supplied day-keys, i.e. on the hot achievements
// path) so a single hour pass replaces the per-pass re-derivation.
const measurementHours = input.measurementDayKeys
? input.measurements.map((m) => berlinHour(m.measuredAt))
: undefined;
const engagement = getEngagementMetrics({
measurements: input.measurements,
moodEntries: input.moodEntries,
intakeEvents: input.intakeEvents,
measurementDayKeys: input.measurementDayKeys,
});
const hidden = getHiddenMetrics({
measurements: input.measurements,
moodEntries: input.moodEntries,
intakeEvents: input.intakeEvents,
auditEvents: input.auditEvents,
measurementDayKeys: input.measurementDayKeys,
measurementHours,
});
return {