chrome-devtools-frontend
Version:
Chrome DevTools UI
595 lines (525 loc) • 25.1 kB
text/typescript
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import * as Common from '../../core/common/common.js';
import * as i18n from '../../core/i18n/i18n.js';
import * as Platform from '../../core/platform/platform.js';
import * as Root from '../../core/root/root.js';
import * as SDK from '../../core/sdk/sdk.js';
import * as Bindings from '../../models/bindings/bindings.js';
import * as TimelineModel from '../../models/timeline_model/timeline_model.js';
import * as PerfUI from '../../ui/legacy/components/perf_ui/perf_ui.js';
import * as UI from '../../ui/legacy/legacy.js';
import {CountersGraph} from './CountersGraph.js';
import type {PerformanceModel, WindowChangedEvent} from './PerformanceModel.js';
import {Events as PerformanceModelEvents} from './PerformanceModel.js';
import {TimelineDetailsView} from './TimelineDetailsView.js';
import {TimelineRegExp} from './TimelineFilters.js';
import {
Events as TimelineFlameChartDataProviderEvents,
TimelineFlameChartDataProvider,
} from './TimelineFlameChartDataProvider.js';
import {TimelineFlameChartNetworkDataProvider} from './TimelineFlameChartNetworkDataProvider.js';
import type {TimelineModeViewDelegate} from './TimelinePanel.js';
import {TimelineSelection} from './TimelinePanel.js';
import {AggregatedTimelineTreeView} from './TimelineTreeView.js';
import type {TimelineMarkerStyle} from './TimelineUIUtils.js';
import {TimelineUIUtils} from './TimelineUIUtils.js';
import {WebVitalsIntegrator} from './WebVitalsTimelineUtils.js';
const UIStrings = {
/**
*@description Text in Timeline Flame Chart View of the Performance panel
*@example {Frame} PH1
*@example {10ms} PH2
*/
sAtS: '{PH1} at {PH2}',
};
const str_ = i18n.i18n.registerUIStrings('panels/timeline/TimelineFlameChartView.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
class MainSplitWidget extends UI.SplitWidget.SplitWidget {
private webVitals!: WebVitalsIntegrator;
private model!: PerformanceModel|null;
constructor(
isVertical: boolean, secondIsSidebar: boolean, settingName?: string, defaultSidebarWidth?: number,
defaultSidebarHeight?: number, constraintsInDip?: boolean) {
super(isVertical, secondIsSidebar, settingName, defaultSidebarWidth, defaultSidebarHeight, constraintsInDip);
}
setWebVitals(webVitals: WebVitalsIntegrator): void {
this.webVitals = webVitals;
this.webVitals.setMinimumSize(0, 120);
}
setWindowTimes(left: number, right: number, animate: boolean): void {
if (!this.webVitals) {
return;
}
const startTime = left - (this.model ? this.model.timelineModel().minimumRecordTime() : 0);
this.webVitals.chartViewport.setWindowTimes(left, right, animate);
this.webVitals.webVitalsTimeline.data = {
startTime: startTime,
duration: right - left,
fcps: undefined,
lcps: undefined,
layoutShifts: undefined,
longTasks: undefined,
mainFrameNavigations: undefined,
maxDuration: undefined,
};
}
setModelAndUpdateBoundaries(model: PerformanceModel|null): void {
this.model = model;
if (!this.webVitals || !model) {
return;
}
const left = model.window().left;
const right = model.window().right;
const timelineModel = model.timelineModel();
const events: SDK.TracingModel.Event[] =
timelineModel.tracks().reduce((prev, curr) => prev.concat(curr.events), ([] as SDK.TracingModel.Event[]));
const minimumBoundary = model.timelineModel().minimumRecordTime();
const prepareEvents = (filterFunction: (arg0: SDK.TracingModel.Event) => boolean): number[] =>
events.filter(filterFunction).map(e => e.startTime - minimumBoundary);
const lcpEvents = events.filter(e => timelineModel.isLCPCandidateEvent(e) || timelineModel.isLCPInvalidateEvent(e));
const lcpEventsByNavigationId = new Map<string, SDK.TracingModel.Event>();
for (const e of lcpEvents) {
const navigationId = e.args['data']['navigationId'];
const previousLastEvent = lcpEventsByNavigationId.get(navigationId);
if (!previousLastEvent || previousLastEvent.args['data']['candidateIndex'] < e.args['data']['candidateIndex']) {
lcpEventsByNavigationId.set(navigationId, e);
}
}
const latestLcpCandidatesByNavigationId = Array.from(lcpEventsByNavigationId.values());
const latestLcpEvents = latestLcpCandidatesByNavigationId.filter(e => timelineModel.isLCPCandidateEvent(e));
const longTasks =
events.filter(e => SDK.TracingModel.TracingModel.isCompletePhase(e.phase) && timelineModel.isLongRunningTask(e))
.map(e => ({start: e.startTime - minimumBoundary, duration: e.duration || 0}));
this.webVitals.chartViewport.setBoundaries(left, right - left);
this.webVitals.chartViewport.setWindowTimes(left, right);
const startTime = left - (this.model ? this.model.timelineModel().minimumRecordTime() : 0);
this.webVitals.webVitalsTimeline.data = {
startTime: startTime,
duration: right - left,
maxDuration: timelineModel.maximumRecordTime(),
fcps: events.filter(e => timelineModel.isFCPEvent(e)).map(e => ({timestamp: e.startTime - minimumBoundary, e})),
lcps: latestLcpEvents.map(e => e.startTime).map(t => ({timestamp: t - minimumBoundary})),
layoutShifts: prepareEvents(e => timelineModel.isLayoutShiftEvent(e)).map(t => ({timestamp: t})),
longTasks,
mainFrameNavigations: prepareEvents(e => timelineModel.isMainFrameNavigationStartEvent(e)),
};
}
}
export class TimelineFlameChartView extends UI.Widget.VBox implements PerfUI.FlameChart.FlameChartDelegate,
UI.SearchableView.Searchable {
private readonly delegate: TimelineModeViewDelegate;
private model: PerformanceModel|null;
private searchResults!: number[]|undefined;
private eventListeners: Common.EventTarget.EventDescriptor[];
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private readonly showMemoryGraphSetting: Common.Settings.Setting<any>;
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private readonly showWebVitalsSetting: Common.Settings.Setting<any>;
private readonly networkSplitWidget: UI.SplitWidget.SplitWidget;
private mainDataProvider: TimelineFlameChartDataProvider;
private readonly mainFlameChart: PerfUI.FlameChart.FlameChart;
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private readonly networkFlameChartGroupExpansionSetting: Common.Settings.Setting<any>;
private networkDataProvider: TimelineFlameChartNetworkDataProvider;
private readonly networkFlameChart: PerfUI.FlameChart.FlameChart;
private readonly networkPane: UI.Widget.VBox;
private readonly splitResizer: HTMLElement;
private readonly webVitals: WebVitalsIntegrator;
private readonly mainSplitWidget: MainSplitWidget;
private readonly chartSplitWidget: UI.SplitWidget.SplitWidget;
private readonly countersView: CountersGraph;
private readonly detailsSplitWidget: UI.SplitWidget.SplitWidget;
private readonly detailsView: TimelineDetailsView;
private readonly onMainEntrySelected: (event: Common.EventTarget.EventTargetEvent<number>) => void;
private readonly onNetworkEntrySelected: (event: Common.EventTarget.EventTargetEvent<number>) => void;
private nextExtensionIndex: number;
private readonly boundRefresh: () => void;
private selectedTrack: TimelineModel.TimelineModel.Track|null;
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private readonly groupBySetting: Common.Settings.Setting<any>;
private searchableView!: UI.SearchableView.SearchableView;
private urlToColorCache?: Map<Platform.DevToolsPath.UrlString, string>;
private needsResizeToPreferredHeights?: boolean;
private selectedSearchResult?: number;
private searchRegex?: RegExp;
constructor(delegate: TimelineModeViewDelegate) {
super();
this.element.classList.add('timeline-flamechart');
this.delegate = delegate;
this.model = null;
this.eventListeners = [];
this.showMemoryGraphSetting = Common.Settings.Settings.instance().createSetting('timelineShowMemory', false);
this.showWebVitalsSetting = Common.Settings.Settings.instance().createSetting('timelineWebVitals', false);
// Create main and network flamecharts.
this.networkSplitWidget = new UI.SplitWidget.SplitWidget(false, false, 'timelineFlamechartMainView', 150);
// Ensure that the network panel & resizer appears above the web vitals / main thread.
this.networkSplitWidget.sidebarElement().style.zIndex = '120';
const mainViewGroupExpansionSetting =
Common.Settings.Settings.instance().createSetting('timelineFlamechartMainViewGroupExpansion', {});
this.mainDataProvider = new TimelineFlameChartDataProvider();
this.mainDataProvider.addEventListener(
TimelineFlameChartDataProviderEvents.DataChanged, () => this.mainFlameChart.scheduleUpdate());
this.mainFlameChart = new PerfUI.FlameChart.FlameChart(this.mainDataProvider, this, mainViewGroupExpansionSetting);
this.mainFlameChart.alwaysShowVerticalScroll();
this.mainFlameChart.enableRuler(false);
this.networkFlameChartGroupExpansionSetting =
Common.Settings.Settings.instance().createSetting('timelineFlamechartNetworkViewGroupExpansion', {});
this.networkDataProvider = new TimelineFlameChartNetworkDataProvider();
this.networkFlameChart =
new PerfUI.FlameChart.FlameChart(this.networkDataProvider, this, this.networkFlameChartGroupExpansionSetting);
this.networkFlameChart.alwaysShowVerticalScroll();
this.networkFlameChart.disableRangeSelection();
this.networkPane = new UI.Widget.VBox();
this.networkPane.setMinimumSize(23, 23);
this.networkFlameChart.show(this.networkPane.element);
this.splitResizer = this.networkPane.element.createChild('div', 'timeline-flamechart-resizer');
this.networkSplitWidget.hideDefaultResizer(true);
this.networkSplitWidget.installResizer(this.splitResizer);
this.webVitals = new WebVitalsIntegrator(this);
this.mainSplitWidget = new MainSplitWidget(false, false, 'timelineFlamechartMainAndVitalsView', undefined, 120);
this.mainSplitWidget.setWebVitals(this.webVitals);
this.mainSplitWidget.setMainWidget(this.mainFlameChart);
this.mainSplitWidget.setSidebarWidget(this.webVitals);
this.toggleWebVitalsLane();
this.networkSplitWidget.setMainWidget(this.mainSplitWidget);
this.networkSplitWidget.setSidebarWidget(this.networkPane);
// Create counters chart splitter.
this.chartSplitWidget = new UI.SplitWidget.SplitWidget(false, true, 'timelineCountersSplitViewState');
this.countersView = new CountersGraph(this.delegate);
this.chartSplitWidget.setMainWidget(this.networkSplitWidget);
this.chartSplitWidget.setSidebarWidget(this.countersView);
this.chartSplitWidget.hideDefaultResizer();
this.chartSplitWidget.installResizer((this.countersView.resizerElement() as Element));
this.updateCountersGraphToggle();
// Create top level properties splitter.
this.detailsSplitWidget = new UI.SplitWidget.SplitWidget(false, true, 'timelinePanelDetailsSplitViewState');
this.detailsSplitWidget.element.classList.add('timeline-details-split');
this.detailsView = new TimelineDetailsView(delegate);
this.detailsSplitWidget.installResizer(this.detailsView.headerElement());
this.detailsSplitWidget.setMainWidget(this.chartSplitWidget);
this.detailsSplitWidget.setSidebarWidget(this.detailsView);
this.detailsSplitWidget.show(this.element);
this.onMainEntrySelected = this.onEntrySelected.bind(this, this.mainDataProvider);
this.onNetworkEntrySelected = this.onEntrySelected.bind(this, this.networkDataProvider);
this.mainFlameChart.addEventListener(PerfUI.FlameChart.Events.EntrySelected, this.onMainEntrySelected, this);
this.mainFlameChart.addEventListener(PerfUI.FlameChart.Events.EntryInvoked, this.onMainEntrySelected, this);
this.networkFlameChart.addEventListener(PerfUI.FlameChart.Events.EntrySelected, this.onNetworkEntrySelected, this);
this.networkFlameChart.addEventListener(PerfUI.FlameChart.Events.EntryInvoked, this.onNetworkEntrySelected, this);
this.mainFlameChart.addEventListener(PerfUI.FlameChart.Events.EntryHighlighted, this.onEntryHighlighted, this);
this.nextExtensionIndex = 0;
this.boundRefresh = this.refresh.bind(this);
this.selectedTrack = null;
this.mainDataProvider.setEventColorMapping(TimelineUIUtils.eventColor);
this.groupBySetting = Common.Settings.Settings.instance().createSetting(
'timelineTreeGroupBy', AggregatedTimelineTreeView.GroupBy.None);
this.groupBySetting.addChangeListener(this.updateColorMapper, this);
this.updateColorMapper();
}
toggleWebVitalsLane(): void {
if (this.showWebVitalsSetting.get()) {
this.mainSplitWidget.showBoth();
this.mainSplitWidget.setSidebarSize(120);
this.mainSplitWidget.setResizable(false);
this.mainSplitWidget.hideDefaultResizer(true);
} else {
this.mainSplitWidget.hideSidebar();
}
}
private updateColorMapper(): void {
this.urlToColorCache = new Map();
if (!this.model) {
return;
}
this.mainDataProvider.setEventColorMapping(TimelineUIUtils.eventColor);
this.mainFlameChart.update();
}
private onWindowChanged(event: Common.EventTarget.EventTargetEvent<WindowChangedEvent>): void {
const {window, animate} = event.data;
this.mainFlameChart.setWindowTimes(window.left, window.right, animate);
this.networkFlameChart.setWindowTimes(window.left, window.right, animate);
this.networkDataProvider.setWindowTimes(window.left, window.right);
this.mainSplitWidget.setWindowTimes(window.left, window.right, Boolean(animate));
this.updateSearchResults(false, false);
}
windowChanged(windowStartTime: number, windowEndTime: number, animate: boolean): void {
if (this.model) {
this.model.setWindow({left: windowStartTime, right: windowEndTime}, animate);
}
}
updateRangeSelection(startTime: number, endTime: number): void {
this.delegate.select(TimelineSelection.fromRange(startTime, endTime));
}
updateSelectedGroup(flameChart: PerfUI.FlameChart.FlameChart, group: PerfUI.FlameChart.Group|null): void {
if (flameChart !== this.mainFlameChart) {
return;
}
const track = group ? this.mainDataProvider.groupTrack(group) : null;
this.selectedTrack = track;
this.updateTrack();
}
setModel(model: PerformanceModel|null): void {
if (model === this.model) {
return;
}
Common.EventTarget.removeEventListeners(this.eventListeners);
this.model = model;
this.selectedTrack = null;
this.mainDataProvider.setModel(this.model);
this.networkDataProvider.setModel(this.model);
if (this.model) {
this.eventListeners = [
this.model.addEventListener(PerformanceModelEvents.WindowChanged, this.onWindowChanged, this),
this.model.addEventListener(PerformanceModelEvents.ExtensionDataAdded, this.appendExtensionData, this),
];
const window = this.model.window();
this.mainFlameChart.setWindowTimes(window.left, window.right);
this.networkFlameChart.setWindowTimes(window.left, window.right);
this.networkDataProvider.setWindowTimes(window.left, window.right);
this.mainSplitWidget.setModelAndUpdateBoundaries(model);
this.updateSearchResults(false, false);
}
this.updateColorMapper();
this.updateTrack();
this.nextExtensionIndex = 0;
this.appendExtensionData();
this.refresh();
}
private updateTrack(): void {
this.countersView.setModel(this.model, this.selectedTrack);
this.detailsView.setModel(this.model, this.selectedTrack);
}
private refresh(): void {
if (this.networkDataProvider.isEmpty()) {
this.mainFlameChart.enableRuler(true);
this.networkSplitWidget.hideSidebar();
} else {
this.mainFlameChart.enableRuler(false);
this.networkSplitWidget.showBoth();
this.resizeToPreferredHeights();
}
this.mainFlameChart.reset();
this.networkFlameChart.reset();
this.updateSearchResults(false, false);
}
private appendExtensionData(): void {
if (!this.model) {
return;
}
const extensions = this.model.extensionInfo();
while (this.nextExtensionIndex < extensions.length) {
this.mainDataProvider.appendExtensionEvents(extensions[this.nextExtensionIndex++]);
}
this.mainFlameChart.scheduleUpdate();
}
private onEntryHighlighted(commonEvent: Common.EventTarget.EventTargetEvent<number>): void {
SDK.OverlayModel.OverlayModel.hideDOMNodeHighlight();
const entryIndex = commonEvent.data;
const event = this.mainDataProvider.eventByIndex(entryIndex);
if (!event) {
return;
}
const target = this.model && this.model.timelineModel().targetByEvent(event);
if (!target) {
return;
}
const timelineData = TimelineModel.TimelineModel.TimelineData.forEvent(event);
const backendNodeIds = timelineData.backendNodeIds;
if (!backendNodeIds) {
return;
}
for (let i = 0; i < backendNodeIds.length; ++i) {
new SDK.DOMModel.DeferredDOMNode(target, backendNodeIds[i]).highlight();
}
}
highlightEvent(event: SDK.TracingModel.Event|null): void {
const entryIndex =
event ? this.mainDataProvider.entryIndexForSelection(TimelineSelection.fromTraceEvent(event)) : -1;
if (entryIndex >= 0) {
this.mainFlameChart.highlightEntry(entryIndex);
} else {
this.mainFlameChart.hideHighlight();
}
}
willHide(): void {
this.networkFlameChartGroupExpansionSetting.removeChangeListener(this.resizeToPreferredHeights, this);
this.showMemoryGraphSetting.removeChangeListener(this.updateCountersGraphToggle, this);
Bindings.IgnoreListManager.IgnoreListManager.instance().removeChangeListener(this.boundRefresh);
}
wasShown(): void {
this.networkFlameChartGroupExpansionSetting.addChangeListener(this.resizeToPreferredHeights, this);
this.showMemoryGraphSetting.addChangeListener(this.updateCountersGraphToggle, this);
Bindings.IgnoreListManager.IgnoreListManager.instance().addChangeListener(this.boundRefresh);
if (this.needsResizeToPreferredHeights) {
this.resizeToPreferredHeights();
}
this.mainFlameChart.scheduleUpdate();
this.networkFlameChart.scheduleUpdate();
}
private updateCountersGraphToggle(): void {
if (this.showMemoryGraphSetting.get()) {
this.chartSplitWidget.showBoth();
} else {
this.chartSplitWidget.hideSidebar();
}
}
setSelection(selection: TimelineSelection|null): void {
let index = this.mainDataProvider.entryIndexForSelection(selection);
this.mainFlameChart.setSelectedEntry(index);
index = this.networkDataProvider.entryIndexForSelection(selection);
this.networkFlameChart.setSelectedEntry(index);
if (this.detailsView) {
this.detailsView.setSelection(selection);
}
}
private onEntrySelected(
dataProvider: PerfUI.FlameChart.FlameChartDataProvider,
event: Common.EventTarget.EventTargetEvent<number>): void {
const entryIndex = event.data;
if (Root.Runtime.experiments.isEnabled('timelineEventInitiators') && dataProvider === this.mainDataProvider) {
if (this.mainDataProvider.buildFlowForInitiator(entryIndex)) {
this.mainFlameChart.scheduleUpdate();
}
}
this.delegate.select((dataProvider as TimelineFlameChartNetworkDataProvider).createSelection(entryIndex));
}
resizeToPreferredHeights(): void {
if (!this.isShowing()) {
this.needsResizeToPreferredHeights = true;
return;
}
this.needsResizeToPreferredHeights = false;
this.networkPane.element.classList.toggle(
'timeline-network-resizer-disabled', !this.networkDataProvider.isExpanded());
this.networkSplitWidget.setSidebarSize(
this.networkDataProvider.preferredHeight() + this.splitResizer.clientHeight + PerfUI.FlameChart.HeaderHeight +
2);
}
setSearchableView(searchableView: UI.SearchableView.SearchableView): void {
this.searchableView = searchableView;
}
// UI.SearchableView.Searchable implementation
jumpToNextSearchResult(): void {
if (!this.searchResults || !this.searchResults.length) {
return;
}
const index =
typeof this.selectedSearchResult !== 'undefined' ? this.searchResults.indexOf(this.selectedSearchResult) : -1;
this.selectSearchResult(Platform.NumberUtilities.mod(index + 1, this.searchResults.length));
}
jumpToPreviousSearchResult(): void {
if (!this.searchResults || !this.searchResults.length) {
return;
}
const index =
typeof this.selectedSearchResult !== 'undefined' ? this.searchResults.indexOf(this.selectedSearchResult) : 0;
this.selectSearchResult(Platform.NumberUtilities.mod(index - 1, this.searchResults.length));
}
supportsCaseSensitiveSearch(): boolean {
return true;
}
supportsRegexSearch(): boolean {
return true;
}
private selectSearchResult(index: number): void {
this.searchableView.updateCurrentMatchIndex(index);
if (this.searchResults) {
this.selectedSearchResult = this.searchResults[index];
this.delegate.select(this.mainDataProvider.createSelection(this.selectedSearchResult));
}
}
private updateSearchResults(shouldJump: boolean, jumpBackwards?: boolean): void {
const oldSelectedSearchResult = (this.selectedSearchResult as number);
delete this.selectedSearchResult;
this.searchResults = [];
if (!this.searchRegex || !this.model) {
return;
}
const regExpFilter = new TimelineRegExp(this.searchRegex);
const window = this.model.window();
this.searchResults = this.mainDataProvider.search(window.left, window.right, regExpFilter);
this.searchableView.updateSearchMatchesCount(this.searchResults.length);
if (!shouldJump || !this.searchResults.length) {
return;
}
let selectedIndex = this.searchResults.indexOf(oldSelectedSearchResult);
if (selectedIndex === -1) {
selectedIndex = jumpBackwards ? this.searchResults.length - 1 : 0;
}
this.selectSearchResult(selectedIndex);
}
searchCanceled(): void {
if (typeof this.selectedSearchResult !== 'undefined') {
this.delegate.select(null);
}
delete this.searchResults;
delete this.selectedSearchResult;
delete this.searchRegex;
}
performSearch(searchConfig: UI.SearchableView.SearchConfig, shouldJump: boolean, jumpBackwards?: boolean): void {
this.searchRegex = searchConfig.toSearchRegex().regex;
this.updateSearchResults(shouldJump, jumpBackwards);
}
}
export class Selection {
timelineSelection: TimelineSelection;
entryIndex: number;
constructor(selection: TimelineSelection, entryIndex: number) {
this.timelineSelection = selection;
this.entryIndex = entryIndex;
}
}
export const FlameChartStyle = {
textColor: '#333',
};
export class TimelineFlameChartMarker implements PerfUI.FlameChart.FlameChartMarker {
private readonly startTimeInternal: number;
private readonly startOffset: number;
private style: TimelineMarkerStyle;
constructor(startTime: number, startOffset: number, style: TimelineMarkerStyle) {
this.startTimeInternal = startTime;
this.startOffset = startOffset;
this.style = style;
}
startTime(): number {
return this.startTimeInternal;
}
color(): string {
return this.style.color;
}
title(): string|null {
if (this.style.lowPriority) {
return null;
}
const startTime = i18n.TimeUtilities.millisToString(this.startOffset);
return i18nString(UIStrings.sAtS, {PH1: this.style.title, PH2: startTime});
}
draw(context: CanvasRenderingContext2D, x: number, height: number, pixelsPerMillisecond: number): void {
const lowPriorityVisibilityThresholdInPixelsPerMs = 4;
if (this.style.lowPriority && pixelsPerMillisecond < lowPriorityVisibilityThresholdInPixelsPerMs) {
return;
}
context.save();
if (this.style.tall) {
context.strokeStyle = this.style.color;
context.lineWidth = this.style.lineWidth;
context.translate(this.style.lineWidth < 1 || (this.style.lineWidth & 1) ? 0.5 : 0, 0.5);
context.beginPath();
context.moveTo(x, 0);
context.setLineDash(this.style.dashStyle);
context.lineTo(x, context.canvas.height);
context.stroke();
}
context.restore();
}
}
// TODO(crbug.com/1167717): Make this a const enum again
// eslint-disable-next-line rulesdir/const_enum
export enum ColorBy {
URL = 'URL',
}