UNPKG

chrome-devtools-frontend

Version:
463 lines (393 loc) • 17.5 kB
/** * Copyright (C) 2014 Google Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * Neither the name of Google Inc. nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ /* eslint-disable rulesdir/no-imperative-dom-api */ 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 type * as CPUProfile from '../../models/cpu_profile/cpu_profile.js'; import * as PerfUI from '../../ui/legacy/components/perf_ui/perf_ui.js'; import * as UI from '../../ui/legacy/legacy.js'; let colorGeneratorInstance: Common.Color.Generator|null = null; export class ProfileFlameChartDataProvider implements PerfUI.FlameChart.FlameChartDataProvider { readonly colorGeneratorInternal: Common.Color.Generator; maxStackDepthInternal: number; timelineDataInternal: PerfUI.FlameChart.FlameChartTimelineData|null; entryNodes: CPUProfile.ProfileTreeModel.ProfileNode[]; #font: string; boldFont?: string; constructor() { this.colorGeneratorInternal = ProfileFlameChartDataProvider.colorGenerator(); this.maxStackDepthInternal = 0; this.timelineDataInternal = null; this.entryNodes = []; this.#font = `${PerfUI.Font.DEFAULT_FONT_SIZE} ${PerfUI.Font.getFontFamilyForCanvas()}`; } static colorGenerator(): Common.Color.Generator { if (!colorGeneratorInstance) { colorGeneratorInstance = new Common.Color.Generator( {min: 30, max: 330, count: undefined}, {min: 50, max: 80, count: 5}, {min: 80, max: 90, count: 3}); colorGeneratorInstance.setColorForID('(idle)', 'hsl(0, 0%, 94%)'); colorGeneratorInstance.setColorForID('(program)', 'hsl(0, 0%, 80%)'); colorGeneratorInstance.setColorForID('(garbage collector)', 'hsl(0, 0%, 80%)'); } return colorGeneratorInstance; } minimumBoundary(): number { throw new Error('Not implemented'); } totalTime(): number { throw new Error('Not implemented'); } formatValue(value: number, precision?: number): string { return i18n.TimeUtilities.preciseMillisToString(value, precision); } maxStackDepth(): number { return this.maxStackDepthInternal; } hasTrackConfigurationMode(): boolean { return false; } timelineData(): PerfUI.FlameChart.FlameChartTimelineData|null { return this.timelineDataInternal || this.calculateTimelineData(); } calculateTimelineData(): PerfUI.FlameChart.FlameChartTimelineData { throw new Error('Not implemented'); } preparePopoverElement(_entryIndex: number): Element|null { throw new Error('Not implemented'); } canJumpToEntry(entryIndex: number): boolean { return this.entryNodes[entryIndex].scriptId !== '0'; } entryTitle(entryIndex: number): string { const node = this.entryNodes[entryIndex]; return UI.UIUtils.beautifyFunctionName(node.functionName); } entryFont(entryIndex: number): string|null { const boldFont = 'bold ' + this.#font; return this.entryHasDeoptReason(entryIndex) ? boldFont : this.#font; } entryHasDeoptReason(_entryIndex: number): boolean { throw new Error('Not implemented'); } entryColor(entryIndex: number): string { const node = this.entryNodes[entryIndex]; // For idle and program, we want different 'shades of gray', so we fallback to functionName as scriptId = 0 // For rest of nodes e.g eval scripts, if url is empty then scriptId will be guaranteed to be non-zero return this.colorGeneratorInternal.colorForID( node.url || (node.scriptId !== '0' ? node.scriptId : node.functionName)); } decorateEntry( _entryIndex: number, _context: CanvasRenderingContext2D, _text: string|null, _barX: number, _barY: number, _barWidth: number, _barHeight: number): boolean { return false; } forceDecoration(_entryIndex: number): boolean { return false; } textColor(_entryIndex: number): string { return '#333'; } entryNodesLength(): number { return this.entryNodes.length; } } export class ProfileFlameChart extends Common.ObjectWrapper.eventMixin<PerfUI.FlameChart.EventTypes, typeof UI.Widget.VBox>(UI.Widget.VBox) implements UI.SearchableView.Searchable { readonly searchableView: UI.SearchableView.SearchableView; readonly overviewPane: OverviewPane; readonly mainPane: PerfUI.FlameChart.FlameChart; entrySelected: boolean; readonly dataProvider: ProfileFlameChartDataProvider; searchResults: number[]; searchResultIndex = -1; constructor(searchableView: UI.SearchableView.SearchableView, dataProvider: ProfileFlameChartDataProvider) { super(); this.element.id = 'cpu-flame-chart'; this.searchableView = searchableView; this.overviewPane = new OverviewPane(dataProvider); this.overviewPane.show(this.element); this.mainPane = new PerfUI.FlameChart.FlameChart(dataProvider, this.overviewPane); this.mainPane.setBarHeight(15); this.mainPane.setTextBaseline(4); this.mainPane.setTextPadding(2); this.mainPane.show(this.element); this.mainPane.addEventListener(PerfUI.FlameChart.Events.ENTRY_SELECTED, this.onEntrySelected, this); this.mainPane.addEventListener(PerfUI.FlameChart.Events.ENTRY_INVOKED, this.onEntryInvoked, this); this.entrySelected = false; this.mainPane.addEventListener(PerfUI.FlameChart.Events.CANVAS_FOCUSED, this.onEntrySelected, this); this.overviewPane.addEventListener(OverviewPaneEvents.WINDOW_CHANGED, this.onWindowChanged, this); this.dataProvider = dataProvider; this.searchResults = []; } override focus(): void { this.mainPane.focus(); } onWindowChanged(event: Common.EventTarget.EventTargetEvent<OverviewPaneWindowChangedEvent>): void { const {windowTimeLeft: windowLeft, windowTimeRight: windowRight} = event.data; this.mainPane.setWindowTimes(windowLeft, windowRight, /* animate */ true); } selectRange(timeLeft: number, timeRight: number): void { this.overviewPane.selectRange(timeLeft, timeRight); } onEntrySelected(event: Common.EventTarget.EventTargetEvent<void|number>): void { if (event.data) { const eventIndex = event.data; this.mainPane.setSelectedEntry(eventIndex); if (eventIndex === -1) { this.entrySelected = false; } else { this.entrySelected = true; } } else if (!this.entrySelected) { this.mainPane.setSelectedEntry(0); this.entrySelected = true; } } onEntryInvoked(event: Common.EventTarget.EventTargetEvent<number>): void { this.onEntrySelected(event); this.dispatchEventToListeners(PerfUI.FlameChart.Events.ENTRY_INVOKED, event.data); } update(): void { this.overviewPane.update(); this.mainPane.update(); } performSearch(searchConfig: UI.SearchableView.SearchConfig, _shouldJump: boolean, jumpBackwards?: boolean): void { const matcher = Platform.StringUtilities.createPlainTextSearchRegex(searchConfig.query, searchConfig.caseSensitive ? '' : 'i'); const selectedEntryIndex: number = this.searchResultIndex !== -1 ? this.searchResults[this.searchResultIndex] : -1; this.searchResults = []; const entriesCount = this.dataProvider.entryNodesLength(); for (let index = 0; index < entriesCount; ++index) { if (this.dataProvider.entryTitle(index).match(matcher)) { this.searchResults.push(index); } } if (this.searchResults.length) { this.searchResultIndex = this.searchResults.indexOf(selectedEntryIndex); if (this.searchResultIndex === -1) { this.searchResultIndex = jumpBackwards ? this.searchResults.length - 1 : 0; } this.mainPane.setSelectedEntry(this.searchResults[this.searchResultIndex]); } else { this.onSearchCanceled(); } this.searchableView.updateSearchMatchesCount(this.searchResults.length); this.searchableView.updateCurrentMatchIndex(this.searchResultIndex); } onSearchCanceled(): void { this.mainPane.setSelectedEntry(-1); this.searchResults = []; this.searchResultIndex = -1; } jumpToNextSearchResult(): void { this.searchResultIndex = (this.searchResultIndex + 1) % this.searchResults.length; this.mainPane.setSelectedEntry(this.searchResults[this.searchResultIndex]); this.searchableView.updateCurrentMatchIndex(this.searchResultIndex); } jumpToPreviousSearchResult(): void { this.searchResultIndex = (this.searchResultIndex - 1 + this.searchResults.length) % this.searchResults.length; this.mainPane.setSelectedEntry(this.searchResults[this.searchResultIndex]); this.searchableView.updateCurrentMatchIndex(this.searchResultIndex); } supportsCaseSensitiveSearch(): boolean { return true; } supportsRegexSearch(): boolean { return false; } } export class OverviewCalculator implements PerfUI.TimelineGrid.Calculator { readonly formatter: (arg0: number, arg1?: number|undefined) => string; minimumBoundaries!: number; maximumBoundaries!: number; xScaleFactor!: number; constructor(formatter: (arg0: number, arg1?: number|undefined) => string) { this.formatter = formatter; } updateBoundaries(overviewPane: OverviewPane): void { this.minimumBoundaries = overviewPane.dataProvider.minimumBoundary(); const totalTime = overviewPane.dataProvider.totalTime(); this.maximumBoundaries = this.minimumBoundaries + totalTime; this.xScaleFactor = overviewPane.overviewContainer.clientWidth / totalTime; } computePosition(time: number): number { return (time - this.minimumBoundaries) * this.xScaleFactor; } formatValue(value: number, precision?: number): string { return this.formatter(value - this.minimumBoundaries, precision); } maximumBoundary(): number { return this.maximumBoundaries; } minimumBoundary(): number { return this.minimumBoundaries; } zeroTime(): number { return this.minimumBoundaries; } boundarySpan(): number { return this.maximumBoundaries - this.minimumBoundaries; } } export class OverviewPane extends Common.ObjectWrapper.eventMixin<OverviewPaneEventTypes, typeof UI.Widget.VBox>( UI.Widget.VBox) implements PerfUI.FlameChart.FlameChartDelegate { overviewContainer: HTMLElement; readonly overviewCalculator: OverviewCalculator; readonly overviewGrid: PerfUI.OverviewGrid.OverviewGrid; overviewCanvas: HTMLCanvasElement; dataProvider: PerfUI.FlameChart.FlameChartDataProvider; windowTimeLeft?: number; windowTimeRight?: number; updateTimerId?: number; constructor(dataProvider: PerfUI.FlameChart.FlameChartDataProvider) { super(); this.element.classList.add('cpu-profile-flame-chart-overview-pane'); this.overviewContainer = this.element.createChild('div', 'cpu-profile-flame-chart-overview-container'); this.overviewCalculator = new OverviewCalculator(dataProvider.formatValue); this.overviewGrid = new PerfUI.OverviewGrid.OverviewGrid('cpu-profile-flame-chart', this.overviewCalculator); this.overviewGrid.element.classList.add('fill'); this.overviewCanvas = this.overviewContainer.createChild('canvas', 'cpu-profile-flame-chart-overview-canvas'); this.overviewContainer.appendChild(this.overviewGrid.element); this.dataProvider = dataProvider; this.overviewGrid.addEventListener( PerfUI.OverviewGrid.Events.WINDOW_CHANGED_WITH_POSITION, this.onWindowChanged, this); } windowChanged(windowStartTime: number, windowEndTime: number): void { this.selectRange(windowStartTime, windowEndTime); } updateRangeSelection(_startTime: number, _endTime: number): void { } updateSelectedGroup(_flameChart: PerfUI.FlameChart.FlameChart, _group: PerfUI.FlameChart.Group|null): void { } selectRange(timeLeft: number, timeRight: number): void { const startTime = this.dataProvider.minimumBoundary(); const totalTime = this.dataProvider.totalTime(); this.overviewGrid.setWindowRatio((timeLeft - startTime) / totalTime, (timeRight - startTime) / totalTime); } onWindowChanged(event: Common.EventTarget.EventTargetEvent<PerfUI.OverviewGrid.WindowChangedWithPositionEvent>): void { const windowPosition = {windowTimeLeft: event.data.rawStartValue, windowTimeRight: event.data.rawEndValue}; this.windowTimeLeft = windowPosition.windowTimeLeft; this.windowTimeRight = windowPosition.windowTimeRight; this.dispatchEventToListeners(OverviewPaneEvents.WINDOW_CHANGED, windowPosition); } timelineData(): PerfUI.FlameChart.FlameChartTimelineData|null { return this.dataProvider.timelineData(); } override onResize(): void { this.scheduleUpdate(); } scheduleUpdate(): void { if (this.updateTimerId) { return; } this.updateTimerId = this.element.window().requestAnimationFrame(this.update.bind(this)); } update(): void { this.updateTimerId = 0; const timelineData = this.timelineData(); if (!timelineData) { return; } this.resetCanvas( this.overviewContainer.clientWidth, this.overviewContainer.clientHeight - PerfUI.FlameChart.RulerHeight); this.overviewCalculator.updateBoundaries(this); this.overviewGrid.updateDividers(this.overviewCalculator); this.drawOverviewCanvas(); } drawOverviewCanvas(): void { const canvasWidth = this.overviewCanvas.width; const canvasHeight = this.overviewCanvas.height; const drawData = this.calculateDrawData(canvasWidth); const context = this.overviewCanvas.getContext('2d'); if (!context) { throw new Error('Failed to get canvas context'); } const ratio = window.devicePixelRatio; const offsetFromBottom = ratio; const lineWidth = 1; const yScaleFactor = canvasHeight / (this.dataProvider.maxStackDepth() * 1.1); context.lineWidth = lineWidth; context.translate(0.5, 0.5); context.strokeStyle = 'rgba(20,0,0,0.4)'; context.fillStyle = 'rgba(214,225,254,0.8)'; context.moveTo(-lineWidth, canvasHeight + lineWidth); context.lineTo(-lineWidth, Math.round(canvasHeight - drawData[0] * yScaleFactor - offsetFromBottom)); let value = 0; for (let x = 0; x < canvasWidth; ++x) { value = Math.round(canvasHeight - drawData[x] * yScaleFactor - offsetFromBottom); context.lineTo(x, value); } context.lineTo(canvasWidth + lineWidth, value); context.lineTo(canvasWidth + lineWidth, canvasHeight + lineWidth); context.fill(); context.stroke(); context.closePath(); } calculateDrawData(width: number): Uint8Array { const dataProvider = this.dataProvider; const timelineData = (this.timelineData() as PerfUI.FlameChart.FlameChartTimelineData); const entryStartTimes = timelineData.entryStartTimes; const entryTotalTimes = timelineData.entryTotalTimes; const entryLevels = timelineData.entryLevels; const length = entryStartTimes.length; const minimumBoundary = this.dataProvider.minimumBoundary(); const drawData = new Uint8Array(width); const scaleFactor = width / dataProvider.totalTime(); for (let entryIndex = 0; entryIndex < length; ++entryIndex) { const start = Math.floor((entryStartTimes[entryIndex] - minimumBoundary) * scaleFactor); const finish = Math.floor((entryStartTimes[entryIndex] - minimumBoundary + entryTotalTimes[entryIndex]) * scaleFactor); for (let x = start; x <= finish; ++x) { drawData[x] = Math.max(drawData[x], entryLevels[entryIndex] + 1); } } return drawData; } resetCanvas(width: number, height: number): void { const ratio = window.devicePixelRatio; this.overviewCanvas.width = width * ratio; this.overviewCanvas.height = height * ratio; this.overviewCanvas.style.width = width + 'px'; this.overviewCanvas.style.height = height + 'px'; } } export const enum OverviewPaneEvents { WINDOW_CHANGED = 'WindowChanged', } export interface OverviewPaneWindowChangedEvent { windowTimeLeft: number; windowTimeRight: number; } export interface OverviewPaneEventTypes { [OverviewPaneEvents.WINDOW_CHANGED]: OverviewPaneWindowChangedEvent; }