blob: 7a655f204ed23056b27feafd440eed157d7d7c32 [file] [log] [blame]
/*
* 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;
}
}