Skip to content

Commit d2b5da9

Browse files
paulirishDevtools-frontend LUCI CQ
authored andcommitted
Bi-directional hover dimming between Flamechart and TreeViews
When hovering items in the tree views, the flame chart items should react. And vice-versa. This CL enables that for immediate feedback and sits behind behind the existing dimming experiment. Video: https://crbug.com/380038346#comment1 Also add additional intended dimming: dim frames, decorations, etc. Bug: 380038346 Change-Id: I3fa746a71cb421c26719d452d9581243640006d0 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/5996112 Auto-Submit: Paul Irish <[email protected]> Reviewed-by: Connor Clark <[email protected]> Commit-Queue: Paul Irish <[email protected]>
1 parent 16f5a6c commit d2b5da9

File tree

9 files changed

+172
-67
lines changed

9 files changed

+172
-67
lines changed

front_end/core/common/Color.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2154,12 +2154,12 @@ export class Legacy implements Color {
21542154

21552155
/**
21562156
* Returns a new color using the NTSC formula for making a RGB color grayscale.
2157-
* Note: this is ill-defined for colors with alpha, and alpha is not modified.
2157+
* Note: We override with an alpha of 50% to enhance the dimming effect.
21582158
*/
21592159
grayscale(): Legacy {
2160-
const [r, g, b, a] = this.#rgbaInternal;
2160+
const [r, g, b] = this.#rgbaInternal;
21612161
const gray = r * 0.299 + g * 0.587 + b * 0.114;
2162-
return new Legacy([gray, gray, gray, a], Format.RGBA);
2162+
return new Legacy([gray, gray, gray, 0.5], Format.RGBA);
21632163
}
21642164

21652165
setAlpha(alpha: number): Legacy {

front_end/models/trace/extras/TraceTree.ts

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,23 @@ export class Node {
1313
totalTime: number;
1414
selfTime: number;
1515
id: string|symbol;
16-
event: Types.Events.Event|null;
16+
/** The first trace event encountered that necessitated the creation of this tree node. */
17+
event: Types.Events.Event;
18+
/** All of the trace events associated with this aggregate node.
19+
* Minor: In the case of Event Log (EventsTimelineTreeView), the node is not aggregate and this will only hold 1 event, the same that's in this.event
20+
*/
21+
events: Types.Events.Event[];
1722
parent!: Node|null;
1823
groupId: string;
1924
isGroupNodeInternal: boolean;
2025
depth: number;
2126

22-
constructor(id: string|symbol, event: Types.Events.Event|null) {
27+
constructor(id: string|symbol, event: Types.Events.Event) {
2328
this.totalTime = 0;
2429
this.selfTime = 0;
2530
this.id = id;
2631
this.event = event;
32+
this.events = [event];
2733

2834
this.groupId = '';
2935
this.isGroupNodeInternal = false;
@@ -67,7 +73,7 @@ export class TopDownNode extends Node {
6773
childrenInternal: ChildrenCache|null;
6874
override parent: TopDownNode|null;
6975

70-
constructor(id: string|symbol, event: Types.Events.Event|null, parent: TopDownNode|null) {
76+
constructor(id: string|symbol, event: Types.Events.Event, parent: TopDownNode|null) {
7177
super(id, event);
7278
this.root = parent && parent.root;
7379
this.hasChildrenInternal = false;
@@ -180,6 +186,8 @@ export class TopDownNode extends Node {
180186
node = new TopDownNode(id, e, self);
181187
node.groupId = groupId;
182188
children.set(id, node);
189+
} else {
190+
node.events.push(e);
183191
}
184192
node.selfTime += duration;
185193
node.totalTime += duration;
@@ -238,8 +246,6 @@ export class TopDownNode extends Node {
238246

239247
export class TopDownRootNode extends TopDownNode {
240248
readonly filter: (e: Types.Events.Event) => boolean;
241-
/** This is all events passed in to create the tree, and it's very likely that it included events outside of the passed startTime/endTime as that filtering is done in `Helpers.Trace.forEachEvent` */
242-
readonly events: Types.Events.Event[];
243249
readonly startTime: Types.Timing.MilliSeconds;
244250
readonly endTime: Types.Timing.MilliSeconds;
245251
eventGroupIdCallback: ((arg0: Types.Events.Event) => string)|null|undefined;
@@ -253,7 +259,8 @@ export class TopDownRootNode extends TopDownNode {
253259
events: Types.Events.Event[], filters: TraceFilter[], startTime: Types.Timing.MilliSeconds,
254260
endTime: Types.Timing.MilliSeconds, doNotAggregate?: boolean,
255261
eventGroupIdCallback?: ((arg0: Types.Events.Event) => string)|null, includeInstantEvents?: boolean) {
256-
super('', null, null);
262+
super('', events[0], null);
263+
this.event = events[0];
257264
this.root = this;
258265
this.events = events;
259266
this.filter = (e: Types.Events.Event): boolean => filters.every(f => f.accept(e));
@@ -281,14 +288,13 @@ export class TopDownRootNode extends TopDownNode {
281288
}
282289
const groupNodes = new Map<string, GroupNode>();
283290
for (const node of flatNodes.values()) {
284-
if (!node.event) {
285-
continue;
286-
}
287291
const groupId = this.eventGroupIdCallback(node.event);
288292
let groupNode = groupNodes.get(groupId);
289293
if (!groupNode) {
290-
groupNode = new GroupNode(groupId, this, node.event);
294+
groupNode = new GroupNode(groupId, this, node.events);
291295
groupNodes.set(groupId, groupNode);
296+
} else {
297+
groupNode.events.push(...node.events);
292298
}
293299
groupNode.addChild(node as BottomUpNode, node.selfTime, node.totalTime);
294300
}
@@ -303,7 +309,6 @@ export class TopDownRootNode extends TopDownNode {
303309

304310
export class BottomUpRootNode extends Node {
305311
private childrenInternal: ChildrenCache|null;
306-
readonly events: Types.Events.Event[];
307312
private textFilter: TraceFilter;
308313
readonly filter: (e: Types.Events.Event) => boolean;
309314
readonly startTime: Types.Timing.MilliSeconds;
@@ -315,7 +320,7 @@ export class BottomUpRootNode extends Node {
315320
events: Types.Events.Event[], textFilter: TraceFilter, filters: TraceFilter[],
316321
startTime: Types.Timing.MilliSeconds, endTime: Types.Timing.MilliSeconds,
317322
eventGroupIdCallback: ((arg0: Types.Events.Event) => string)|null) {
318-
super('', null);
323+
super('', events[0]);
319324
this.childrenInternal = null;
320325
this.events = events;
321326
this.textFilter = textFilter;
@@ -388,6 +393,8 @@ export class BottomUpRootNode extends Node {
388393
if (!node) {
389394
node = new BottomUpNode(root, id, event, false, root);
390395
nodeById.set(id, node);
396+
} else {
397+
node.events.push(event);
391398
}
392399
node.selfTime += selfTimeStack.pop() || 0;
393400
if (firstNodeStack.pop()) {
@@ -415,14 +422,13 @@ export class BottomUpRootNode extends Node {
415422
}
416423
const groupNodes = new Map<string, GroupNode>();
417424
for (const node of flatNodes.values()) {
418-
if (!node.event) {
419-
continue;
420-
}
421425
const groupId = this.eventGroupIdCallback(node.event);
422426
let groupNode = groupNodes.get(groupId);
423427
if (!groupNode) {
424-
groupNode = new GroupNode(groupId, this, node.event);
428+
groupNode = new GroupNode(groupId, this, node.events);
425429
groupNodes.set(groupId, groupNode);
430+
} else {
431+
groupNode.events.push(...node.events);
426432
}
427433
groupNode.addChild(node as BottomUpNode, node.selfTime, node.selfTime);
428434
}
@@ -433,9 +439,11 @@ export class BottomUpRootNode extends Node {
433439
export class GroupNode extends Node {
434440
private readonly childrenInternal: ChildrenCache;
435441
override isGroupNodeInternal: boolean;
442+
override events: Types.Events.Event[];
436443

437-
constructor(id: string, parent: BottomUpRootNode|TopDownRootNode, event: Types.Events.Event) {
438-
super(id, event);
444+
constructor(id: string, parent: BottomUpRootNode|TopDownRootNode, events: Types.Events.Event[]) {
445+
super(id, events[0]);
446+
this.events = events;
439447
this.childrenInternal = new Map();
440448
this.parent = parent;
441449
this.isGroupNodeInternal = true;
@@ -539,6 +547,8 @@ export class BottomUpNode extends Node {
539547
const hasChildren = eventStack.length > self.depth;
540548
node = new BottomUpNode(self.root, childId, event, hasChildren, self);
541549
nodeById.set(childId, node);
550+
} else {
551+
node.events.push(e);
542552
}
543553
const actualEndTime = currentEndTime !== undefined ? Math.min(currentEndTime, endTime) : endTime;
544554
const totalTime = actualEndTime - Math.max(currentStartTime, lastTimeMarker);

front_end/panels/timeline/TimelineDetailsView.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import {
2727
type TimelineSelection,
2828
} from './TimelineSelection.js';
2929
import {TimelineSelectorStatsView} from './TimelineSelectorStatsView.js';
30-
import {BottomUpTimelineTreeView, CallTreeTimelineTreeView, type TimelineTreeView} from './TimelineTreeView.js';
30+
import {BottomUpTimelineTreeView, CallTreeTimelineTreeView, TimelineTreeView} from './TimelineTreeView.js';
3131
import {TimelineDetailsContentHelper, TimelineUIUtils} from './TimelineUIUtils.js';
3232

3333
const UIStrings = {
@@ -68,7 +68,8 @@ const UIStrings = {
6868
};
6969
const str_ = i18n.i18n.registerUIStrings('panels/timeline/TimelineDetailsView.ts', UIStrings);
7070
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
71-
export class TimelineDetailsView extends UI.Widget.VBox {
71+
export class TimelineDetailsView extends
72+
Common.ObjectWrapper.eventMixin<TimelineTreeView.EventTypes, typeof UI.Widget.VBox>(UI.Widget.VBox) {
7273
private readonly detailsLinkifier: Components.Linkifier.Linkifier;
7374
private tabbedPane: UI.TabbedPane.TabbedPane;
7475
private readonly defaultDetailsWidget: UI.Widget.VBox;
@@ -126,6 +127,12 @@ export class TimelineDetailsView extends UI.Widget.VBox {
126127
this.appendTab(Tab.EventLog, i18nString(UIStrings.eventLog), eventsView);
127128
this.rangeDetailViews.set(Tab.EventLog, eventsView);
128129

130+
this.rangeDetailViews.values().forEach(view => {
131+
view.addEventListener(
132+
TimelineTreeView.Events.TREE_ROW_HOVERED,
133+
node => this.dispatchEventToListeners(TimelineTreeView.Events.TREE_ROW_HOVERED, node.data));
134+
});
135+
129136
this.#networkRequestDetails =
130137
new TimelineComponents.NetworkRequestDetails.NetworkRequestDetails(this.detailsLinkifier);
131138

@@ -153,6 +160,12 @@ export class TimelineDetailsView extends UI.Widget.VBox {
153160
return this.defaultDetailsContentElement;
154161
}
155162

163+
revealEventInTreeView(event: Trace.Types.Events.Event|null): void {
164+
if (this.tabbedPane.visibleView instanceof TimelineTreeView) {
165+
this.tabbedPane.visibleView.highlightEventInTree(event);
166+
}
167+
}
168+
156169
async #onTraceBoundsChange(event: TraceBounds.TraceBounds.StateChangedEvent): Promise<void> {
157170
if (event.updateType === 'MINIMAP_BOUNDS') {
158171
// If new minimap bounds are set, we might need to update the selected entry summary because

front_end/panels/timeline/TimelineFlameChartDataProvider.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -776,6 +776,8 @@ export class TimelineFlameChartDataProvider extends Common.ObjectWrapper.ObjectW
776776
additionalContent.push(...popoverInfo.additionalElements);
777777
}
778778

779+
this.dispatchEventToListeners(Events.FLAME_CHART_ITEM_HOVERED, event);
780+
779781
} else if (entryType === EntryType.FRAME) {
780782
const frame = (this.entryData[entryIndex] as Trace.Types.Events.LegacyTimelineFrame);
781783
time =
@@ -790,6 +792,7 @@ export class TimelineFlameChartDataProvider extends Common.ObjectWrapper.ObjectW
790792
title = i18nString(UIStrings.frame);
791793
}
792794
} else {
795+
this.dispatchEventToListeners(Events.FLAME_CHART_ITEM_HOVERED, null);
793796
return null;
794797
}
795798

@@ -913,12 +916,12 @@ export class TimelineFlameChartDataProvider extends Common.ObjectWrapper.ObjectW
913916

914917
private drawFrame(
915918
entryIndex: number, context: CanvasRenderingContext2D, barX: number, barY: number, barWidth: number,
916-
barHeight: number): void {
919+
barHeight: number, transformColor: (color: string) => string): void {
917920
const hPadding = 1;
918921
const frame = this.entryData[entryIndex] as Trace.Types.Events.LegacyTimelineFrame;
919922
barX += hPadding;
920923
barWidth -= 2 * hPadding;
921-
context.fillStyle = this.entryColor(entryIndex);
924+
context.fillStyle = transformColor(this.entryColor(entryIndex));
922925

923926
if (frame.dropped) {
924927
if (frame.isPartial) {
@@ -973,11 +976,12 @@ export class TimelineFlameChartDataProvider extends Common.ObjectWrapper.ObjectW
973976

974977
decorateEntry(
975978
entryIndex: number, context: CanvasRenderingContext2D, text: string|null, barX: number, barY: number,
976-
barWidth: number, barHeight: number, unclippedBarX: number, timeToPixelRatio: number): boolean {
979+
barWidth: number, barHeight: number, unclippedBarX: number, timeToPixelRatio: number,
980+
transformColor: (color: string) => string): boolean {
977981
const entryType = this.#entryTypeForIndex(entryIndex);
978982

979983
if (entryType === EntryType.FRAME) {
980-
this.drawFrame(entryIndex, context, barX, barY, barWidth, barHeight);
984+
this.drawFrame(entryIndex, context, barX, barY, barWidth, barHeight, transformColor);
981985
return true;
982986
}
983987

@@ -1317,10 +1321,12 @@ export const InstantEventVisibleDurationMs = Trace.Types.Timing.MilliSeconds(0.0
13171321

13181322
export const enum Events {
13191323
DATA_CHANGED = 'DataChanged',
1324+
FLAME_CHART_ITEM_HOVERED = 'FlameChartItemHovered',
13201325
}
13211326

13221327
export type EventTypes = {
13231328
[Events.DATA_CHANGED]: void,
1329+
[Events.FLAME_CHART_ITEM_HOVERED]: Trace.Types.Events.Event|null,
13241330
};
13251331

13261332
// an entry is a trace event, they are classified into "entry types"

front_end/panels/timeline/TimelineFlameChartView.ts

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ import {
3939
selectionIsRange,
4040
type TimelineSelection,
4141
} from './TimelineSelection.js';
42-
import {AggregatedTimelineTreeView} from './TimelineTreeView.js';
42+
import {AggregatedTimelineTreeView, TimelineTreeView} from './TimelineTreeView.js';
4343
import type {TimelineMarkerStyle} from './TimelineUIUtils.js';
4444

4545
const UIStrings = {
@@ -53,8 +53,9 @@ const UIStrings = {
5353
const str_ = i18n.i18n.registerUIStrings('panels/timeline/TimelineFlameChartView.ts', UIStrings);
5454
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
5555

56-
export class TimelineFlameChartView extends UI.Widget.VBox implements PerfUI.FlameChart.FlameChartDelegate,
57-
UI.SearchableView.Searchable {
56+
export class TimelineFlameChartView extends
57+
Common.ObjectWrapper.eventMixin<TimelineTreeView.EventTypes, typeof UI.Widget.VBox>(UI.Widget.VBox)
58+
implements PerfUI.FlameChart.FlameChartDelegate, UI.SearchableView.Searchable {
5859
private readonly delegate: TimelineModeViewDelegate;
5960
/**
6061
* Tracks the indexes of matched entries when the user searches the panel.
@@ -165,6 +166,10 @@ export class TimelineFlameChartView extends UI.Widget.VBox implements PerfUI.Fla
165166
this.mainDataProvider = new TimelineFlameChartDataProvider();
166167
this.mainDataProvider.addEventListener(
167168
TimelineFlameChartDataProviderEvents.DATA_CHANGED, () => this.mainFlameChart.scheduleUpdate());
169+
this.mainDataProvider.addEventListener(
170+
TimelineFlameChartDataProviderEvents.FLAME_CHART_ITEM_HOVERED,
171+
e => this.detailsView.revealEventInTreeView(e.data));
172+
168173
this.mainFlameChart = new PerfUI.FlameChart.FlameChart(this.mainDataProvider, this, {
169174
groupExpansionSetting: mainViewGroupExpansionSetting,
170175
// The TimelineOverlays are used for selected elements
@@ -312,6 +317,19 @@ export class TimelineFlameChartView extends UI.Widget.VBox implements PerfUI.Fla
312317
PerfUI.FlameChart.Events.ENTRIES_LINK_ANNOTATION_CREATED, this.#onNetworkEntriesLinkAnnotationCreated, this);
313318
}
314319

320+
this.detailsView.addEventListener(TimelineTreeView.Events.TREE_ROW_HOVERED, node => {
321+
if (!Root.Runtime.experiments.isEnabled(Root.Runtime.ExperimentName.TIMELINE_DIM_UNRELATED_EVENTS)) {
322+
return;
323+
}
324+
const events = node?.data?.events;
325+
if (events) {
326+
this.#dimInsightRelatedEvents(events);
327+
} else {
328+
this.mainFlameChart.disableDimming();
329+
this.networkFlameChart.disableDimming();
330+
}
331+
});
332+
315333
/**
316334
* NOTE: ENTRY_SELECTED, ENTRY_INVOKED and ENTRY_HOVERED are not always super obvious:
317335
* ENTRY_SELECTED: is KEYBOARD ONLY selection of events (e.g. navigating through the flamechart with your arrow keys)
@@ -355,8 +373,8 @@ export class TimelineFlameChartView extends UI.Widget.VBox implements PerfUI.Fla
355373

356374
#dimInsightRelatedEvents(relatedEvents: Trace.Types.Events.Event[]): void {
357375
// Dim all events except those related to the active insight.
358-
const relevantMainEvents = relatedEvents.map(event => this.mainDataProvider.indexForEvent(event) ?? -1);
359-
const relevantNetworkEvents = relatedEvents.map(event => this.networkDataProvider.indexForEvent(event) ?? -1);
376+
const relatedMainIndices = relatedEvents.map(event => this.mainDataProvider.indexForEvent(event) ?? -1);
377+
const relatedNetworkIndices = relatedEvents.map(event => this.networkDataProvider.indexForEvent(event) ?? -1);
360378

361379
// Further, overlays defining a trace bounds do not dim an event that falls within those bounds.
362380
for (const overlay of this.#currentInsightOverlays) {
@@ -382,15 +400,15 @@ export class TimelineFlameChartView extends UI.Widget.VBox implements PerfUI.Fla
382400
if (overlayEvent) {
383401
if (this.mainDataProvider.indexForEvent(overlayEvent) !== null) {
384402
provider = this.mainDataProvider;
385-
relevantEvents = relevantMainEvents;
403+
relevantEvents = relatedMainIndices;
386404
} else if (this.networkDataProvider.indexForEvent(overlayEvent) !== null) {
387405
provider = this.networkDataProvider;
388-
relevantEvents = relevantNetworkEvents;
406+
relevantEvents = relatedNetworkIndices;
389407
}
390408
} else if (overlay.type === 'TIMESPAN_BREAKDOWN') {
391409
// For this overlay type, if there is no associated event it is rendered on mainFlameChart.
392410
provider = this.mainDataProvider;
393-
relevantEvents = relevantMainEvents;
411+
relevantEvents = relatedMainIndices;
394412
}
395413

396414
if (!provider || !relevantEvents) {
@@ -399,8 +417,8 @@ export class TimelineFlameChartView extends UI.Widget.VBox implements PerfUI.Fla
399417

400418
relevantEvents.push(...provider.search(bounds).map(r => r.index));
401419
}
402-
this.mainFlameChart.enableDimming(relevantMainEvents);
403-
this.networkFlameChart.enableDimming(relevantNetworkEvents);
420+
this.mainFlameChart.enableDimming(relatedMainIndices);
421+
this.networkFlameChart.enableDimming(relatedNetworkIndices);
404422
}
405423

406424
setOverlays(overlays: Overlays.Overlays.TimelineOverlay[], options: Overlays.Overlays.TimelineOverlaySetOptions):

0 commit comments

Comments
 (0)