Skip to content

Commit c7c38b7

Browse files
Adam RaineDevtools-frontend LUCI CQ
authored andcommitted
[RPP] Create row limit and aggregate helper
One strategy I considered was to add a `maxRows` property to the table component. The overlays for the "N others" row would be aggregated from the table rows beyond `maxRows`. However there are a couple drawbacks to this strategy: - We would be creating lit templates for table rows that don't actually get rendered. - It isn't possible to fill the second column of the "N others" row with relevant data (e.g. total network payload of the rows not shown) The strategy taken in this CL resolves the above concerns but is less DRY. Bug: None Change-Id: Id6e4dde29712f92c867d605ec6239b772feabcaa Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6355097 Commit-Queue: Adam Raine <[email protected]> Reviewed-by: Connor Clark <[email protected]>
1 parent e557210 commit c7c38b7

File tree

11 files changed

+260
-85
lines changed

11 files changed

+260
-85
lines changed

front_end/models/trace/insights/FontDisplay.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export const UIStrings = {
3333
const str_ = i18n.i18n.registerUIStrings('models/trace/insights/FontDisplay.ts', UIStrings);
3434
export const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
3535

36-
interface RemoteFont {
36+
export interface RemoteFont {
3737
name?: string;
3838
request: Types.Events.SyntheticNetworkRequest;
3939
display: string;

front_end/models/trace/insights/UseCache.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,9 +182,15 @@ export function cachingDisabled(
182182
return false;
183183
}
184184

185+
export interface CacheableRequest {
186+
request: Types.Events.SyntheticNetworkRequest;
187+
ttl: number;
188+
wastedBytes: number;
189+
}
190+
185191
export function generateInsight(
186192
parsedTrace: Handlers.Types.ParsedTrace, context: InsightSetContext): UseCacheInsightModel {
187-
const results = [];
193+
const results: CacheableRequest[] = [];
188194
const allRequests = parsedTrace.NetworkRequests.byTime;
189195
let totalWastedBytes = 0;
190196
const wastedBytesByRequestId = new Map<string, number>();

front_end/panels/timeline/components/insights/BUILD.gn

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ ts_library("unittests") {
8080
sources = [
8181
"BaseInsightComponent.test.ts",
8282
"CLSCulprits.test.ts",
83+
"Table.test.ts",
8384
]
8485

8586
deps = [

front_end/panels/timeline/components/insights/FontDisplay.ts

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import type * as Overlays from '../../overlays/overlays.js';
1212

1313
import {BaseInsightComponent} from './BaseInsightComponent.js';
1414
import {eventRef} from './EventRef.js';
15-
import type {TableData} from './Table.js';
15+
import {createLimitedRows, renderOthersLabel, type TableData, type TableDataRow} from './Table.js';
1616

1717
const {UIStrings, i18nString} = Trace.Insights.Models.FontDisplay;
1818

@@ -22,6 +22,25 @@ export class FontDisplay extends BaseInsightComponent<FontDisplayInsightModel> {
2222
static override readonly litTagName = Lit.StaticHtml.literal`devtools-performance-font-display`;
2323
override internalName = 'font-display';
2424

25+
mapToRow(font: Trace.Insights.Models.FontDisplay.RemoteFont): TableDataRow {
26+
const overlay = this.#overlayForRequest.get(font.request);
27+
return {
28+
values: [
29+
eventRef(font.request, {text: font.name}),
30+
i18n.TimeUtilities.millisToString(font.wastedTime),
31+
],
32+
overlays: overlay ? [overlay] : [],
33+
};
34+
}
35+
36+
createAggregatedTableRow(remaining: Trace.Insights.Models.FontDisplay.RemoteFont[]): TableDataRow {
37+
return {
38+
values: [renderOthersLabel(remaining.length), ''],
39+
overlays: remaining.map(r => this.#overlayForRequest.get(r.request))
40+
.filter((o): o is Overlays.Overlays.TimelineOverlay => Boolean(o)),
41+
};
42+
}
43+
2544
#overlayForRequest = new Map<Trace.Types.Events.SyntheticNetworkRequest, Overlays.Overlays.TimelineOverlay>();
2645

2746
override createOverlays(): Overlays.Overlays.TimelineOverlay[] {
@@ -51,20 +70,16 @@ export class FontDisplay extends BaseInsightComponent<FontDisplayInsightModel> {
5170
return Lit.nothing;
5271
}
5372

73+
const rows = createLimitedRows(this.model.fonts, this);
74+
5475
// clang-format off
5576
return html`
5677
<div class="insight-section">
5778
${html`<devtools-performance-table
5879
.data=${{
5980
insight: this,
6081
headers: [i18nString(UIStrings.fontColumn), i18nString(UIStrings.wastedTimeColumn)],
61-
rows: this.model.fonts.map(font => ({
62-
values: [
63-
eventRef(font.request, {text: font.name}),
64-
i18n.TimeUtilities.millisToString(font.wastedTime),
65-
],
66-
overlays: [this.#overlayForRequest.get(font.request)],
67-
})),
82+
rows,
6883
} as TableData}>
6984
</devtools-performance-table>`}
7085
</div>`;

front_end/panels/timeline/components/insights/ForcedReflow.ts

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,29 @@ import * as Lit from '../../../../ui/lit/lit.js';
1414
import type * as Overlays from '../../overlays/overlays.js';
1515

1616
import {BaseInsightComponent} from './BaseInsightComponent.js';
17-
import type {TableData} from './Table.js';
17+
import {createLimitedRows, renderOthersLabel, type TableData, type TableDataRow} from './Table.js';
1818

1919
const {UIStrings, i18nString} = Trace.Insights.Models.ForcedReflow;
2020

2121
const {html, nothing} = Lit;
2222

2323
export class ForcedReflow extends BaseInsightComponent<ForcedReflowInsightModel> {
2424
static override readonly litTagName = Lit.StaticHtml.literal`devtools-performance-forced-reflow`;
25+
26+
mapToRow(data: Trace.Insights.Models.ForcedReflow.BottomUpCallStack): TableDataRow {
27+
return {
28+
values: [this.#linkifyUrl(data.bottomUpData)],
29+
overlays: this.#createOverlayForEvents(data.relatedEvents),
30+
};
31+
}
32+
33+
createAggregatedTableRow(remaining: Trace.Insights.Models.ForcedReflow.BottomUpCallStack[]): TableDataRow {
34+
return {
35+
values: [renderOthersLabel(remaining.length)],
36+
overlays: remaining.flatMap(r => this.#createOverlayForEvents(r.relatedEvents)),
37+
};
38+
}
39+
2540
override internalName = 'forced-reflow';
2641

2742
#linkifyUrl(callFrame: Trace.Types.Events.CallFrame|Protocol.Runtime.CallFrame|null): Lit.LitTemplate {
@@ -57,6 +72,9 @@ export class ForcedReflow extends BaseInsightComponent<ForcedReflowInsightModel>
5772
const bottomUpCallStackData = this.model.aggregatedBottomUpData;
5873
const time = (us: Trace.Types.Timing.Micro): string =>
5974
i18n.TimeUtilities.millisToString(Platform.Timing.microSecondsToMilliSeconds(us));
75+
76+
const rows = createLimitedRows(bottomUpCallStackData, this);
77+
6078
// clang-format off
6179
return html`
6280
${topLevelFunctionCallData ? html`
@@ -81,10 +99,7 @@ export class ForcedReflow extends BaseInsightComponent<ForcedReflowInsightModel>
8199
.data=${{
82100
insight: this,
83101
headers: [i18nString(UIStrings.relatedStackTrace)],
84-
rows: bottomUpCallStackData.map(data => ({
85-
values: [this.#linkifyUrl(data.bottomUpData)],
86-
overlays: this.#createOverlayForEvents(data.relatedEvents),
87-
})),
102+
rows,
88103
} as TableData}>
89104
</devtools-performance-table>
90105
</div>`;

front_end/panels/timeline/components/insights/ImageDelivery.ts

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
// found in the LICENSE file.
44

55
import '../../../../ui/components/icon_button/icon_button.js';
6-
import './Table.js';
76

87
import type {ImageDeliveryInsightModel} from '../../../../models/trace/insights/ImageDelivery.js';
98
import * as Trace from '../../../../models/trace/trace.js';
@@ -12,14 +11,12 @@ import type * as Overlays from '../../overlays/overlays.js';
1211

1312
import {BaseInsightComponent} from './BaseInsightComponent.js';
1413
import {imageRef} from './EventRef.js';
15-
import type {TableDataRow} from './Table.js';
14+
import {createLimitedRows, renderOthersLabel, type TableDataRow} from './Table.js';
1615

1716
const {UIStrings, i18nString} = Trace.Insights.Models.ImageDelivery;
1817

1918
const {html} = Lit;
2019

21-
const MAX_REQUESTS = 10;
22-
2320
export class ImageDelivery extends BaseInsightComponent<ImageDeliveryInsightModel> {
2421
static override readonly litTagName = Lit.StaticHtml.literal`devtools-performance-image-delivery`;
2522
override internalName = 'image-delivery';
@@ -41,6 +38,20 @@ export class ImageDelivery extends BaseInsightComponent<ImageDeliveryInsightMode
4138
};
4239
}
4340

41+
mapToRow(image: Trace.Insights.Models.ImageDelivery.OptimizableImage): TableDataRow {
42+
return {
43+
values: [imageRef(image.request)],
44+
overlays: [this.#createOverlayForRequest(image.request)],
45+
};
46+
}
47+
48+
createAggregatedTableRow(remaining: Trace.Insights.Models.ImageDelivery.OptimizableImage[]): TableDataRow {
49+
return {
50+
values: [renderOthersLabel(remaining.length)],
51+
overlays: remaining.map(r => this.#createOverlayForRequest(r.request)),
52+
};
53+
}
54+
4455
override getEstimatedSavingsBytes(): number|null {
4556
return this.model?.totalByteSavings ?? null;
4657
}
@@ -55,20 +66,7 @@ export class ImageDelivery extends BaseInsightComponent<ImageDeliveryInsightMode
5566
const topImages =
5667
optimizableImages.sort((a, b) => b.request.args.data.decodedBodyLength - a.request.args.data.decodedBodyLength);
5768

58-
const remaining = topImages.splice(MAX_REQUESTS);
59-
const rows: TableDataRow[] = topImages.map(image => ({
60-
values: [imageRef(image.request)],
61-
overlays: [this.#createOverlayForRequest(image.request)],
62-
}));
63-
64-
if (remaining.length > 0) {
65-
const value =
66-
remaining.length > 1 ? i18nString(UIStrings.others, {PH1: remaining.length}) : imageRef(remaining[0].request);
67-
rows.push({
68-
values: [value],
69-
overlays: remaining.map(r => this.#createOverlayForRequest(r.request)),
70-
});
71-
}
69+
const rows = createLimitedRows(topImages, this);
7270

7371
if (!rows.length) {
7472
return html`<div class="insight-section">${i18nString(UIStrings.noOptimizableImages)}</div>`;

front_end/panels/timeline/components/insights/RenderBlocking.ts

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,40 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5-
import './Table.js';
6-
75
import * as i18n from '../../../../core/i18n/i18n.js';
8-
import * as Platform from '../../../../core/platform/platform.js';
96
import type {RenderBlockingInsightModel} from '../../../../models/trace/insights/RenderBlocking.js';
107
import * as Trace from '../../../../models/trace/trace.js';
118
import * as Lit from '../../../../ui/lit/lit.js';
129
import type * as Overlays from '../../overlays/overlays.js';
1310

1411
import {BaseInsightComponent} from './BaseInsightComponent.js';
1512
import {eventRef} from './EventRef.js';
13+
import {createLimitedRows, renderOthersLabel, type TableDataRow} from './Table.js';
1614

1715
const {UIStrings, i18nString} = Trace.Insights.Models.RenderBlocking;
1816

1917
const {html} = Lit;
2018

2119
export class RenderBlocking extends BaseInsightComponent<RenderBlockingInsightModel> {
2220
static override readonly litTagName = Lit.StaticHtml.literal`devtools-performance-render-blocking-requests`;
21+
22+
mapToRow(request: Trace.Types.Events.SyntheticNetworkRequest): TableDataRow {
23+
return {
24+
values: [
25+
eventRef(request),
26+
i18n.TimeUtilities.formatMicroSecondsTime(request.dur),
27+
],
28+
overlays: [this.#createOverlayForRequest(request)],
29+
};
30+
}
31+
32+
createAggregatedTableRow(remaining: Trace.Types.Events.SyntheticNetworkRequest[]): TableDataRow {
33+
return {
34+
values: [renderOthersLabel(remaining.length), ''],
35+
overlays: remaining.map(r => this.#createOverlayForRequest(r)),
36+
};
37+
}
38+
2339
protected override hasAskAISupport = true;
2440
override internalName = 'render-blocking-requests';
2541

@@ -48,27 +64,21 @@ export class RenderBlocking extends BaseInsightComponent<RenderBlockingInsightMo
4864
return Lit.nothing;
4965
}
5066

51-
const MAX_REQUESTS = 3;
52-
const topRequests = this.model.renderBlockingRequests.slice(0, MAX_REQUESTS);
53-
54-
if (!topRequests.length) {
67+
const requests = this.model.renderBlockingRequests;
68+
if (!requests.length) {
5569
return html`<div class="insight-section">${i18nString(UIStrings.noRenderBlocking)}</div>`;
5670
}
5771

72+
const rows = createLimitedRows(requests, this);
73+
5874
// clang-format off
5975
return html`
6076
<div class="insight-section">
6177
<devtools-performance-table
6278
.data=${{
6379
insight: this,
6480
headers: [i18nString(UIStrings.renderBlockingRequest), i18nString(UIStrings.duration)],
65-
rows: topRequests.map(request => ({
66-
values: [
67-
eventRef(request),
68-
i18n.TimeUtilities.millisToString(Platform.Timing.microSecondsToMilliSeconds(request.dur)),
69-
],
70-
overlays: [this.#createOverlayForRequest(request)],
71-
})),
81+
rows,
7282
}}>
7383
</devtools-performance-table>
7484
</div>
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// Copyright 2025 The Chromium Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import * as Insights from './insights.js';
6+
import type {RowLimitAggregator} from './Table.js';
7+
8+
const {createLimitedRows} = Insights.Table;
9+
10+
const {assert} = chai;
11+
12+
describe('rowLimitAggregate', () => {
13+
const aggregator: RowLimitAggregator<number> = {
14+
mapToRow: num => ({values: [num]}),
15+
createAggregatedTableRow: others => ({values: [`${others.length} others`]}),
16+
};
17+
18+
it('handles empty array', () => {
19+
const rows = createLimitedRows([], aggregator);
20+
assert.deepEqual(rows, []);
21+
});
22+
23+
it('respects limit 0', () => {
24+
const rows = createLimitedRows([1, 2], aggregator, 0);
25+
assert.deepEqual(rows, []);
26+
});
27+
28+
it('respects limit 1', () => {
29+
const rows = createLimitedRows([1, 2, 3], aggregator, 1);
30+
assert.deepEqual(rows, [
31+
{values: ['3 others']},
32+
]);
33+
});
34+
35+
it('correctly limits rows', () => {
36+
const rows = createLimitedRows([1, 2, 3, 4, 5], aggregator, 3);
37+
assert.deepEqual(rows, [
38+
{values: [1]},
39+
{values: [2]},
40+
{values: ['3 others']},
41+
]);
42+
});
43+
44+
it('does not aggregate if input shorter than the limit', () => {
45+
const rows = createLimitedRows([1, 2], aggregator, 3);
46+
assert.deepEqual(rows, [
47+
{values: [1]},
48+
{values: [2]},
49+
]);
50+
});
51+
52+
it('does not aggregate if input is exactly at the limit', () => {
53+
const rows = createLimitedRows([1, 2, 3], aggregator, 3);
54+
assert.deepEqual(rows, [
55+
{values: [1]},
56+
{values: [2]},
57+
{values: [3]},
58+
]);
59+
});
60+
});

0 commit comments

Comments
 (0)