blob: fcff675532b1f6f796af15453d8916b0c7491f56 [file] [log] [blame]
// 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.
/* eslint-disable rulesdir/no_underscored_properties */
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 PerfUI from '../perf_ui/perf_ui.js';
import * as TimelineModel from '../timeline_model/timeline_model.js';
import * as UI from '../ui/legacy/legacy.js';
import {CountersGraph} from './CountersGraph.js';
import {Events as PerformanceModelEvents, PerformanceModel, Window} from './PerformanceModel.js'; // eslint-disable-line no-unused-vars
import {TimelineDetailsView} from './TimelineDetailsView.js';
import {TimelineRegExp} from './TimelineFilters.js';
import {Events as TimelineFlameChartDataProviderEvents, TimelineFlameChartDataProvider} from './TimelineFlameChartDataProvider.js';
import {TimelineFlameChartNetworkDataProvider} from './TimelineFlameChartNetworkDataProvider.js';
import {TimelineModeViewDelegate, TimelineSelection} from './TimelinePanel.js'; // eslint-disable-line no-unused-vars
import {AggregatedTimelineTreeView} from './TimelineTreeView.js';
import {TimelineMarkerStyle, TimelineUIUtils} from './TimelineUIUtils.js'; // eslint-disable-line no-unused-vars
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('timeline/TimelineFlameChartView.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
class MainSplitWidget extends UI.SplitWidget.SplitWidget {
_webVitals!: WebVitalsIntegrator;
_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 {
/** @type {!WebVitalsIntegrator} */
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 {
_delegate: TimelineModeViewDelegate;
_model: PerformanceModel|null;
_searchResults!: number[]|undefined;
_eventListeners: Common.EventTarget.EventDescriptor[];
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration
// eslint-disable-next-line @typescript-eslint/no-explicit-any
_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
_showWebVitalsSetting: Common.Settings.Setting<any>;
_networkSplitWidget: UI.SplitWidget.SplitWidget;
_mainDataProvider: TimelineFlameChartDataProvider;
_mainFlameChart: PerfUI.FlameChart.FlameChart;
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration
// eslint-disable-next-line @typescript-eslint/no-explicit-any
_networkFlameChartGroupExpansionSetting: Common.Settings.Setting<any>;
_networkDataProvider: TimelineFlameChartNetworkDataProvider;
_networkFlameChart: PerfUI.FlameChart.FlameChart;
_networkPane: UI.Widget.VBox;
_splitResizer: HTMLElement;
_webVitals: WebVitalsIntegrator;
_mainSplitWidget: MainSplitWidget;
_chartSplitWidget: UI.SplitWidget.SplitWidget;
_countersView: CountersGraph;
_detailsSplitWidget: UI.SplitWidget.SplitWidget;
_detailsView: TimelineDetailsView;
_onMainEntrySelected: (event?: Common.EventTarget.EventTargetEvent) => void;
_onNetworkEntrySelected: (event?: Common.EventTarget.EventTargetEvent) => void;
_nextExtensionIndex: number;
_boundRefresh: () => void;
_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
_groupBySetting: Common.Settings.Setting<any>;
_searchableView!: UI.SearchableView.SearchableView;
_urlToColorCache?: Map<string, string>;
_needsResizeToPreferredHeights?: boolean;
_selectedSearchResult?: number;
_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);
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration
// @ts-expect-error
this._onMainEntrySelected = this._onEntrySelected.bind(this, this._mainDataProvider);
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration
// @ts-expect-error
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();
}
}
_updateColorMapper(): void {
this._urlToColorCache = new Map();
if (!this._model) {
return;
}
this._mainDataProvider.setEventColorMapping(TimelineUIUtils.eventColor);
this._mainFlameChart.update();
}
_onWindowChanged(event: Common.EventTarget.EventTargetEvent): void {
const window = (event.data.window as Window);
const animate = Boolean(event.data.animate);
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, 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.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();
}
_updateTrack(): void {
this._countersView.setModel(this._model, this._selectedTrack);
this._detailsView.setModel(this._model, this._selectedTrack);
}
_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);
}
_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();
}
_onEntryHighlighted(commonEvent: Common.EventTarget.EventTargetEvent): void {
SDK.OverlayModel.OverlayModel.hideDOMNodeHighlight();
const entryIndex = (commonEvent.data as number);
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();
}
_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);
}
}
_onEntrySelected(dataProvider: PerfUI.FlameChart.FlameChartDataProvider, event: Common.EventTarget.EventTargetEvent):
void {
const entryIndex = (event.data as number);
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;
}
_selectSearchResult(index: number): void {
this._searchableView.updateCurrentMatchIndex(index);
if (this._searchResults) {
this._selectedSearchResult = this._searchResults[index];
this._delegate.select(this._mainDataProvider.createSelection(this._selectedSearchResult));
}
}
_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();
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 {
_startTime: number;
_startOffset: number;
_style: TimelineMarkerStyle;
constructor(startTime: number, startOffset: number, style: TimelineMarkerStyle) {
this._startTime = startTime;
this._startOffset = startOffset;
this._style = style;
}
startTime(): number {
return this._startTime;
}
color(): string {
return this._style.color;
}
title(): string|null {
if (this._style.lowPriority) {
return null;
}
const startTime = Number.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',
}