Skip to content

Commit 5da583c

Browse files
Connor ClarkDevtools-frontend LUCI CQ
authored andcommitted
[RPP] Desaturate unrelated events when insight/search is active
This is added initially as an experiment, as there is a long tail of improvements to make this UX treatment seamless. Adds to FlameChart the ability to transform the colors used for drawing based on a set of currently relevant events. The active insight and the searching feature both utilize this now. FlameChart now maintains a "color => grayscale color" mapping. "getColorForEntry" is used to select a color that takes into account the currently configured relevant events. Appenders have their own custom drawing, and so are handled by passing them a "tranformColor" callback, which plugs into FlameChart's logic for determining if an event needed to be dimmed. The active insight derives which events are relevant from the provided overlays. Events associated with an overlay are relevant, as well as events that fall within the time range of an overlay that defines a range. Drive-by change: the LCPPhases insight now produces an overlay for the LCP request event, if any. https://i.imgur.com/BS8ANMc.png Bug: 370836271 Change-Id: I832405a439b9b631485be2e17465e1bfadbe56e1 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/5939713 Reviewed-by: Paul Irish <[email protected]> Commit-Queue: Connor Clark <[email protected]>
1 parent 32235d9 commit 5da583c

File tree

18 files changed

+185
-34
lines changed

18 files changed

+185
-34
lines changed

front_end/core/common/Color.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2152,6 +2152,16 @@ export class Legacy implements Color {
21522152
return new Legacy(rgba, Format.RGBA);
21532153
}
21542154

2155+
/**
2156+
* 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.
2158+
*/
2159+
grayscale(): Legacy {
2160+
const [r, g, b, a] = this.#rgbaInternal;
2161+
const gray = r * 0.299 + g * 0.587 + b * 0.114;
2162+
return new Legacy([gray, gray, gray, a], Format.RGBA);
2163+
}
2164+
21552165
setAlpha(alpha: number): Legacy {
21562166
const rgba: Color4D = [...this.#rgbaInternal];
21572167
rgba[3] = alpha;

front_end/core/host/UserMetrics.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1020,10 +1020,11 @@ export enum DevtoolsExperiments {
10201020
'extension-storage-viewer' = 100,
10211021
'floating-entry-points-for-ai-assistance' = 101,
10221022
'timeline-experimental-insights' = 102,
1023+
'timeline-dim-unrelated-events' = 103,
10231024
/* eslint-enable @typescript-eslint/naming-convention */
10241025

10251026
// Increment this when new experiments are added.
1026-
MAX_VALUE = 103,
1027+
MAX_VALUE = 104,
10271028
}
10281029

10291030
export const enum CSSPropertyDocumentation {

front_end/core/root/Runtime.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,8 @@ export const enum ExperimentName {
300300
EXTENSION_STORAGE_VIEWER = 'extension-storage-viewer',
301301
FLOATING_ENTRY_POINTS_FOR_AI_ASSISTANCE = 'floating-entry-points-for-ai-assistance',
302302
TIMELINE_EXPERIMENTAL_INSIGHTS = 'timeline-experimental-insights',
303+
TIMELINE_DIM_UNRELATED_EVENTS = 'timeline-dim-unrelated-events',
304+
// when adding to this enum, you'll need to also add to REGISTERED_EXPERIMENTS in EnvironmentHelpers.ts
303305
}
304306

305307
export interface AidaAvailability {

front_end/entrypoints/main/MainImpl.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,11 @@ export class MainImpl {
413413
'Performance panel: enable experimental performance insights',
414414
);
415415

416+
Root.Runtime.experiments.register(
417+
Root.Runtime.ExperimentName.TIMELINE_DIM_UNRELATED_EVENTS,
418+
'Performance panel: enable dimming unrelated events in performance insights and search results',
419+
);
420+
416421
Root.Runtime.experiments.enableExperimentsByDefault([
417422
'css-type-component-length-deprecate',
418423
Root.Runtime.ExperimentName.AUTOFILL_VIEW,

front_end/panels/timeline/LayoutShiftsTrackAppender.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -175,12 +175,12 @@ export class LayoutShiftsTrackAppender implements TrackAppender {
175175
// If the new CLS experience isn't on.. Continue to present that Shifts are 5ms long. (but now via drawOverrides)
176176
// TODO: Remove this when the experiment ships
177177
if (Trace.Types.Events.isLayoutShift(event)) {
178-
return (context, x, y, _width, levelHeight, timeToPosition) => {
178+
return (context, x, y, _width, levelHeight, timeToPosition, transformColor) => {
179179
const fakeDurMs = Trace.Helpers.Timing.microSecondsToMilliseconds(
180180
Trace.Types.Timing.MicroSeconds(event.ts + LAYOUT_SHIFT_SYNTHETIC_DURATION));
181181
const barEnd = timeToPosition(fakeDurMs);
182182
const barWidth = barEnd - x;
183-
context.fillStyle = this.colorForEvent(event);
183+
context.fillStyle = transformColor(this.colorForEvent(event));
184184
context.fillRect(x, y, barWidth - 0.5, levelHeight - 1);
185185
return {
186186
x,
@@ -202,7 +202,7 @@ export class LayoutShiftsTrackAppender implements TrackAppender {
202202
// A LS score of ~0 will create a diamond of minimum size (exactly 0 should not happen in practice)
203203
const bufferScale = 1 - Math.min(score / 0.10, 1);
204204

205-
return (context, x, y, _width, levelHeight) => {
205+
return (context, x, y, _width, levelHeight, _, transformColor) => {
206206
// levelHeight is 17px, so this math translates to a minimum diamond size of 5.6px tall.
207207
const maxBuffer = levelHeight / 3;
208208
const buffer = bufferScale * maxBuffer;
@@ -216,7 +216,7 @@ export class LayoutShiftsTrackAppender implements TrackAppender {
216216
context.lineTo(x, y + levelHeight - buffer);
217217
context.lineTo(x - halfSize + buffer, y + halfSize);
218218
context.closePath();
219-
context.fillStyle = this.colorForEvent(event);
219+
context.fillStyle = transformColor(this.colorForEvent(event));
220220

221221
context.fill();
222222
context.restore();
@@ -227,10 +227,10 @@ export class LayoutShiftsTrackAppender implements TrackAppender {
227227
};
228228
}
229229
if (Trace.Types.Events.isSyntheticLayoutShiftCluster(event)) {
230-
return (context, x, y, width, levelHeight) => {
230+
return (context, x, y, width, levelHeight, _, transformColor) => {
231231
const barHeight = levelHeight * 0.2;
232232
const barY = y + (levelHeight - barHeight) / 2 + 0.5;
233-
context.fillStyle = this.colorForEvent(event);
233+
context.fillStyle = transformColor(this.colorForEvent(event));
234234
context.fillRect(x, barY, width - 0.5, barHeight - 1);
235235
return {x, width, z: -1};
236236
};

front_end/panels/timeline/TimelineFlameChartDataProvider.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -644,7 +644,7 @@ export class TimelineFlameChartDataProvider extends Common.ObjectWrapper.ObjectW
644644

645645
search(
646646
visibleWindow: Trace.Types.Timing.TraceWindowMicroSeconds,
647-
filter: TimelineModel.TimelineModelFilter.TimelineModelFilter): PerfUI.FlameChart.DataProviderSearchResult[] {
647+
filter?: TimelineModel.TimelineModelFilter.TimelineModelFilter): PerfUI.FlameChart.DataProviderSearchResult[] {
648648
const results: PerfUI.FlameChart.DataProviderSearchResult[] = [];
649649
this.timelineData();
650650
for (let i = 0; i < this.entryData.length; ++i) {
@@ -665,7 +665,7 @@ export class TimelineFlameChartDataProvider extends Common.ObjectWrapper.ObjectW
665665
if (!Trace.Helpers.Timing.eventIsInBounds(entry, visibleWindow)) {
666666
continue;
667667
}
668-
if (filter.accept(entry, this.parsedTrace || undefined)) {
668+
if (!filter || filter.accept(entry, this.parsedTrace || undefined)) {
669669
const startTimeMilli = Trace.Helpers.Timing.microSecondsToMilliseconds(entry.ts);
670670
results.push({index: i, startTimeMilli, provider: 'main'});
671671
}

front_end/panels/timeline/TimelineFlameChartNetworkDataProvider.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -488,7 +488,7 @@ export class TimelineFlameChartNetworkDataProvider implements PerfUI.FlameChart.
488488
*/
489489
search(
490490
visibleWindow: Trace.Types.Timing.TraceWindowMicroSeconds,
491-
filter: TimelineModel.TimelineModelFilter.TimelineModelFilter,
491+
filter?: TimelineModel.TimelineModelFilter.TimelineModelFilter,
492492
): PerfUI.FlameChart.DataProviderSearchResult[] {
493493
const results: PerfUI.FlameChart.DataProviderSearchResult[] = [];
494494
for (let i = 0; i < this.#events.length; i++) {
@@ -501,7 +501,7 @@ export class TimelineFlameChartNetworkDataProvider implements PerfUI.FlameChart.
501501
continue;
502502
}
503503

504-
if (filter.accept(entry, this.#parsedTrace ?? undefined)) {
504+
if (!filter || filter.accept(entry, this.#parsedTrace ?? undefined)) {
505505
const startTimeMilli = Trace.Helpers.Timing.microSecondsToMilliseconds(entry.ts);
506506
results.push({startTimeMilli, index: i, provider: 'network'});
507507
}

front_end/panels/timeline/TimelineFlameChartView.ts

Lines changed: 61 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,6 @@ const UIStrings = {
4646
const str_ = i18n.i18n.registerUIStrings('panels/timeline/TimelineFlameChartView.ts', UIStrings);
4747
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
4848

49-
const MAX_HIGHLIGHTED_SEARCH_ELEMENTS: number = 200;
50-
5149
export class TimelineFlameChartView extends UI.Widget.VBox implements PerfUI.FlameChart.FlameChartDelegate,
5250
UI.SearchableView.Searchable {
5351
private readonly delegate: TimelineModeViewDelegate;
@@ -348,6 +346,56 @@ export class TimelineFlameChartView extends UI.Widget.VBox implements PerfUI.Fla
348346
return this.element;
349347
}
350348

349+
#dimInsightRelatedEvents(relatedEvents: Trace.Types.Events.Event[]): void {
350+
// Dim all events except those related to the active insight.
351+
const relevantMainEvents = relatedEvents.map(event => this.mainDataProvider.indexForEvent(event) ?? -1);
352+
const relevantNetworkEvents = relatedEvents.map(event => this.networkDataProvider.indexForEvent(event) ?? -1);
353+
354+
// Further, overlays defining a trace bounds do not dim an event that falls within those bounds.
355+
for (const overlay of this.#currentInsightOverlays) {
356+
let bounds;
357+
if (overlay.type === 'TIMESPAN_BREAKDOWN') {
358+
const firstSection = overlay.sections.at(0);
359+
const lastSection = overlay.sections.at(-1);
360+
if (firstSection && lastSection) {
361+
bounds = Trace.Helpers.Timing.traceWindowFromMicroSeconds(firstSection.bounds.min, lastSection.bounds.max);
362+
}
363+
} else if (overlay.type === 'TIME_RANGE') {
364+
bounds = overlay.bounds;
365+
}
366+
367+
if (!bounds) {
368+
continue;
369+
}
370+
371+
let provider, relevantEvents;
372+
373+
// Using a relevant event for the overlay, determine which provider this overlay is for.
374+
const overlayEvent = Overlays.Overlays.entriesForOverlay(overlay).at(0);
375+
if (overlayEvent) {
376+
if (this.mainDataProvider.indexForEvent(overlayEvent) !== null) {
377+
provider = this.mainDataProvider;
378+
relevantEvents = relevantMainEvents;
379+
} else if (this.networkDataProvider.indexForEvent(overlayEvent) !== null) {
380+
provider = this.networkDataProvider;
381+
relevantEvents = relevantNetworkEvents;
382+
}
383+
} else if (overlay.type === 'TIMESPAN_BREAKDOWN') {
384+
// For this overlay type, if there is no associated event it is rendered on mainFlameChart.
385+
provider = this.mainDataProvider;
386+
relevantEvents = relevantMainEvents;
387+
}
388+
389+
if (!provider || !relevantEvents) {
390+
continue;
391+
}
392+
393+
relevantEvents.push(...provider.search(bounds).map(r => r.index));
394+
}
395+
this.mainFlameChart.enableDimming(relevantMainEvents);
396+
this.networkFlameChart.enableDimming(relevantNetworkEvents);
397+
}
398+
351399
setOverlays(overlays: Overlays.Overlays.TimelineOverlay[], options: Overlays.Overlays.TimelineOverlaySetOptions):
352400
void {
353401
this.bulkRemoveOverlays(this.#currentInsightOverlays);
@@ -374,6 +422,13 @@ export class TimelineFlameChartView extends UI.Widget.VBox implements PerfUI.Fla
374422
this.#expandEntryTrack(entry);
375423
}
376424

425+
if (Root.Runtime.experiments.isEnabled(Root.Runtime.ExperimentName.TIMELINE_DIM_UNRELATED_EVENTS)) {
426+
// The insight's `relatedEvents` property likely already includes the events associated with
427+
// and overlay, but just in case not, include both arrays. Duplicates are fine.
428+
const relatedEvents = [...entries, ...this.#activeInsight?.relatedEvents || []];
429+
this.#dimInsightRelatedEvents(relatedEvents);
430+
}
431+
377432
if (options.updateTraceWindow) {
378433
const overlaysBounds = Overlays.Overlays.traceWindowContainingOverlays(this.#currentInsightOverlays);
379434
// Trace window covering all overlays expanded by 100% so that the overlays cover 50% of the visible window.
@@ -430,6 +485,8 @@ export class TimelineFlameChartView extends UI.Widget.VBox implements PerfUI.Fla
430485
this.bulkRemoveOverlays(this.#currentInsightOverlays);
431486

432487
if (!this.#activeInsight) {
488+
this.mainFlameChart.disableDimming();
489+
this.networkFlameChart.disableDimming();
433490
return;
434491
}
435492

@@ -1346,13 +1403,8 @@ export class TimelineFlameChartView extends UI.Widget.VBox implements PerfUI.Fla
13461403

13471404
this.searchableView.updateSearchMatchesCount(this.searchResults.length);
13481405

1349-
// To avoid too many highlights when the search regex matches too many entries,
1350-
// for example, when user only types in "e" as the search query,
1351-
// We only highlight the search results when the number of matches is less than or equal to 200.
1352-
if (this.searchResults.length <= MAX_HIGHLIGHTED_SEARCH_ELEMENTS) {
1353-
this.mainFlameChart.highlightAllEntries(mainMatches.map(m => m.index));
1354-
this.networkFlameChart.highlightAllEntries(networkMatches.map(m => m.index));
1355-
}
1406+
this.mainFlameChart.highlightAllEntries(mainMatches.map(m => m.index));
1407+
this.networkFlameChart.highlightAllEntries(networkMatches.map(m => m.index));
13561408
if (!shouldJump || !this.searchResults.length) {
13571409
return;
13581410
}

front_end/panels/timeline/TimelinePanel.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -535,7 +535,6 @@ export class TimelinePanel extends UI.Panel.Panel implements Client, TimelineMod
535535

536536
this.searchableViewInternal = new UI.SearchableView.SearchableView(this.flameChart, null);
537537
this.searchableViewInternal.setMinimumSize(0, 100);
538-
this.searchableViewInternal.setMinimalSearchQuerySize(0);
539538
this.searchableViewInternal.element.classList.add('searchable-view');
540539
this.searchableViewInternal.show(this.timelinePane.element);
541540
this.flameChart.show(this.searchableViewInternal.element);
@@ -555,8 +554,8 @@ export class TimelinePanel extends UI.Panel.Panel implements Client, TimelineMod
555554
// is not on the DOM. That only happens when the sidebar tabbed pane component is set to Annotations.
556555
// In that case, clicking on the insight chip will do nothing.
557556
this.#sideBar.element.addEventListener(TimelineInsights.SidebarInsight.InsightActivated.eventName, event => {
558-
const {name, insightSetKey, overlays} = event;
559-
this.#setActiveInsight({name, insightSetKey, overlays});
557+
const {name, insightSetKey, overlays, relatedEvents} = event;
558+
this.#setActiveInsight({name, insightSetKey, overlays, relatedEvents});
560559
});
561560

562561
this.#sideBar.element.addEventListener(TimelineInsights.SidebarInsight.InsightProvideOverlays.eventName, event => {

front_end/panels/timeline/components/Sidebar.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export interface ActiveInsight {
1616
name: string;
1717
insightSetKey: string;
1818
overlays: Overlays.Overlays.TimelineOverlay[];
19+
relatedEvents: Trace.Types.Events.Event[];
1920
}
2021

2122
export class RemoveAnnotation extends Event {

0 commit comments

Comments
 (0)