Skip to content

Commit b9ec6d9

Browse files
Connor ClarkDevtools-frontend LUCI CQ
authored andcommitted
[RPP] Display field metrics in insights tab
https://i.imgur.com/DIcZ2vE.png (local + field) https://i.imgur.com/vL7vk2C.png (just local) Bug: 368135130 Change-Id: I9b7d9f2bfbc3ce01c4cbbcc051d6b0cf3e0f8b30 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6169823 Commit-Queue: Connor Clark <[email protected]> Reviewed-by: Paul Irish <[email protected]>
1 parent 710acd1 commit b9ec6d9

15 files changed

+283
-147
lines changed

front_end/models/trace/insights/Common.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ describeWithEnvironment('Common', function() {
4646
const {insightSet, metadata} = await process(this, 'image-delivery.json.gz');
4747

4848
const weights = calculateMetricWeightsForSorting(insightSet, metadata);
49-
assert.deepEqual(weights, {lcp: 0.07778127820223579, inp: 0.5504200439526509, cls: 0.37179867784511333});
49+
assert.deepEqual(weights, {lcp: 0.48649783990559314, inp: 0.48649783990559314, cls: 0.027004320188813675});
5050
});
5151
});
5252
});

front_end/models/trace/insights/Common.ts

Lines changed: 80 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,81 @@ export function evaluateCLSMetricScore(value: number): number {
8484
return getLogNormalScore({p10: 0.1, median: 0.25}, value);
8585
}
8686

87+
export interface CrUXFieldMetricTimingResult {
88+
value: Types.Timing.MicroSeconds;
89+
pageScope: CrUXManager.PageScope;
90+
}
91+
export interface CrUXFieldMetricNumberResult {
92+
value: number;
93+
pageScope: CrUXManager.PageScope;
94+
}
95+
export interface CrUXFieldMetricResults {
96+
fcp: CrUXFieldMetricTimingResult|null;
97+
lcp: CrUXFieldMetricTimingResult|null;
98+
inp: CrUXFieldMetricTimingResult|null;
99+
cls: CrUXFieldMetricNumberResult|null;
100+
}
101+
102+
function getPageResult(cruxFieldData: CrUXManager.PageResult[], url: string, origin: string): CrUXManager.PageResult|
103+
undefined {
104+
return cruxFieldData.find(result => {
105+
const key = (result['url-ALL'] || result['origin-ALL'])?.record.key;
106+
return (key?.url && key.url === url) || (key?.origin && key.origin === origin);
107+
});
108+
}
109+
110+
function getMetricResult(
111+
pageResult: CrUXManager.PageResult, name: CrUXManager.StandardMetricNames): CrUXFieldMetricNumberResult|null {
112+
let value = pageResult['url-ALL']?.record.metrics[name]?.percentiles?.p75;
113+
if (typeof value === 'string') {
114+
value = Number(value);
115+
}
116+
if (typeof value === 'number' && Number.isFinite(value)) {
117+
return {value, pageScope: 'url'};
118+
}
119+
120+
value = pageResult['origin-ALL']?.record.metrics[name]?.percentiles?.p75;
121+
if (typeof value === 'string') {
122+
value = Number(value);
123+
}
124+
if (typeof value === 'number' && Number.isFinite(value)) {
125+
return {value, pageScope: 'origin'};
126+
}
127+
128+
return null;
129+
}
130+
131+
function getMetricTimingResult(
132+
pageResult: CrUXManager.PageResult, name: CrUXManager.StandardMetricNames): CrUXFieldMetricTimingResult|null {
133+
const result = getMetricResult(pageResult, name);
134+
if (result) {
135+
const valueMs = result.value as Types.Timing.MilliSeconds;
136+
return {value: Helpers.Timing.millisecondsToMicroseconds(valueMs), pageScope: result.pageScope};
137+
}
138+
139+
return null;
140+
}
141+
142+
export function getFieldMetricsForInsightSet(
143+
insightSet: InsightSet, metadata: Types.File.MetaData|null): CrUXFieldMetricResults|null {
144+
const cruxFieldData = metadata?.cruxFieldData;
145+
if (!cruxFieldData) {
146+
return null;
147+
}
148+
149+
const pageResult = getPageResult(cruxFieldData, insightSet.url.href, insightSet.url.origin);
150+
if (!pageResult) {
151+
return null;
152+
}
153+
154+
return {
155+
fcp: getMetricTimingResult(pageResult, 'first_contentful_paint'),
156+
lcp: getMetricTimingResult(pageResult, 'largest_contentful_paint'),
157+
inp: getMetricTimingResult(pageResult, 'interaction_to_next_paint'),
158+
cls: getMetricResult(pageResult, 'cumulative_layout_shift'),
159+
};
160+
}
161+
87162
export function calculateMetricWeightsForSorting(
88163
insightSet: InsightSet, metadata: Types.File.MetaData|null): {lcp: number, inp: number, cls: number} {
89164
const weights = {
@@ -97,32 +172,14 @@ export function calculateMetricWeightsForSorting(
97172
return weights;
98173
}
99174

100-
const getPageResult = (url: string, origin: string): CrUXManager.PageResult|undefined => {
101-
return cruxFieldData.find(result => {
102-
const key = (result['url-ALL'] || result['origin-ALL'])?.record.key;
103-
return (key?.url && key.url === url) || (key?.origin && key.origin === origin);
104-
});
105-
};
106-
const getMetricValue = (pageResult: CrUXManager.PageResult, name: CrUXManager.StandardMetricNames): number|null => {
107-
const score = pageResult['url-ALL']?.record.metrics[name]?.percentiles?.p75 ??
108-
pageResult['origin-ALL']?.record.metrics[name]?.percentiles?.p75;
109-
if (typeof score === 'number') {
110-
return score;
111-
}
112-
if (typeof score === 'string' && Number.isFinite(Number(score))) {
113-
return Number(score);
114-
}
115-
return null;
116-
};
117-
118-
const pageResult = getPageResult(insightSet.url.href, insightSet.url.origin);
119-
if (!pageResult) {
175+
const fieldMetrics = getFieldMetricsForInsightSet(insightSet, metadata);
176+
if (!fieldMetrics) {
120177
return weights;
121178
}
122179

123-
const fieldLcp = getMetricValue(pageResult, 'largest_contentful_paint');
124-
const fieldInp = getMetricValue(pageResult, 'interaction_to_next_paint');
125-
const fieldCls = getMetricValue(pageResult, 'cumulative_layout_shift');
180+
const fieldLcp = fieldMetrics.lcp?.value ?? null;
181+
const fieldInp = fieldMetrics.inp?.value ?? null;
182+
const fieldCls = fieldMetrics.cls?.value ?? null;
126183
const fieldLcpScore = fieldLcp !== null ? evaluateLCPMetricScore(fieldLcp) : 0;
127184
const fieldInpScore = fieldInp !== null ? evaluateINPMetricScore(fieldInp) : 0;
128185
const fieldClsScore = fieldCls !== null ? evaluateCLSMetricScore(fieldCls) : 0;

front_end/panels/timeline/TimelineFlameChartView.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -293,10 +293,11 @@ describeWithEnvironment('TimelineFlameChartView', function() {
293293
});
294294

295295
it('renders metrics as marker overlays w/ tooltips', async function() {
296-
const {parsedTrace, metadata} = await TraceLoader.traceEngine(this, 'crux.json.gz');
296+
const {parsedTrace, metadata, insights} = await TraceLoader.traceEngine(this, 'crux.json.gz');
297297
const mockViewDelegate = new MockViewDelegate();
298298

299299
const flameChartView = new Timeline.TimelineFlameChartView.TimelineFlameChartView(mockViewDelegate);
300+
flameChartView.setInsights(insights, new Map());
300301
flameChartView.setModel(parsedTrace, metadata);
301302

302303
const tooltips =

front_end/panels/timeline/TimelineFlameChartView.ts

Lines changed: 19 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import * as Platform from '../../core/platform/platform.js';
88
import * as Root from '../../core/root/root.js';
99
import * as SDK from '../../core/sdk/sdk.js';
1010
import * as Bindings from '../../models/bindings/bindings.js';
11-
import type * as CrUXManager from '../../models/crux-manager/crux-manager.js';
1211
import * as Trace from '../../models/trace/trace.js';
1312
import * as TraceBounds from '../../services/trace_bounds/trace_bounds.js';
1413
import * as PerfUI from '../../ui/legacy/components/perf_ui/perf_ui.js';
@@ -524,32 +523,18 @@ export class TimelineFlameChartView extends Common.ObjectWrapper.eventMixin<Even
524523
});
525524
}
526525

527-
#amendMarkerWithFieldData(parsedTrace: Trace.Handlers.Types.ParsedTrace): void {
528-
const cruxFieldData = this.#traceMetadata?.cruxFieldData;
529-
if (!cruxFieldData) {
526+
#amendMarkerWithFieldData(): void {
527+
if (!this.#traceMetadata?.cruxFieldData || !this.#traceInsightSets) {
530528
return;
531529
}
532530

533-
const getPageResult = (url: string, origin: string): CrUXManager.PageResult|undefined => {
534-
return cruxFieldData.find(result => {
535-
const key = (result['url-ALL'] || result['origin-ALL'])?.record.key;
536-
return (key?.url && key.url === url) || (key?.origin && key.origin === origin);
537-
});
538-
};
539-
const getMetricScore = (pageResult: CrUXManager.PageResult, name: CrUXManager.StandardMetricNames):
540-
{value: number, pageScope: CrUXManager.PageScope}|null => {
541-
let value = pageResult['url-ALL']?.record.metrics[name]?.percentiles?.p75;
542-
if (typeof value === 'number') {
543-
return {value, pageScope: 'url'};
544-
}
545-
546-
value = pageResult['origin-ALL']?.record.metrics[name]?.percentiles?.p75;
547-
if (typeof value === 'number') {
548-
return {value, pageScope: 'origin'};
549-
}
550-
551-
return null;
552-
};
531+
const fieldMetricResultsByNavigationId = new Map<string, Trace.Insights.Common.CrUXFieldMetricResults|null>();
532+
for (const [key, insightSet] of this.#traceInsightSets) {
533+
if (insightSet.navigation) {
534+
fieldMetricResultsByNavigationId.set(
535+
key, Trace.Insights.Common.getFieldMetricsForInsightSet(insightSet, this.#traceMetadata));
536+
}
537+
}
553538

554539
for (const marker of this.#markers) {
555540
for (const event of marker.entries) {
@@ -558,35 +543,23 @@ export class TimelineFlameChartView extends Common.ObjectWrapper.eventMixin<Even
558543
continue;
559544
}
560545

561-
let cruxMetricName: CrUXManager.StandardMetricNames;
562-
if (event.name === Trace.Types.Events.Name.MARK_FCP) {
563-
cruxMetricName = 'first_contentful_paint';
564-
} else if (event.name === Trace.Types.Events.Name.MARK_LCP_CANDIDATE) {
565-
cruxMetricName = 'largest_contentful_paint';
566-
} else {
567-
continue;
568-
}
569-
570-
const nav = parsedTrace.Meta.navigationsByNavigationId.get(navigationId);
571-
// TODO(paulirish): Trace.Types.Events.NavigationStart type should be fixed, not working as intended.
572-
const url = nav?.args.data.documentLoaderURL as (string | undefined);
573-
if (!nav || !url) {
546+
const fieldMetricResults = fieldMetricResultsByNavigationId.get(navigationId);
547+
if (!fieldMetricResults) {
574548
continue;
575549
}
576550

577-
const pageResult = getPageResult(url, new URL(url).origin);
578-
if (!pageResult) {
579-
continue;
551+
let fieldMetricResult;
552+
if (event.name === Trace.Types.Events.Name.MARK_FCP) {
553+
fieldMetricResult = fieldMetricResults.fcp;
554+
} else if (event.name === Trace.Types.Events.Name.MARK_LCP_CANDIDATE) {
555+
fieldMetricResult = fieldMetricResults.lcp;
580556
}
581557

582-
const metricScoreResult = getMetricScore(pageResult, cruxMetricName);
583-
if (!metricScoreResult) {
558+
if (!fieldMetricResult) {
584559
continue;
585560
}
586561

587-
const tsMs = metricScoreResult.value as Trace.Types.Timing.MilliSeconds;
588-
const ts = Trace.Helpers.Timing.millisecondsToMicroseconds(tsMs);
589-
marker.entryToFieldResult.set(event, {ts, pageScope: metricScoreResult.pageScope});
562+
marker.entryToFieldResult.set(event, fieldMetricResult);
590563
}
591564
}
592565
}
@@ -640,7 +613,7 @@ export class TimelineFlameChartView extends Common.ObjectWrapper.eventMixin<Even
640613
return;
641614
}
642615

643-
this.#amendMarkerWithFieldData(parsedTrace);
616+
this.#amendMarkerWithFieldData();
644617
this.bulkAddOverlays(this.#markers);
645618
}
646619

front_end/panels/timeline/TimelinePanel.ts

Lines changed: 18 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1916,7 +1916,7 @@ export class TimelinePanel extends UI.Panel.Panel implements Client, TimelineMod
19161916
}
19171917
const {traceIndex} = this.#viewMode;
19181918
const parsedTrace = this.#traceEngineModel.parsedTrace(traceIndex);
1919-
const metadata = this.#traceEngineModel.metadata(traceIndex);
1919+
const traceMetadata = this.#traceEngineModel.metadata(traceIndex);
19201920
const syntheticEventsManager = this.#traceEngineModel.syntheticTraceEventsManager(traceIndex);
19211921

19221922
if (!parsedTrace || !syntheticEventsManager) {
@@ -1950,13 +1950,26 @@ export class TimelinePanel extends UI.Panel.Panel implements Client, TimelineMod
19501950
}
19511951
this.statusPane?.updateProgressBar(i18nString(UIStrings.processed), 70);
19521952

1953-
const isCpuProfile =
1954-
this.#traceEngineModel.metadata(traceIndex)?.dataOrigin === Trace.Types.File.DataOrigin.CPU_PROFILE;
1955-
this.flameChart.setModel(parsedTrace, metadata, isCpuProfile);
1953+
let traceInsightsSets = this.#traceEngineModel.traceInsights(traceIndex);
1954+
if (traceInsightsSets) {
1955+
// Omit insight sets that don't have anything of interest to show to the user.
1956+
const filteredTraceInsightsSets = new Map();
1957+
for (const [key, insightSet] of traceInsightsSets) {
1958+
if (Object.values(insightSet.model).some(model => model.shouldShow)) {
1959+
filteredTraceInsightsSets.set(key, insightSet);
1960+
}
1961+
}
1962+
1963+
traceInsightsSets = filteredTraceInsightsSets.size ? filteredTraceInsightsSets : null;
1964+
}
1965+
this.flameChart.setInsights(traceInsightsSets, this.#eventToRelatedInsights);
1966+
1967+
const isCpuProfile = traceMetadata?.dataOrigin === Trace.Types.File.DataOrigin.CPU_PROFILE;
1968+
this.flameChart.setModel(parsedTrace, traceMetadata, isCpuProfile);
19561969
this.flameChart.resizeToPreferredHeights();
19571970
// Reset the visual selection as we've just swapped to a new trace.
19581971
this.flameChart.setSelectionAndReveal(null);
1959-
this.#sideBar.setParsedTrace(parsedTrace);
1972+
this.#sideBar.setParsedTrace(parsedTrace, traceMetadata);
19601973

19611974
this.searchableViewInternal.showWidget();
19621975

@@ -2048,20 +2061,6 @@ export class TimelinePanel extends UI.Panel.Panel implements Client, TimelineMod
20482061

20492062
this.#setActiveInsight(null);
20502063

2051-
let traceInsightsSets = this.#traceEngineModel.traceInsights(traceIndex);
2052-
if (traceInsightsSets) {
2053-
// Omit insight sets that don't have anything of interest to show to the user.
2054-
const filteredTraceInsightsSets = new Map();
2055-
for (const [key, insightSet] of traceInsightsSets) {
2056-
if (Object.values(insightSet.model).some(model => model.shouldShow)) {
2057-
filteredTraceInsightsSets.set(key, insightSet);
2058-
}
2059-
}
2060-
2061-
traceInsightsSets = filteredTraceInsightsSets.size ? filteredTraceInsightsSets : null;
2062-
}
2063-
2064-
this.flameChart.setInsights(traceInsightsSets, this.#eventToRelatedInsights);
20652064
this.#sideBar.setInsights(traceInsightsSets);
20662065

20672066
this.#eventToRelatedInsights.clear();

front_end/panels/timeline/components/Sidebar.test.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,23 +12,24 @@ import * as Components from './components.js';
1212
describeWithEnvironment('Sidebar', () => {
1313
async function renderSidebar(
1414
parsedTrace: Trace.Handlers.Types.ParsedTrace,
15+
metadata: Trace.Types.File.MetaData|null,
1516
insights: Trace.Insights.Types.TraceInsightSets|null,
1617
): Promise<Components.Sidebar.SidebarWidget> {
1718
const container = document.createElement('div');
1819
renderElementIntoDOM(container);
1920
const sidebar = new Components.Sidebar.SidebarWidget();
2021
sidebar.markAsRoot();
21-
sidebar.setParsedTrace(parsedTrace);
22+
sidebar.setParsedTrace(parsedTrace, metadata);
2223
sidebar.setInsights(insights);
2324
sidebar.show(container);
2425
await raf();
2526
return sidebar;
2627
}
2728

2829
it('renders with two tabs for insights & annotations', async function() {
29-
const {parsedTrace, insights} = await TraceLoader.traceEngine(this, 'web-dev-with-commit.json.gz');
30+
const {parsedTrace, metadata, insights} = await TraceLoader.traceEngine(this, 'web-dev-with-commit.json.gz');
3031

31-
const sidebar = await renderSidebar(parsedTrace, insights);
32+
const sidebar = await renderSidebar(parsedTrace, metadata, insights);
3233
const tabbedPane = sidebar.element.querySelector('.tabbed-pane')?.shadowRoot;
3334
assert.isOk(tabbedPane);
3435
const tabs = Array.from(tabbedPane.querySelectorAll('[role="tab"]'));
@@ -38,9 +39,9 @@ describeWithEnvironment('Sidebar', () => {
3839
});
3940

4041
it('selects the insights tab by default', async function() {
41-
const {parsedTrace, insights} = await TraceLoader.traceEngine(this, 'web-dev-with-commit.json.gz');
42+
const {parsedTrace, metadata, insights} = await TraceLoader.traceEngine(this, 'web-dev-with-commit.json.gz');
4243

43-
const sidebar = await renderSidebar(parsedTrace, insights);
44+
const sidebar = await renderSidebar(parsedTrace, metadata, insights);
4445
const tabbedPane = sidebar.element.querySelector('.tabbed-pane')?.shadowRoot;
4546
assert.isOk(tabbedPane);
4647

@@ -51,8 +52,8 @@ describeWithEnvironment('Sidebar', () => {
5152
});
5253

5354
it('disables the insights tab if there are no insights', async function() {
54-
const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev-with-commit.json.gz');
55-
const sidebar = await renderSidebar(parsedTrace, null);
55+
const {parsedTrace, metadata} = await TraceLoader.traceEngine(this, 'web-dev-with-commit.json.gz');
56+
const sidebar = await renderSidebar(parsedTrace, metadata, null);
5657
const tabbedPane = sidebar.element.querySelector('.tabbed-pane')?.shadowRoot;
5758
assert.isOk(tabbedPane);
5859
const tabs = Array.from(tabbedPane.querySelectorAll('[role="tab"]'));

front_end/panels/timeline/components/Sidebar.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -122,8 +122,8 @@ export class SidebarWidget extends UI.Widget.VBox {
122122
this.#tabbedPane.setSuffixElement('annotations', countAdorner);
123123
}
124124

125-
setParsedTrace(parsedTrace: Trace.Handlers.Types.ParsedTrace|null): void {
126-
this.#insightsView.setParsedTrace(parsedTrace);
125+
setParsedTrace(parsedTrace: Trace.Handlers.Types.ParsedTrace|null, metadata: Trace.Types.File.MetaData|null): void {
126+
this.#insightsView.setParsedTrace(parsedTrace, metadata);
127127
}
128128

129129
setInsights(insights: Trace.Insights.Types.TraceInsightSets|null): void {
@@ -153,8 +153,9 @@ class InsightsView extends UI.Widget.VBox {
153153
this.element.appendChild(this.#component);
154154
}
155155

156-
setParsedTrace(data: Trace.Handlers.Types.ParsedTrace|null): void {
157-
this.#component.parsedTrace = data;
156+
setParsedTrace(parsedTrace: Trace.Handlers.Types.ParsedTrace|null, metadata: Trace.Types.File.MetaData|null): void {
157+
this.#component.parsedTrace = parsedTrace;
158+
this.#component.traceMetadata = metadata;
158159
}
159160

160161
setInsights(data: Trace.Insights.Types.TraceInsightSets|null): void {

0 commit comments

Comments
 (0)