fix(hydration-timing): report list hydration relative to the foreground attempt (#1813)

The headline listVisibleMs was boot-relative, so a navigation or node
switch minutes after boot reported page age as the foreground hydration
duration. The developer-mode report now distinguishes boot-, node-session-,
and attempt-relative durations (schema v2), the collapsed chip shows the
attempt-relative foreground value with its anchor, and superseded, aborted,
or failed attempts can never become the reported success.

The report schema bumps to v2 with explicit anchors: bootAgeMs and the
boot->auth/nodes/shell durations, sessionAgeMs and session-relative
list visible/hydrated, and lastAttempt* fields resolved from the newest
committed list_visible event in the active node session (event-derived, so
a later stack-detail attempt cannot steal the headline). listVisibleMs is
retained as the raw boot-relative compatibility field.

The repeatable performance baseline scenario matrix is documented in the
local internal docs (docs/internal/performance/hydration-timing-baseline.md);
the post-hot-path-fix baseline capture is a follow-up evidence step.
This commit is contained in:
Anso
2026-08-09 19:19:00 -04:00
committed by GitHub
parent 600367e66c
commit 88fc5470e8
6 changed files with 720 additions and 42 deletions
@@ -4,10 +4,15 @@ import type { HydrationReport, HydrationSnapshot } from '@/lib/hydrationTiming';
let mockSnapshot: HydrationSnapshot;
let mockListVisibleMs: number | null;
let mockListAnchor: 'attempt' | 'session' | null;
let mockReport: HydrationReport;
vi.mock('@/hooks/useHydrationTiming', () => ({
useHydrationTiming: () => ({ snapshot: mockSnapshot, listVisibleMs: mockListVisibleMs }),
useHydrationTiming: () => ({
snapshot: mockSnapshot,
listVisibleMs: mockListVisibleMs,
listAnchor: mockListAnchor,
}),
}));
const clearReportMock = vi.fn();
@@ -23,25 +28,39 @@ import { HydrationTimingPanel } from '../HydrationTimingPanel';
function snapshot(events: HydrationSnapshot['events'] = []): HydrationSnapshot {
return {
schemaVersion: 1,
clock: 'performance.now',
bootSessionId: 'boot-1',
bootStartAt: 0,
nodeSessionId: 'node-2',
nodeId: 1,
nodeSessionStartAt: 0,
lastAttempt: null,
events,
};
}
function report(over: Partial<HydrationReport> = {}): HydrationReport {
return {
schemaVersion: 1,
schemaVersion: 2,
capturedAt: 0,
clock: 'performance.now',
bootSessionId: 'boot-1',
nodeSessionId: 'node-2',
nodeId: 1,
listVisibleMs: 1200,
bootAgeMs: 620000,
bootAuthResolvedMs: 200,
bootNodesResolvedMs: 400,
bootShellCommittedMs: 500,
sessionAgeMs: 4200,
sessionListVisibleMs: 1200,
sessionListHydratedMs: 1500,
lastAttemptId: 'attempt-1',
lastAttemptListVisibleMs: 420,
lastAttemptListHydratedMs: 700,
lastAttemptHydrationGapMs: 280,
lastAttemptProxied: null,
lastAttemptNodeId: null,
anyProxied: false,
phases: [
{ phase: 'boot_start', kind: 'milestone', offsetMs: 0, critical: true, outcome: 'ok' },
@@ -55,18 +74,27 @@ beforeEach(() => {
clearReportMock.mockClear();
copyMock.mockClear();
mockSnapshot = snapshot();
mockListVisibleMs = 1200;
mockListVisibleMs = 420;
mockListAnchor = 'attempt';
mockReport = report();
});
describe('HydrationTimingPanel', () => {
it('shows the list_visible elapsed time on the collapsed chip', () => {
it('shows the foreground list_visible elapsed time and its anchor on the collapsed chip', () => {
render(<HydrationTimingPanel />);
expect(screen.getByTestId('hydration-chip')).toHaveTextContent('list 1.2s');
expect(screen.getByTestId('hydration-chip')).toHaveTextContent('list 420ms · attempt');
});
it('shows the session anchor when no foreground attempt exists', () => {
mockListAnchor = 'session';
mockListVisibleMs = 100;
render(<HydrationTimingPanel />);
expect(screen.getByTestId('hydration-chip')).toHaveTextContent('list 100ms · session');
});
it('shows an ellipsis before list_visible commits', () => {
mockListVisibleMs = null;
mockListAnchor = null;
render(<HydrationTimingPanel />);
expect(screen.getByTestId('hydration-chip')).toHaveTextContent('list …');
});
@@ -83,6 +111,13 @@ describe('HydrationTimingPanel', () => {
expect(screen.getByTestId('hydration-chip')).toBeInTheDocument();
});
it('shows boot age and session age as context alongside the chip', () => {
render(<HydrationTimingPanel />);
fireEvent.click(screen.getByTestId('hydration-chip'));
expect(screen.getByText(/Boot age/)).toHaveTextContent('620.0s');
expect(screen.getByText(/Session age/)).toHaveTextContent('4.2s');
});
it('collapses on Escape', () => {
render(<HydrationTimingPanel />);
fireEvent.click(screen.getByTestId('hydration-chip'));