| /* |
| * Copyright (C) 2013 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_underscored_properties */ |
| |
| import * as i18n from '../i18n/i18n.js'; |
| import * as PerfUI from '../perf_ui/perf_ui.js'; |
| import * as Platform from '../platform/platform.js'; |
| import type * as SDK from '../sdk/sdk.js'; |
| import * as UI from '../ui/ui.js'; |
| |
| export const UIStrings = { |
| /** |
| *@description Text to indicate the progress of a profile |
| */ |
| profiling: 'Profiling…', |
| /** |
| *@description Text in Paint Profiler View of the Layers panel |
| */ |
| shapes: 'Shapes', |
| /** |
| *@description Text in Paint Profiler View of the Layers panel |
| */ |
| bitmap: 'Bitmap', |
| /** |
| *@description Generic label for any text |
| */ |
| text: 'Text', |
| /** |
| *@description Text in Paint Profiler View of the Layers panel |
| */ |
| misc: 'Misc', |
| /** |
| *@description ARIA label for a pie chart that shows the results of the paint profiler |
| */ |
| profilingResults: 'Profiling results', |
| /** |
| *@description Label for command log tree in the Profiler tab |
| */ |
| commandLog: 'Command Log', |
| }; |
| const str_ = i18n.i18n.registerUIStrings('layer_viewer/PaintProfilerView.ts', UIStrings); |
| const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); |
| let categories: {[x: string]: PaintProfilerCategory}|null = null; |
| |
| let logItemCategoriesMap: {[x: string]: PaintProfilerCategory}|null = null; |
| |
| export class PaintProfilerView extends UI.Widget.HBox { |
| _canvasContainer: HTMLElement; |
| _progressBanner: HTMLElement; |
| _pieChart: PerfUI.PieChart.PieChart; |
| _showImageCallback: (arg0?: string|undefined) => void; |
| _canvas: HTMLCanvasElement; |
| _context: CanvasRenderingContext2D; |
| _selectionWindow: PerfUI.OverviewGrid.Window; |
| _innerBarWidth: number; |
| _minBarHeight: number; |
| _barPaddingWidth: number; |
| _outerBarWidth: number; |
| _pendingScale: number; |
| _scale: number; |
| _samplesPerBar: number; |
| _log: SDK.PaintProfiler.PaintProfilerLogItem[]; |
| _snapshot?: SDK.PaintProfiler.PaintProfilerSnapshot|null; |
| _logCategories?: PaintProfilerCategory[]; |
| _profiles?: Protocol.LayerTree.PaintProfile[]|null; |
| _updateImageTimer?: number; |
| |
| constructor(showImageCallback: (arg0?: string|undefined) => void) { |
| super(true); |
| this.registerRequiredCSS('layer_viewer/paintProfiler.css', {enableLegacyPatching: true}); |
| this.contentElement.classList.add('paint-profiler-overview'); |
| this._canvasContainer = this.contentElement.createChild('div', 'paint-profiler-canvas-container'); |
| this._progressBanner = this.contentElement.createChild('div', 'full-widget-dimmed-banner hidden'); |
| this._progressBanner.textContent = i18nString(UIStrings.profiling); |
| this._pieChart = new PerfUI.PieChart.PieChart(); |
| this._populatePieChart(0, []); |
| this._pieChart.classList.add('paint-profiler-pie-chart'); |
| this.contentElement.appendChild(this._pieChart); |
| |
| this._showImageCallback = showImageCallback; |
| this._canvas = this._canvasContainer.createChild('canvas', 'fill') as HTMLCanvasElement; |
| this._context = this._canvas.getContext('2d') as CanvasRenderingContext2D; |
| this._selectionWindow = new PerfUI.OverviewGrid.Window(this._canvasContainer); |
| this._selectionWindow.addEventListener(PerfUI.OverviewGrid.Events.WindowChanged, this._onWindowChanged, this); |
| |
| this._innerBarWidth = 4 * window.devicePixelRatio; |
| this._minBarHeight = window.devicePixelRatio; |
| this._barPaddingWidth = 2 * window.devicePixelRatio; |
| this._outerBarWidth = this._innerBarWidth + this._barPaddingWidth; |
| this._pendingScale = 1; |
| this._scale = this._pendingScale; |
| this._samplesPerBar = 0; |
| this._log = []; |
| |
| this._reset(); |
| } |
| |
| static categories(): {[x: string]: PaintProfilerCategory} { |
| if (!categories) { |
| categories = { |
| shapes: new PaintProfilerCategory('shapes', i18nString(UIStrings.shapes), 'rgb(255, 161, 129)'), |
| bitmap: new PaintProfilerCategory('bitmap', i18nString(UIStrings.bitmap), 'rgb(136, 196, 255)'), |
| text: new PaintProfilerCategory('text', i18nString(UIStrings.text), 'rgb(180, 255, 137)'), |
| misc: new PaintProfilerCategory('misc', i18nString(UIStrings.misc), 'rgb(206, 160, 255)'), |
| }; |
| } |
| return categories; |
| } |
| |
| static _initLogItemCategories(): {[x: string]: PaintProfilerCategory} { |
| if (!logItemCategoriesMap) { |
| const categories = PaintProfilerView.categories(); |
| |
| const logItemCategories: {[x: string]: PaintProfilerCategory} = {}; |
| logItemCategories['Clear'] = categories['misc']; |
| logItemCategories['DrawPaint'] = categories['misc']; |
| logItemCategories['DrawData'] = categories['misc']; |
| logItemCategories['SetMatrix'] = categories['misc']; |
| logItemCategories['PushCull'] = categories['misc']; |
| logItemCategories['PopCull'] = categories['misc']; |
| logItemCategories['Translate'] = categories['misc']; |
| logItemCategories['Scale'] = categories['misc']; |
| logItemCategories['Concat'] = categories['misc']; |
| logItemCategories['Restore'] = categories['misc']; |
| logItemCategories['SaveLayer'] = categories['misc']; |
| logItemCategories['Save'] = categories['misc']; |
| logItemCategories['BeginCommentGroup'] = categories['misc']; |
| logItemCategories['AddComment'] = categories['misc']; |
| logItemCategories['EndCommentGroup'] = categories['misc']; |
| logItemCategories['ClipRect'] = categories['misc']; |
| logItemCategories['ClipRRect'] = categories['misc']; |
| logItemCategories['ClipPath'] = categories['misc']; |
| logItemCategories['ClipRegion'] = categories['misc']; |
| logItemCategories['DrawPoints'] = categories['shapes']; |
| logItemCategories['DrawRect'] = categories['shapes']; |
| logItemCategories['DrawOval'] = categories['shapes']; |
| logItemCategories['DrawRRect'] = categories['shapes']; |
| logItemCategories['DrawPath'] = categories['shapes']; |
| logItemCategories['DrawVertices'] = categories['shapes']; |
| logItemCategories['DrawDRRect'] = categories['shapes']; |
| logItemCategories['DrawBitmap'] = categories['bitmap']; |
| logItemCategories['DrawBitmapRectToRect'] = categories['bitmap']; |
| logItemCategories['DrawBitmapMatrix'] = categories['bitmap']; |
| logItemCategories['DrawBitmapNine'] = categories['bitmap']; |
| logItemCategories['DrawSprite'] = categories['bitmap']; |
| logItemCategories['DrawPicture'] = categories['bitmap']; |
| logItemCategories['DrawText'] = categories['text']; |
| logItemCategories['DrawPosText'] = categories['text']; |
| logItemCategories['DrawPosTextH'] = categories['text']; |
| logItemCategories['DrawTextOnPath'] = categories['text']; |
| |
| logItemCategoriesMap = logItemCategories; |
| } |
| |
| return logItemCategoriesMap; |
| } |
| |
| static _categoryForLogItem(logItem: SDK.PaintProfiler.PaintProfilerLogItem): PaintProfilerCategory { |
| const method = Platform.StringUtilities.toTitleCase(logItem.method); |
| |
| const logItemCategories = PaintProfilerView._initLogItemCategories(); |
| let result: PaintProfilerCategory = logItemCategories[method]; |
| if (!result) { |
| result = PaintProfilerView.categories()['misc']; |
| logItemCategories[method] = result; |
| } |
| return result; |
| } |
| |
| onResize(): void { |
| this._update(); |
| } |
| |
| async setSnapshotAndLog( |
| snapshot: SDK.PaintProfiler.PaintProfilerSnapshot|null, log: SDK.PaintProfiler.PaintProfilerLogItem[], |
| clipRect: Protocol.DOM.Rect|null): Promise<void> { |
| this._reset(); |
| this._snapshot = snapshot; |
| if (this._snapshot) { |
| this._snapshot.addReference(); |
| } |
| this._log = log; |
| this._logCategories = this._log.map(PaintProfilerView._categoryForLogItem); |
| |
| if (!snapshot) { |
| this._update(); |
| this._populatePieChart(0, []); |
| this._selectionWindow.setEnabled(false); |
| return; |
| } |
| |
| this._selectionWindow.setEnabled(true); |
| this._progressBanner.classList.remove('hidden'); |
| this._updateImage(); |
| |
| const profiles = await snapshot.profile(clipRect); |
| |
| this._progressBanner.classList.add('hidden'); |
| this._profiles = profiles; |
| this._update(); |
| this._updatePieChart(); |
| } |
| |
| setScale(scale: number): void { |
| const needsUpdate = scale > this._scale; |
| const predictiveGrowthFactor = 2; |
| this._pendingScale = Math.min(1, scale * predictiveGrowthFactor); |
| if (needsUpdate && this._snapshot) { |
| this._updateImage(); |
| } |
| } |
| |
| _update(): void { |
| this._canvas.width = this._canvasContainer.clientWidth * window.devicePixelRatio; |
| this._canvas.height = this._canvasContainer.clientHeight * window.devicePixelRatio; |
| this._samplesPerBar = 0; |
| if (!this._profiles || !this._profiles.length || !this._logCategories) { |
| return; |
| } |
| |
| const maxBars = Math.floor((this._canvas.width - 2 * this._barPaddingWidth) / this._outerBarWidth); |
| const sampleCount = this._log.length; |
| this._samplesPerBar = Math.ceil(sampleCount / maxBars); |
| |
| let maxBarTime = 0; |
| const barTimes = []; |
| const barHeightByCategory = []; |
| let heightByCategory: {[category: string]: number} = {}; |
| for (let i = 0, lastBarIndex = 0, lastBarTime = 0; i < sampleCount;) { |
| let categoryName = (this._logCategories[i] && this._logCategories[i].name) || 'misc'; |
| const sampleIndex = this._log[i].commandIndex; |
| for (let row = 0; row < this._profiles.length; row++) { |
| const sample = this._profiles[row][sampleIndex]; |
| lastBarTime += sample; |
| heightByCategory[categoryName] = (heightByCategory[categoryName] || 0) + sample; |
| } |
| ++i; |
| if (i - lastBarIndex === this._samplesPerBar || i === sampleCount) { |
| // Normalize by total number of samples accumulated. |
| const factor = this._profiles.length * (i - lastBarIndex); |
| lastBarTime /= factor; |
| for (categoryName in heightByCategory) { |
| heightByCategory[categoryName] /= factor; |
| } |
| |
| barTimes.push(lastBarTime); |
| barHeightByCategory.push(heightByCategory); |
| |
| if (lastBarTime > maxBarTime) { |
| maxBarTime = lastBarTime; |
| } |
| lastBarTime = 0; |
| heightByCategory = {}; |
| lastBarIndex = i; |
| } |
| } |
| |
| const paddingHeight = 4 * window.devicePixelRatio; |
| const scale = (this._canvas.height - paddingHeight - this._minBarHeight) / maxBarTime; |
| for (let i = 0; i < barTimes.length; ++i) { |
| for (const categoryName in barHeightByCategory[i]) { |
| barHeightByCategory[i][categoryName] *= (barTimes[i] * scale + this._minBarHeight) / barTimes[i]; |
| } |
| this._renderBar(i, barHeightByCategory[i]); |
| } |
| } |
| |
| _renderBar(index: number, heightByCategory: {[x: string]: number}): void { |
| const categories = PaintProfilerView.categories(); |
| let currentHeight = 0; |
| const x = this._barPaddingWidth + index * this._outerBarWidth; |
| for (const categoryName in categories) { |
| if (!heightByCategory[categoryName]) { |
| continue; |
| } |
| currentHeight += heightByCategory[categoryName]; |
| const y = this._canvas.height - currentHeight; |
| this._context.fillStyle = categories[categoryName].color; |
| this._context.fillRect(x, y, this._innerBarWidth, heightByCategory[categoryName]); |
| } |
| } |
| |
| _onWindowChanged(): void { |
| this.dispatchEventToListeners(Events.WindowChanged); |
| this._updatePieChart(); |
| if (this._updateImageTimer) { |
| return; |
| } |
| this._updateImageTimer = window.setTimeout(this._updateImage.bind(this), 100); |
| } |
| |
| _updatePieChart(): void { |
| const {total, slices} = this._calculatePieChart(); |
| this._populatePieChart(total, slices); |
| } |
| |
| _calculatePieChart(): {total: number, slices: Array<{value: number, color: string, title: string}>} { |
| const window = this.selectionWindow(); |
| if (!this._profiles || !this._profiles.length || !window) { |
| return {total: 0, slices: []}; |
| } |
| let totalTime = 0; |
| const timeByCategory: {[x: string]: number} = {}; |
| for (let i = window.left; i < window.right; ++i) { |
| const logEntry = this._log[i]; |
| const category = PaintProfilerView._categoryForLogItem(logEntry); |
| timeByCategory[category.color] = timeByCategory[category.color] || 0; |
| for (let j = 0; j < this._profiles.length; ++j) { |
| const time = this._profiles[j][logEntry.commandIndex]; |
| totalTime += time; |
| timeByCategory[category.color] += time; |
| } |
| } |
| const slices: PerfUI.PieChart.Slice[] = []; |
| for (const color in timeByCategory) { |
| slices.push({value: timeByCategory[color] / this._profiles.length, color, title: ''}); |
| } |
| return {total: totalTime / this._profiles.length, slices}; |
| } |
| |
| _populatePieChart(total: number, slices: PerfUI.PieChart.Slice[]): void { |
| this._pieChart.data = { |
| chartName: i18nString(UIStrings.profilingResults), |
| size: 55, |
| formatter: this._formatPieChartTime.bind(this), |
| showLegend: false, |
| total, |
| slices, |
| }; |
| } |
| |
| _formatPieChartTime(value: number): string { |
| return Number.millisToString(value * 1000, true); |
| } |
| |
| selectionWindow(): {left: number, right: number}|null { |
| if (!this._log) { |
| return null; |
| } |
| |
| const screenLeft = (this._selectionWindow.windowLeft || 0) * this._canvas.width; |
| const screenRight = (this._selectionWindow.windowRight || 0) * this._canvas.width; |
| const barLeft = Math.floor(screenLeft / this._outerBarWidth); |
| const barRight = Math.floor((screenRight + this._innerBarWidth - this._barPaddingWidth / 2) / this._outerBarWidth); |
| const stepLeft = Platform.NumberUtilities.clamp(barLeft * this._samplesPerBar, 0, this._log.length - 1); |
| const stepRight = Platform.NumberUtilities.clamp(barRight * this._samplesPerBar, 0, this._log.length); |
| |
| return {left: stepLeft, right: stepRight}; |
| } |
| |
| _updateImage(): void { |
| delete this._updateImageTimer; |
| let left; |
| let right; |
| const window = this.selectionWindow(); |
| if (this._profiles && this._profiles.length && window) { |
| left = this._log[window.left].commandIndex; |
| right = this._log[window.right - 1].commandIndex; |
| } |
| const scale = this._pendingScale; |
| if (!this._snapshot) { |
| return; |
| } |
| this._snapshot.replay(scale, left, right).then(image => { |
| if (!image) { |
| return; |
| } |
| this._scale = scale; |
| this._showImageCallback(image); |
| }); |
| } |
| |
| _reset(): void { |
| if (this._snapshot) { |
| this._snapshot.release(); |
| } |
| this._snapshot = null; |
| this._profiles = null; |
| this._selectionWindow.reset(); |
| this._selectionWindow.setEnabled(false); |
| } |
| } |
| |
| // TODO(crbug.com/1167717): Make this a const enum again |
| // eslint-disable-next-line rulesdir/const_enum |
| export enum Events { |
| WindowChanged = 'WindowChanged', |
| } |
| |
| export class PaintProfilerCommandLogView extends UI.ThrottledWidget.ThrottledWidget { |
| _treeOutline: UI.TreeOutline.TreeOutlineInShadow; |
| _log: SDK.PaintProfiler.PaintProfilerLogItem[]; |
| _treeItemCache: Map<SDK.PaintProfiler.PaintProfilerLogItem, LogTreeElement>; |
| _selectionWindow?: {left: number, right: number}|null; |
| constructor() { |
| super(); |
| this.setMinimumSize(100, 25); |
| this.element.classList.add('overflow-auto'); |
| |
| this._treeOutline = new UI.TreeOutline.TreeOutlineInShadow(); |
| UI.ARIAUtils.setAccessibleName(this._treeOutline.contentElement, i18nString(UIStrings.commandLog)); |
| this.element.appendChild(this._treeOutline.element); |
| this.setDefaultFocusedElement(this._treeOutline.contentElement); |
| |
| this._log = []; |
| this._treeItemCache = new Map(); |
| } |
| |
| setCommandLog(log: SDK.PaintProfiler.PaintProfilerLogItem[]): void { |
| this._log = log; |
| |
| this.updateWindow({left: 0, right: this._log.length}); |
| } |
| |
| _appendLogItem(logItem: SDK.PaintProfiler.PaintProfilerLogItem): void { |
| let treeElement = this._treeItemCache.get(logItem); |
| if (!treeElement) { |
| treeElement = new LogTreeElement(this, logItem); |
| this._treeItemCache.set(logItem, treeElement); |
| } else if (treeElement.parent) { |
| return; |
| } |
| this._treeOutline.appendChild(treeElement); |
| } |
| |
| updateWindow(selectionWindow: {left: number, right: number}|null): void { |
| this._selectionWindow = selectionWindow; |
| this.update(); |
| } |
| |
| doUpdate(): Promise<void> { |
| if (!this._selectionWindow || !this._log.length) { |
| this._treeOutline.removeChildren(); |
| return Promise.resolve(); |
| } |
| const root = this._treeOutline.rootElement(); |
| for (;;) { |
| const child = root.firstChild() as LogTreeElement; |
| if (!child || child._logItem.commandIndex >= this._selectionWindow.left) { |
| break; |
| } |
| root.removeChildAtIndex(0); |
| } |
| for (;;) { |
| const child = root.lastChild() as LogTreeElement; |
| if (!child || child._logItem.commandIndex < this._selectionWindow.right) { |
| break; |
| } |
| root.removeChildAtIndex(root.children().length - 1); |
| } |
| for (let i = this._selectionWindow.left, right = this._selectionWindow.right; i < right; ++i) { |
| this._appendLogItem(this._log[i]); |
| } |
| return Promise.resolve(); |
| } |
| } |
| |
| export class LogTreeElement extends UI.TreeOutline.TreeElement { |
| _logItem: SDK.PaintProfiler.PaintProfilerLogItem; |
| _ownerView: PaintProfilerCommandLogView; |
| _filled: boolean; |
| |
| constructor(ownerView: PaintProfilerCommandLogView, logItem: SDK.PaintProfiler.PaintProfilerLogItem) { |
| super('', Boolean(logItem.params)); |
| this._logItem = logItem; |
| this._ownerView = ownerView; |
| this._filled = false; |
| } |
| |
| onattach(): void { |
| this._update(); |
| } |
| |
| async onpopulate(): Promise<void> { |
| for (const param in this._logItem.params) { |
| LogPropertyTreeElement._appendLogPropertyItem(this, param, this._logItem.params[param]); |
| } |
| } |
| |
| _paramToString(param: SDK.PaintProfiler.RawPaintProfilerLogItemParamValue, name: string): string { |
| if (typeof param !== 'object') { |
| return typeof param === 'string' && param.length > 100 ? name : JSON.stringify(param); |
| } |
| let str = ''; |
| let keyCount = 0; |
| for (const key in param) { |
| if (++keyCount > 4 || typeof param[key] === 'object' || |
| (typeof param[key] === 'string' && param[key].length > 100)) { |
| return name; |
| } |
| if (str) { |
| str += ', '; |
| } |
| str += param[key]; |
| } |
| return str; |
| } |
| |
| _paramsToString(params: SDK.PaintProfiler.RawPaintProfilerLogItemParams|null): string { |
| let str = ''; |
| for (const key in params) { |
| if (str) { |
| str += ', '; |
| } |
| str += this._paramToString(params[key], key); |
| } |
| return str; |
| } |
| |
| _update(): void { |
| const title = document.createDocumentFragment(); |
| UI.UIUtils.createTextChild(title, this._logItem.method + '(' + this._paramsToString(this._logItem.params) + ')'); |
| this.title = title; |
| } |
| } |
| |
| export class LogPropertyTreeElement extends UI.TreeOutline.TreeElement { |
| _property: {name: string, value: SDK.PaintProfiler.RawPaintProfilerLogItemParamValue}; |
| |
| constructor(property: {name: string, value: SDK.PaintProfiler.RawPaintProfilerLogItemParamValue}) { |
| super(); |
| this._property = property; |
| } |
| |
| static _appendLogPropertyItem( |
| element: UI.TreeOutline.TreeElement, name: string, |
| value: SDK.PaintProfiler.RawPaintProfilerLogItemParamValue): void { |
| const treeElement = new LogPropertyTreeElement({name: name, value: value}); |
| element.appendChild(treeElement); |
| if (value && typeof value === 'object') { |
| for (const property in value) { |
| LogPropertyTreeElement._appendLogPropertyItem(treeElement, property, value[property]); |
| } |
| } |
| } |
| |
| onattach(): void { |
| const title = document.createDocumentFragment(); |
| const nameElement = title.createChild('span', 'name'); |
| nameElement.textContent = this._property.name; |
| const separatorElement = title.createChild('span', 'separator'); |
| separatorElement.textContent = ': '; |
| if (this._property.value === null || typeof this._property.value !== 'object') { |
| const valueElement = title.createChild('span', 'value'); |
| valueElement.textContent = JSON.stringify(this._property.value); |
| valueElement.classList.add('cm-js-' + (this._property.value === null ? 'null' : typeof this._property.value)); |
| } |
| this.title = title; |
| } |
| } |
| |
| export class PaintProfilerCategory { |
| name: string; |
| title: string; |
| color: string; |
| |
| constructor(name: string, title: string, color: string) { |
| this.name = name; |
| this.title = title; |
| this.color = color; |
| } |
| } |