Skip to content

Commit c813158

Browse files
nabramovitznorman-abramovitz
authored andcommitted
feat(frontend): waterfall zoom, initial-load filter, fresher reports
1 parent 5249ee7 commit c813158

5 files changed

Lines changed: 270 additions & 54 deletions

File tree

src/frontend/packages/core/src/features/about/diagnostic-performance-page/diagnostic-performance-page.component.html

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,18 @@
2626
<!-- Scope note -->
2727
<app-card>
2828
<div class="px-4 py-3 text-sm text-content-muted">
29-
These numbers reflect this browsing session's initial page load and stay put
30-
as you move around the app. Reload &amp; measure captures a fresh warm
31-
(cached) load; for a cold load, hard-reload the browser with the cache
32-
bypassed (&#8984;&#8679;R in Chrome/Firefox on macOS, Ctrl+Shift+R elsewhere).
33-
Numbers are cleanest when this page is opened soon after the app loads &mdash;
34-
background requests made before the first visit are included.
29+
The milestone numbers reflect this browsing session's initial page load and
30+
stay put as you move around the app. The waterfall and byte totals keep
31+
accumulating: every fetch since the page loaded is recorded, including lazy
32+
route chunks loaded as you navigate, so the timeline grows (and its scale
33+
stretches from ms to s) the longer the session runs &mdash; check
34+
&ldquo;Initial load only&rdquo; on the waterfall to compare like with like.
35+
Recording stops silently after 500 resources. Reload &amp; measure captures
36+
a fresh warm (cached) load; for a cold load, hard-reload the browser with
37+
the cache bypassed (&#8984;&#8679;R in Chrome/Firefox on macOS,
38+
Ctrl+Shift+R elsewhere). Numbers are cleanest when this page is opened soon
39+
after the app loads &mdash; background requests made before the first visit
40+
are included.
3541
</div>
3642
</app-card>
3743

src/frontend/packages/core/src/features/about/diagnostic-performance-page/diagnostic-performance-page.component.spec.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,14 +119,16 @@ describe('DiagnosticPerformancePageComponent', () => {
119119
expect(verdict).toContain('50% cached');
120120
});
121121

122-
it('re-displays the saved report instead of re-measuring on re-entry', async () => {
122+
it('re-displays the saved report immediately and re-measures on re-entry', async () => {
123123
expect(vi.mocked(buildLoadReport)).toHaveBeenCalledTimes(1);
124124

125125
const second = TestBed.createComponent(DiagnosticPerformancePageComponent);
126126
second.detectChanges();
127+
// The saved report shows before the fresh measurement resolves.
128+
expect(second.componentInstance.report()).toEqual(fixedReport);
127129
await second.whenStable();
128130

129-
expect(vi.mocked(buildLoadReport)).toHaveBeenCalledTimes(1);
131+
expect(vi.mocked(buildLoadReport)).toHaveBeenCalledTimes(2);
130132
expect(second.componentInstance.report()).toEqual(fixedReport);
131133
});
132134

src/frontend/packages/core/src/features/about/diagnostic-performance-page/diagnostic-performance-page.component.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,11 @@ import { ResourceWaterfallComponent } from './resource-waterfall.component';
1818

1919
const TOP_RESOURCE_COUNT = 20;
2020

21-
// The initial-load report is immutable for a browsing session, so it survives
22-
// route changes here and re-displays when the user returns to this tab. A
23-
// hard reload resets module state and yields a fresh measurement.
21+
// The last report survives route changes so returning to this tab shows data
22+
// immediately instead of waiting out the ~500ms paint-observer window. Each
23+
// visit still re-measures in the background: the milestone numbers come out
24+
// identical (same navigation), but the waterfall picks up resources fetched
25+
// since the last look. A hard reload resets module state entirely.
2426
let savedReport: LoadReport | null = null;
2527

2628
/** Test hook: clear the session-persisted report between specs. */
@@ -70,9 +72,8 @@ export class DiagnosticPerformancePageComponent implements OnInit {
7072
ngOnInit() {
7173
if (savedReport) {
7274
this.report.set(savedReport);
73-
} else {
74-
this.measure();
7575
}
76+
this.measure();
7677
}
7778

7879
async measure() {

src/frontend/packages/core/src/features/about/diagnostic-performance-page/resource-waterfall.component.spec.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,15 @@ import {
1111
basename,
1212
formatTick,
1313
groupRows,
14+
initialLoadResources,
1415
mergeMilestoneLabels,
1516
milestoneLines,
1617
spanPercent,
18+
rowLabel,
1719
toPercent,
1820
waterfallScaleMax,
21+
windowPercent,
22+
windowTicks,
1923
} from './resource-waterfall.component';
2024

2125
const row = (over: Partial<ResourceRow> = {}): ResourceRow => ({
@@ -152,6 +156,63 @@ describe('groupRows', () => {
152156
});
153157
});
154158

159+
describe('rowLabel', () => {
160+
it('keeps a file basename bare', () => {
161+
expect(rowLabel('/assets/js/main.abc123.js')).toBe('main.abc123.js');
162+
});
163+
164+
it('prefixes an extensionless basename with its parent segment', () => {
165+
expect(rowLabel('/@vite/client')).toBe('@vite/client');
166+
expect(rowLabel('/pp/v1/proxy/apps/df29d654')).toBe('apps/df29d654');
167+
});
168+
169+
it('leaves a single extensionless segment bare', () => {
170+
expect(rowLabel('/info')).toBe('info');
171+
});
172+
});
173+
174+
describe('windowPercent', () => {
175+
it('maps ms linearly onto 0-100 within the window', () => {
176+
expect(windowPercent(1500, { startMs: 1000, endMs: 2000 })).toBe(50);
177+
});
178+
179+
it('clamps values outside the window', () => {
180+
expect(windowPercent(500, { startMs: 1000, endMs: 2000 })).toBe(0);
181+
expect(windowPercent(2500, { startMs: 1000, endMs: 2000 })).toBe(100);
182+
});
183+
184+
it('returns 0 for an empty window', () => {
185+
expect(windowPercent(1000, { startMs: 1000, endMs: 1000 })).toBe(0);
186+
});
187+
});
188+
189+
describe('windowTicks', () => {
190+
it('matches axisTicks for a zero-based window', () => {
191+
expect(windowTicks({ startMs: 0, endMs: 1000 })).toEqual(axisTicks(1000));
192+
});
193+
194+
it('emits round-number ticks inside a shifted window, excluding the start edge', () => {
195+
expect(windowTicks({ startMs: 1000, endMs: 2000 }))
196+
.toEqual([1100, 1200, 1300, 1400, 1500, 1600, 1700, 1800, 1900, 2000]);
197+
});
198+
199+
it('returns nothing for an empty window', () => {
200+
expect(windowTicks({ startMs: 1000, endMs: 1000 })).toEqual([]);
201+
});
202+
});
203+
204+
describe('initialLoadResources', () => {
205+
it('drops resources starting after the load event, keeping the boundary inclusive', () => {
206+
const rows = [row({ startMs: 100 }), row({ startMs: 500 }), row({ startMs: 501 })];
207+
expect(initialLoadResources(rows, 500).map(r => r.startMs)).toEqual([100, 500]);
208+
});
209+
210+
it('filters nothing when the load event is unknown', () => {
211+
const rows = [row({ startMs: 100 }), row({ startMs: 9999 })];
212+
expect(initialLoadResources(rows, 0)).toEqual(rows);
213+
});
214+
});
215+
155216
describe('milestoneLines', () => {
156217
it('includes DCL, load, FCP and LCP when all are present', () => {
157218
const lines = milestoneLines({

0 commit comments

Comments
 (0)