Skip to content

Commit 9ce0d9a

Browse files
jackfranklinDevtools-frontend LUCI CQ
authored andcommitted
RPP: select the event when an annotation label is clicked
With this CL when a user clicks on an annotation label in the timeline we now will select the associated entry. This is a useful change because when the event is small it can be hard to select, especially when zoomed out. But the label is always going to be (relatively) large compared to the event, so let's allow them to click the label. Bug: 383120286 Fixed: 388224764 Change-Id: Ia58c2ffd8e075087480e651e31e0fea186039a54 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6148172 Reviewed-by: Alina Varkki <[email protected]> Commit-Queue: Alina Varkki <[email protected]> Auto-Submit: Jack Franklin <[email protected]>
1 parent 39e5133 commit 9ce0d9a

File tree

6 files changed

+115
-22
lines changed

6 files changed

+115
-22
lines changed

front_end/panels/timeline/TimelineFlameChartView.ts

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,8 @@ export const SORT_ORDER_PAGE_LOAD_MARKERS: Readonly<Record<string, number>> = {
7171
// on top of each other.
7272
const TIMESTAMP_THRESHOLD_MS = Trace.Types.Timing.MicroSeconds(10);
7373

74-
export class TimelineFlameChartView extends
75-
Common.ObjectWrapper.eventMixin<TimelineTreeView.EventTypes, typeof UI.Widget.VBox>(UI.Widget.VBox)
76-
implements PerfUI.FlameChart.FlameChartDelegate, UI.SearchableView.Searchable {
74+
export class TimelineFlameChartView extends Common.ObjectWrapper.eventMixin<EventTypes, typeof UI.Widget.VBox>(
75+
UI.Widget.VBox) implements PerfUI.FlameChart.FlameChartDelegate, UI.SearchableView.Searchable {
7776
private readonly delegate: TimelineModeViewDelegate;
7877
/**
7978
* Tracks the indexes of matched entries when the user searches the panel.
@@ -258,6 +257,16 @@ export class TimelineFlameChartView extends
258257
},
259258
});
260259

260+
this.#overlays.addEventListener(Overlays.Overlays.EntryLabelMouseClick.eventName, event => {
261+
const {overlay} = (event as Overlays.Overlays.EntryLabelMouseClick);
262+
this.dispatchEventToListeners(
263+
Events.ENTRY_LABEL_ANNOTATION_CLICKED,
264+
{
265+
entry: overlay.entry,
266+
},
267+
);
268+
});
269+
261270
this.#overlays.addEventListener(Overlays.Overlays.AnnotationOverlayActionEvent.eventName, event => {
262271
const {overlay, action} = (event as Overlays.Overlays.AnnotationOverlayActionEvent);
263272
if (action === 'Remove') {
@@ -1680,3 +1689,12 @@ export function groupForLevel(groups: PerfUI.FlameChart.Group[], level: number):
16801689
});
16811690
return groupForLevel ?? null;
16821691
}
1692+
1693+
export const enum Events {
1694+
ENTRY_LABEL_ANNOTATION_CLICKED = 'EntryLabelAnnotationClicked',
1695+
}
1696+
export type EventTypes = {
1697+
[Events.ENTRY_LABEL_ANNOTATION_CLICKED]: {
1698+
entry: Trace.Types.Events.Event,
1699+
},
1700+
};

front_end/panels/timeline/TimelinePanel.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ import * as Overlays from './overlays/overlays.js';
6868
import {cpuprofileJsonGenerator, traceJsonGenerator} from './SaveFileFormatter.js';
6969
import {type Client, TimelineController} from './TimelineController.js';
7070
import type {TimelineFlameChartDataProvider} from './TimelineFlameChartDataProvider.js';
71-
import {TimelineFlameChartView} from './TimelineFlameChartView.js';
71+
import {Events as TimelineFlameChartViewEvents, TimelineFlameChartView} from './TimelineFlameChartView.js';
7272
import {TimelineHistoryManager} from './TimelineHistoryManager.js';
7373
import {TimelineLoader} from './TimelineLoader.js';
7474
import {TimelineMiniMap} from './TimelineMiniMap.js';
@@ -592,6 +592,11 @@ export class TimelinePanel extends UI.Panel.Panel implements Client, TimelineMod
592592
this.flameChart.getMainFlameChart().addEventListener(
593593
PerfUI.FlameChart.Events.ENTRY_HOVERED, this.#onMainEntryHovered);
594594

595+
this.flameChart.addEventListener(TimelineFlameChartViewEvents.ENTRY_LABEL_ANNOTATION_CLICKED, event => {
596+
const selection = selectionFromEvent(event.data.entry);
597+
this.select(selection);
598+
});
599+
595600
this.searchableViewInternal = new UI.SearchableView.SearchableView(this.flameChart, null);
596601
this.searchableViewInternal.setMinimumSize(0, 100);
597602
this.searchableViewInternal.setMinimalSearchQuerySize(2); // At 1 it can introduce a bit of jank.

front_end/panels/timeline/overlays/OverlaysImpl.test.ts

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

55
import * as Trace from '../../../models/trace/trace.js';
6+
import {dispatchClickEvent} from '../../../testing/DOMHelpers.js';
67
import {describeWithEnvironment} from '../../../testing/EnvironmentHelpers.js';
78
import {
89
makeInstantEvent,
@@ -396,6 +397,40 @@ describeWithEnvironment('Overlays', () => {
396397
assert.isOk(overlayDOM);
397398
});
398399

400+
it('dispatches an event when the entry label overlay is clicked', async function() {
401+
const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev.json.gz');
402+
const {overlays, container, charts} = setupChartWithDimensionsAndAnnotationOverlayListeners(parsedTrace);
403+
const event = charts.mainProvider.eventByIndex?.(50);
404+
assert.isOk(event);
405+
406+
overlays.add({
407+
type: 'ENTRY_LABEL',
408+
entry: event,
409+
label: 'entry label',
410+
});
411+
await overlays.update();
412+
413+
// Ensure that the overlay was created.
414+
const overlayDOM = container.querySelector<HTMLElement>('.overlay-type-ENTRY_LABEL');
415+
assert.isOk(overlayDOM);
416+
417+
const overlayClick = new Promise<Overlays.Overlays.EntryLabel>(resolve => {
418+
overlays.addEventListener(Overlays.Overlays.EntryLabelMouseClick.eventName, e => {
419+
const event = e as Overlays.Overlays.EntryLabelMouseClick;
420+
resolve(event.overlay);
421+
}, {once: true});
422+
});
423+
424+
dispatchClickEvent(overlayDOM);
425+
const overlayFromEvent = await overlayClick;
426+
// Check that the event was dispatched on the right overlay.
427+
assert.deepEqual(overlayFromEvent, {
428+
type: 'ENTRY_LABEL',
429+
entry: event,
430+
label: 'entry label',
431+
});
432+
});
433+
399434
it('toggles overlays container display', async function() {
400435
const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev.json.gz');
401436
const {overlays, container} = setupChartWithDimensionsAndAnnotationOverlayListeners(parsedTrace);

front_end/panels/timeline/overlays/OverlaysImpl.ts

Lines changed: 30 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,13 @@ export class TimeRangeMouseOutEvent extends Event {
387387
}
388388
}
389389

390+
export class EntryLabelMouseClick extends Event {
391+
static readonly eventName = 'entrylabelmouseclick';
392+
constructor(public overlay: EntryLabel) {
393+
super(EntryLabelMouseClick.eventName, {composed: true, bubbles: true});
394+
}
395+
}
396+
390397
interface EntriesLinkVisibleEntries {
391398
entryFrom: Trace.Types.Events.Event;
392399
entryTo: Trace.Types.Events.Event|undefined;
@@ -1443,12 +1450,12 @@ export class Overlays extends EventTarget {
14431450
}
14441451

14451452
#createElementForNewOverlay(overlay: TimelineOverlay): HTMLElement {
1446-
const div = document.createElement('div');
1447-
div.classList.add('overlay-item', `overlay-type-${overlay.type}`);
1453+
const overlayElement = document.createElement('div');
1454+
overlayElement.classList.add('overlay-item', `overlay-type-${overlay.type}`);
14481455

14491456
const jslogContext = jsLogContext(overlay);
14501457
if (jslogContext) {
1451-
div.setAttribute('jslog', `${VisualLogging.item(jslogContext)}`);
1458+
overlayElement.setAttribute('jslog', `${VisualLogging.item(jslogContext)}`);
14521459
}
14531460

14541461
switch (overlay.type) {
@@ -1463,16 +1470,21 @@ export class Overlays extends EventTarget {
14631470
overlay.label = newLabel;
14641471
this.dispatchEvent(new AnnotationOverlayActionEvent(overlay, 'Update'));
14651472
});
1466-
div.appendChild(component);
1467-
return div;
1473+
overlayElement.appendChild(component);
1474+
overlayElement.addEventListener('click', event => {
1475+
event.preventDefault();
1476+
event.stopPropagation();
1477+
this.dispatchEvent(new EntryLabelMouseClick(overlay));
1478+
});
1479+
return overlayElement;
14681480
}
14691481
case 'ENTRIES_LINK': {
14701482
const entries = this.#calculateFromAndToForEntriesLink(overlay);
14711483
if (entries === null) {
14721484
// For some reason, we don't have two entries we can draw between
14731485
// (can happen if the user has collapsed an icicle in the flame
14741486
// chart, or a track), so just draw an empty div.
1475-
return div;
1487+
return overlayElement;
14761488
}
14771489
const entryEndX = this.xPixelForEventEndOnChart(entries.entryFrom) ?? 0;
14781490
const entryStartX = this.xPixelForEventEndOnChart(entries.entryFrom) ?? 0;
@@ -1487,12 +1499,12 @@ export class Overlays extends EventTarget {
14871499
overlay.state = Trace.Types.File.EntriesLinkState.PENDING_TO_EVENT;
14881500
this.dispatchEvent(new AnnotationOverlayActionEvent(overlay, 'Update'));
14891501
});
1490-
div.appendChild(component);
1491-
return div;
1502+
overlayElement.appendChild(component);
1503+
return overlayElement;
14921504
}
14931505
case 'ENTRY_OUTLINE': {
1494-
div.classList.add(`outline-reason-${overlay.outlineReason}`);
1495-
return div;
1506+
overlayElement.classList.add(`outline-reason-${overlay.outlineReason}`);
1507+
return overlayElement;
14961508
}
14971509
case 'TIME_RANGE': {
14981510
const component = new Components.TimeRangeOverlay.TimeRangeOverlay(overlay.label);
@@ -1512,26 +1524,26 @@ export class Overlays extends EventTarget {
15121524
component.addEventListener('mouseout', () => {
15131525
this.dispatchEvent(new TimeRangeMouseOutEvent());
15141526
});
1515-
div.appendChild(component);
1516-
return div;
1527+
overlayElement.appendChild(component);
1528+
return overlayElement;
15171529
}
15181530
case 'TIMESPAN_BREAKDOWN': {
15191531
const component = new Components.TimespanBreakdownOverlay.TimespanBreakdownOverlay();
15201532
component.sections = overlay.sections;
15211533
component.canvasRect = this.#charts.mainChart.canvasBoundingClientRect();
15221534
component.isBelowEntry = overlay.renderLocation === 'BELOW_EVENT';
1523-
div.appendChild(component);
1524-
return div;
1535+
overlayElement.appendChild(component);
1536+
return overlayElement;
15251537
}
15261538
case 'TIMINGS_MARKER': {
15271539
const {color} = EntryStyles.markerDetailsForEvent(overlay.entries[0]);
15281540
const markersComponent = this.#createTimingsMarkerElement(overlay);
1529-
div.appendChild(markersComponent);
1530-
div.style.backgroundColor = color;
1531-
return div;
1541+
overlayElement.appendChild(markersComponent);
1542+
overlayElement.style.backgroundColor = color;
1543+
return overlayElement;
15321544
}
15331545
default: {
1534-
return div;
1546+
return overlayElement;
15351547
}
15361548
}
15371549
}

test/interactions/panels/performance/timeline/BUILD.gn

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import("../../../../../scripts/build/typescript/typescript.gni")
77
node_ts_library("timeline") {
88
sources = [
99
"animations_track_test.ts",
10+
"annotations_test.ts",
1011
"auction_worklets_tracks_test.ts",
1112
"flamechart_test.ts",
1213
"gpu_track_test.ts",
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
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+
import {assert} from 'chai';
5+
6+
import {click, waitFor} from '../../../../shared/helper.js';
7+
import {loadComponentDocExample} from '../../../helpers/shared.js';
8+
9+
describe('Performance panel annotations', () => {
10+
it('allows the user to click on an entry label to select the event', async () => {
11+
await loadComponentDocExample('performance_panel/basic.html?trace=web-dev-modifications');
12+
await waitFor('.timeline-flamechart');
13+
await waitFor<HTMLElement>('.overlay-item.overlay-type-ENTRY_LABEL');
14+
await click('.overlay-type-ENTRY_LABEL', {clickOptions: {offset: {x: 10, y: 10}}});
15+
16+
// Ensure that the right event hsa been selected by inspecting the details.
17+
const details = await waitFor('.timeline-details-view-body');
18+
const titleElem = await waitFor<HTMLElement>('.timeline-details-chip-title', details);
19+
const title = await titleElem.evaluate(e => e.innerText);
20+
assert.strictEqual(title, 'initializeApp');
21+
});
22+
});

0 commit comments

Comments
 (0)