Skip to content

Commit d37d426

Browse files
authored
Add experimental Perf Issues sub-panel (#217)
1 parent d8824de commit d37d426

File tree

11 files changed

+608
-4
lines changed

11 files changed

+608
-4
lines changed

config/gni/devtools_grd_files.gni

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1941,9 +1941,12 @@ grd_files_debug_sources = [
19411941
"front_end/panels/timeline/components/NetworkThrottlingSelector.js",
19421942
"front_end/panels/timeline/components/OriginMap.js",
19431943
"front_end/panels/timeline/components/RelatedInsightChips.js",
1944+
"front_end/panels/timeline/components/RNPerfIssueTypes.js",
19441945
"front_end/panels/timeline/components/Sidebar.js",
19451946
"front_end/panels/timeline/components/SidebarAnnotationsTab.js",
19461947
"front_end/panels/timeline/components/SidebarInsightsTab.js",
1948+
"front_end/panels/timeline/components/SidebarRNPerfSignalsTab.js",
1949+
"front_end/panels/timeline/components/SidebarRNPerfIssueItem.js",
19471950
"front_end/panels/timeline/components/SidebarSingleInsightSet.js",
19481951
"front_end/panels/timeline/components/TimelineSummary.js",
19491952
"front_end/panels/timeline/components/Utils.js",
@@ -1994,6 +1997,8 @@ grd_files_debug_sources = [
19941997
"front_end/panels/timeline/components/relatedInsightChips.css.js",
19951998
"front_end/panels/timeline/components/sidebarAnnotationsTab.css.js",
19961999
"front_end/panels/timeline/components/sidebarInsightsTab.css.js",
2000+
"front_end/panels/timeline/components/sidebarRNPerfIssuesTab.css.js",
2001+
"front_end/panels/timeline/components/sidebarRNPerfIssueItem.css.js",
19972002
"front_end/panels/timeline/components/sidebarSingleInsightSet.css.js",
19982003
"front_end/panels/timeline/components/timelineSummary.css.js",
19992004
"front_end/panels/timeline/extensions/ExtensionUI.js",

front_end/panels/timeline/TimelineFlameChartView.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ export class TimelineFlameChartView extends Common.ObjectWrapper.eventMixin<Even
178178
#treeRowClickDimmer = this.#registerFlameChartDimmer({inclusive: false, outline: false});
179179
#activeInsightDimmer = this.#registerFlameChartDimmer({inclusive: false, outline: true});
180180
#thirdPartyCheckboxDimmer = this.#registerFlameChartDimmer({inclusive: true, outline: false});
181+
#performanceIssuesDimmer = this.#registerFlameChartDimmer({inclusive: false, outline: false});
181182

182183
constructor(delegate: TimelineModeViewDelegate) {
183184
super();
@@ -533,6 +534,26 @@ export class TimelineFlameChartView extends Common.ObjectWrapper.eventMixin<Even
533534
this.networkFlameChart.enableDimming(dimmer.networkChartIndices, dimmer.inclusive, networkOutline);
534535
}
535536

537+
/**
538+
* [RN] Apply highlighting/dimming to a specific Performance Issue event.
539+
*/
540+
updatePerfIssueFlameChartDimmer(event: Trace.Types.Events.Event): void {
541+
this.#flameChartDimmers.forEach(dimmer => {
542+
dimmer.active = false;
543+
});
544+
545+
const mainIndex = this.mainDataProvider.indexForEvent(event);
546+
const networkIndex = this.networkDataProvider.indexForEvent(event);
547+
const mainChartIndices = mainIndex !== null && mainIndex >= 0 ? [mainIndex] : [];
548+
const networkChartIndices = networkIndex !== null && networkIndex >= 0 ? [networkIndex] : [];
549+
550+
this.#updateFlameChartDimmerWithIndices(this.#performanceIssuesDimmer, mainChartIndices, networkChartIndices);
551+
}
552+
553+
#clearPerformanceIssuesDimmer(): void {
554+
this.#updateFlameChartDimmerWithEvents(this.#performanceIssuesDimmer, null);
555+
}
556+
536557
#dimInsightRelatedEvents(relatedEvents: Trace.Types.Events.Event[]): void {
537558
// Dim all events except those related to the active insight.
538559
const relatedMainIndices = relatedEvents.map(event => this.mainDataProvider.indexForEvent(event) ?? -1);
@@ -1595,6 +1616,9 @@ export class TimelineFlameChartView extends Common.ObjectWrapper.eventMixin<Even
15951616
}
15961617
}
15971618

1619+
// [RN] Clear Performance Issues dimmer when clicking directly on the timeline
1620+
this.#clearPerformanceIssuesDimmer();
1621+
15981622
dataProvider.buildFlowForInitiator(entryIndex);
15991623
this.delegate.select(dataProvider.createSelection(entryIndex));
16001624
}

front_end/panels/timeline/TimelinePanel.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -623,6 +623,13 @@ export class TimelinePanel extends UI.Panel.Panel implements Client, TimelineMod
623623
this.#splitWidget.enableShowModeSaving();
624624
this.#splitWidget.show(this.element);
625625

626+
// [RN] Set up callback for Performance Issue selection
627+
this.#sideBar.setSelectTimelineEventCallback((event: Trace.Types.Events.Event) => {
628+
const selection = selectionFromEvent(event);
629+
this.flameChart.setSelectionAndReveal(selection);
630+
this.flameChart.updatePerfIssueFlameChartDimmer(event);
631+
});
632+
626633
this.flameChart.overlays().addEventListener(Overlays.Overlays.TimeRangeMouseOverEvent.eventName, event => {
627634
const {overlay} = event as Overlays.Overlays.TimeRangeMouseOverEvent;
628635
const overlayBounds = Overlays.Overlays.traceWindowContainingOverlays([overlay]);
@@ -2185,10 +2192,6 @@ export class TimelinePanel extends UI.Panel.Panel implements Client, TimelineMod
21852192
// Used in interaction tests & screenshot tests.
21862193
return;
21872194
}
2188-
// [RN] Keep sidebar collapsed by default
2189-
if (Root.Runtime.experiments.isEnabled(Root.Runtime.ExperimentName.REACT_NATIVE_SPECIFIC_UI)) {
2190-
return;
2191-
}
21922195
const needToRestore = this.#restoreSidebarVisibilityOnTraceLoad;
21932196
const userHasSeenSidebar = this.#sideBar.userHasOpenedSidebarOnce();
21942197

front_end/panels/timeline/components/BUILD.gn

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ generate_css("css_files") {
2525
"relatedInsightChips.css",
2626
"sidebarAnnotationsTab.css",
2727
"sidebarInsightsTab.css",
28+
"sidebarRNPerfIssueItem.css",
29+
"sidebarRNPerfIssuesTab.css",
2830
"sidebarSingleInsightSet.css",
2931
"timelineSummary.css",
3032
]
@@ -48,9 +50,12 @@ devtools_module("components") {
4850
"NetworkThrottlingSelector.ts",
4951
"OriginMap.ts",
5052
"RelatedInsightChips.ts",
53+
"RNPerfIssueTypes.ts",
5154
"Sidebar.ts",
5255
"SidebarAnnotationsTab.ts",
5356
"SidebarInsightsTab.ts",
57+
"SidebarRNPerfIssueItem.ts",
58+
"SidebarRNPerfSignalsTab.ts",
5459
"SidebarSingleInsightSet.ts",
5560
"TimelineSummary.ts",
5661
"Utils.ts",
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// Copyright (c) Meta Platforms, Inc. and affiliates.
2+
// Copyright 2024 The Chromium Authors. All rights reserved.
3+
// Use of this source code is governed by a BSD-style license that can be
4+
// found in the LICENSE file.
5+
6+
import type * as Trace from '../../../models/trace/trace.js';
7+
8+
export type PerfIssueSeverity = 'info' | 'warning' | 'error';
9+
10+
export interface RNPerfIssueDetail {
11+
name: string;
12+
description?: string;
13+
severity?: PerfIssueSeverity;
14+
learnMoreUrl?: string;
15+
}
16+
17+
export interface PerfIssueEvent {
18+
event: Trace.Types.Events.Event;
19+
timestampMs: number;
20+
}
21+
22+
export interface AggregatedPerfIssue {
23+
name: string;
24+
description?: string;
25+
severity: PerfIssueSeverity;
26+
learnMoreUrl?: string;
27+
events: PerfIssueEvent[];
28+
count: number;
29+
}

front_end/panels/timeline/components/Sidebar.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import * as UI from '../../../ui/legacy/legacy.js';
99

1010
import {SidebarAnnotationsTab} from './SidebarAnnotationsTab.js';
1111
import {SidebarInsightsTab} from './SidebarInsightsTab.js';
12+
import {SidebarRNPerfSignalsTab} from './SidebarRNPerfSignalsTab.js';
1213

1314
export interface ActiveInsight {
1415
model: Trace.Insights.Types.InsightModel;
@@ -40,6 +41,7 @@ declare global {
4041
export const enum SidebarTabs {
4142
INSIGHTS = 'insights',
4243
ANNOTATIONS = 'annotations',
44+
PERF_SIGNALS = 'perf-signals',
4345
}
4446
export const DEFAULT_SIDEBAR_TAB = SidebarTabs.INSIGHTS;
4547

@@ -52,6 +54,9 @@ export class SidebarWidget extends UI.Widget.VBox {
5254
#insightsView = new InsightsView();
5355
#annotationsView = new AnnotationsView();
5456

57+
#perfIssuesTabEnabled = false;
58+
#rnPerfSignalsView = new RNPerfSignalsView();
59+
5560
/**
5661
* Track if the user has opened the sidebar before. We do this so that the
5762
* very first time they record/import a trace after the sidebar ships, we can
@@ -72,6 +77,8 @@ export class SidebarWidget extends UI.Widget.VBox {
7277

7378
// [RN] Disable Insights tab
7479
const showInsightsTab = !Root.Runtime.experiments.isEnabled(Root.Runtime.ExperimentName.REACT_NATIVE_SPECIFIC_UI);
80+
// [RN] Show experimental Performance Issues tab
81+
this.#perfIssuesTabEnabled = Root.Runtime.experiments.isEnabled(Root.Runtime.ExperimentName.REACT_NATIVE_SPECIFIC_UI);
7582

7683
if (showInsightsTab) {
7784
this.#tabbedPane.appendTab(
@@ -114,6 +121,25 @@ export class SidebarWidget extends UI.Widget.VBox {
114121

115122
setParsedTrace(parsedTrace: Trace.Handlers.Types.ParsedTrace|null, metadata: Trace.Types.File.MetaData|null): void {
116123
this.#insightsView.setParsedTrace(parsedTrace, metadata);
124+
this.#rnPerfSignalsView.setParsedTrace(parsedTrace);
125+
126+
// [RN] Show or remove the Perf Issues tab if there are issues present
127+
if (this.#perfIssuesTabEnabled) {
128+
const hasIssues = this.#rnPerfSignalsView.hasIssues();
129+
const tabExists = this.#tabbedPane.hasTab(SidebarTabs.PERF_SIGNALS);
130+
131+
if (hasIssues && !tabExists) {
132+
this.#tabbedPane.appendTab(
133+
SidebarTabs.PERF_SIGNALS, 'Performance Signals', this.#rnPerfSignalsView, undefined, undefined, false, true);
134+
this.#tabbedPane.selectTab(SidebarTabs.PERF_SIGNALS);
135+
} else if (!hasIssues && tabExists) {
136+
this.#tabbedPane.closeTab(SidebarTabs.PERF_SIGNALS);
137+
}
138+
}
139+
}
140+
141+
setSelectTimelineEventCallback(callback: (event: Trace.Types.Events.Event) => void): void {
142+
this.#rnPerfSignalsView.setSelectTimelineEventCallback(callback);
117143
}
118144

119145
setInsights(insights: Trace.Insights.Types.TraceInsightSets|null): void {
@@ -185,3 +211,26 @@ class AnnotationsView extends UI.Widget.VBox {
185211
return this.#component.deduplicatedAnnotations();
186212
}
187213
}
214+
215+
/** [RN] Experimental view for Performance Signals. */
216+
class RNPerfSignalsView extends UI.Widget.VBox {
217+
#component = new SidebarRNPerfSignalsTab();
218+
219+
constructor() {
220+
super();
221+
this.element.classList.add('sidebar-perf-signals');
222+
this.element.appendChild(this.#component);
223+
}
224+
225+
setParsedTrace(parsedTrace: Trace.Handlers.Types.ParsedTrace|null): void {
226+
this.#component.parsedTrace = parsedTrace;
227+
}
228+
229+
setSelectTimelineEventCallback(callback: (event: Trace.Types.Events.Event) => void): void {
230+
this.#component.setSelectTimelineEventCallback(callback);
231+
}
232+
233+
hasIssues(): boolean {
234+
return this.#component.hasIssues();
235+
}
236+
}
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
// Copyright (c) Meta Platforms, Inc. and affiliates.
2+
// Copyright 2024 The Chromium Authors. All rights reserved.
3+
// Use of this source code is governed by a BSD-style license that can be
4+
// found in the LICENSE file.
5+
6+
import '../../../ui/components/buttons/buttons.js';
7+
import '../../../ui/components/icon_button/icon_button.js';
8+
9+
import type * as Platform from '../../../core/platform/platform.js';
10+
import type * as Trace from '../../../models/trace/trace.js';
11+
import * as Buttons from '../../../ui/components/buttons/buttons.js';
12+
import * as UI from '../../../ui/legacy/legacy.js';
13+
import * as Lit from '../../../ui/lit/lit.js';
14+
15+
import type {AggregatedPerfIssue, PerfIssueEvent, PerfIssueSeverity} from './RNPerfIssueTypes.js';
16+
import styles from './sidebarRNPerfIssueItem.css.js';
17+
18+
const {html} = Lit;
19+
20+
interface SidebarRNPerfIssueItemData {
21+
issue: AggregatedPerfIssue;
22+
onEventSelected?: (event: Trace.Types.Events.Event) => void;
23+
}
24+
25+
export class SidebarRNPerfIssueItem extends HTMLElement {
26+
readonly #shadow = this.attachShadow({mode: 'open'});
27+
28+
#issue: AggregatedPerfIssue|null = null;
29+
#onEventSelected?: (event: Trace.Types.Events.Event) => void;
30+
#isOpen = false;
31+
32+
set data(data: SidebarRNPerfIssueItemData) {
33+
this.#issue = data.issue;
34+
this.#onEventSelected = data.onEventSelected;
35+
this.#render();
36+
}
37+
38+
#toggleOpen(event: Event): void {
39+
event.preventDefault();
40+
this.#isOpen = !this.#isOpen;
41+
this.#render();
42+
}
43+
44+
#onEventClick(issueEvent: PerfIssueEvent): void {
45+
if (this.#onEventSelected) {
46+
this.#onEventSelected(issueEvent.event);
47+
}
48+
}
49+
50+
#onEventKeyDown(event: KeyboardEvent, issueEvent: PerfIssueEvent): void {
51+
if (event.key === 'Enter' || event.key === ' ') {
52+
event.preventDefault();
53+
this.#onEventClick(issueEvent);
54+
}
55+
}
56+
57+
#renderDropdownIcon(isOpen: boolean): Lit.TemplateResult {
58+
return html`
59+
<devtools-button .data=${{
60+
variant: Buttons.Button.Variant.ICON,
61+
iconName: 'chevron-right',
62+
size: Buttons.Button.Size.SMALL,
63+
} as Buttons.Button.ButtonData} class="dropdown-icon ${isOpen ? 'open' : ''}"></devtools-button>
64+
`;
65+
}
66+
67+
#renderSeverityIcon(severity: PerfIssueSeverity): Lit.TemplateResult {
68+
const iconData = {
69+
error: {iconName: 'issue-cross-filled', color: 'var(--icon-error)'},
70+
warning: {iconName: 'issue-exclamation-filled', color: 'var(--icon-warning)'},
71+
info: {iconName: 'issue-text-filled', color: 'var(--icon-info)'},
72+
} as const;
73+
const {iconName, color} = iconData[severity];
74+
return html`
75+
<devtools-icon .data=${{iconName, color, width: '16px', height: '16px'}}></devtools-icon>
76+
`;
77+
}
78+
79+
#render(): void {
80+
const issue = this.#issue;
81+
if (!issue) {
82+
Lit.render(Lit.nothing, this.#shadow, {host: this});
83+
return;
84+
}
85+
86+
const formatter = new Intl.NumberFormat(undefined, {
87+
minimumFractionDigits: 1,
88+
maximumFractionDigits: 1,
89+
style: 'decimal',
90+
});
91+
const contents = html`
92+
<style>${styles.cssText}</style>
93+
<details ?open=${this.#isOpen}>
94+
<summary @click=${(e: Event) => this.#toggleOpen(e)} class="issue-summary">
95+
${this.#renderDropdownIcon(this.#isOpen)}
96+
${this.#renderSeverityIcon(issue.severity)}
97+
<span class="event-count-pill">${issue.count}</span>
98+
<div class="issue-info">
99+
<div class="issue-name">${issue.name}</div>
100+
${issue.description ? html`<div class="issue-description">${issue.description}</div>` : ''}
101+
${issue.learnMoreUrl ? html`
102+
<div class="issue-learn-more">
103+
${UI.XLink.XLink.create(issue.learnMoreUrl as Platform.DevToolsPath.UrlString, 'Learn more')}
104+
</div>
105+
` : ''}
106+
</div>
107+
</summary>
108+
<div class="issue-content">
109+
<div class="event-list event-list-${issue.severity}">
110+
${issue.events.map((issueEvent, i) => {
111+
return html`
112+
<div class="event-item"
113+
tabindex="0"
114+
role="button"
115+
aria-label="Navigate to item ${i + 1} in timeline"
116+
@click=${() => this.#onEventClick(issueEvent)}
117+
@keydown=${(event: KeyboardEvent) => this.#onEventKeyDown(event, issueEvent)}>
118+
<div class="event-name">${issueEvent.event.name}</div>
119+
<div class="event-timestamp">${formatter.format(issueEvent.timestampMs)} ms</div>
120+
</div>`;
121+
})}
122+
</div>
123+
</div>
124+
</details>
125+
`;
126+
Lit.render(contents, this.#shadow, {host: this});
127+
}
128+
}
129+
130+
customElements.define('devtools-performance-sidebar-perf-issue-item', SidebarRNPerfIssueItem);
131+
132+
declare global {
133+
interface HTMLElementTagNameMap {
134+
'devtools-performance-sidebar-perf-issue-item': SidebarRNPerfIssueItem;
135+
}
136+
}

0 commit comments

Comments
 (0)