blob: e2f85c3bbd56362b7aa12a21f978ab21baecf208 [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.
PerfUI.LineLevelProfile = {};
PerfUI.LineLevelProfile.Performance = class {
constructor() {
this._helper = new PerfUI.LineLevelProfile._Helper('performance');
}
reset() {
this._helper.reset();
}
/**
* @param {!SDK.CPUProfileDataModel} profile
*/
_appendLegacyCPUProfile(profile) {
const target = profile.target();
const nodesToGo = [profile.profileHead];
const sampleDuration = (profile.profileEndTime - profile.profileStartTime) / profile.totalHitCount;
while (nodesToGo.length) {
const nodes = nodesToGo.pop().children;
for (let i = 0; i < nodes.length; ++i) {
const node = nodes[i];
nodesToGo.push(node);
if (!node.url || !node.positionTicks) {
continue;
}
for (let j = 0; j < node.positionTicks.length; ++j) {
const lineInfo = node.positionTicks[j];
const line = lineInfo.line;
const time = lineInfo.ticks * sampleDuration;
this._helper.addLineData(target, node.url, line, time);
}
}
}
}
/**
* @param {!SDK.CPUProfileDataModel} profile
*/
appendCPUProfile(profile) {
if (!profile.lines) {
this._appendLegacyCPUProfile(profile);
this._helper.scheduleUpdate();
return;
}
const target = profile.target();
for (let i = 1; i < profile.samples.length; ++i) {
const line = profile.lines[i];
if (!line) {
continue;
}
const node = profile.nodeByIndex(i);
const scriptIdOrUrl = node.scriptId || node.url;
if (!scriptIdOrUrl) {
continue;
}
const time = profile.timestamps[i] - profile.timestamps[i - 1];
this._helper.addLineData(target, scriptIdOrUrl, line, time);
}
this._helper.scheduleUpdate();
}
};
PerfUI.LineLevelProfile.Memory = class {
constructor() {
this._helper = new PerfUI.LineLevelProfile._Helper('memory');
}
reset() {
this._helper.reset();
}
/**
* @param {!Protocol.HeapProfiler.SamplingHeapProfile} profile
* @param {?SDK.Target} target
*/
appendHeapProfile(profile, target) {
const helper = this._helper;
processNode(profile.head);
helper.scheduleUpdate();
/**
* @param {!Protocol.HeapProfiler.SamplingHeapProfileNode} node
*/
function processNode(node) {
node.children.forEach(processNode);
if (!node.selfSize) {
return;
}
const script = Number(node.callFrame.scriptId) || node.callFrame.url;
if (!script) {
return;
}
const line = node.callFrame.lineNumber + 1;
helper.addLineData(target, script, line, node.selfSize);
}
}
};
PerfUI.LineLevelProfile._Helper = class {
/**
* @param {string} type
*/
constructor(type) {
this._type = type;
this._locationPool = new Bindings.LiveLocationPool();
this._updateTimer = null;
this.reset();
}
reset() {
// The second map uses string keys for script URLs and numbers for scriptId.
/** @type {!Map<?SDK.Target, !Map<string|number, !Map<number, number>>>} */
this._lineData = new Map();
this.scheduleUpdate();
}
/**
* @param {?SDK.Target} target
* @param {string|number} scriptIdOrUrl
* @param {number} line
* @param {number} data
*/
addLineData(target, scriptIdOrUrl, line, data) {
let targetData = this._lineData.get(target);
if (!targetData) {
targetData = new Map();
this._lineData.set(target, targetData);
}
let scriptData = targetData.get(scriptIdOrUrl);
if (!scriptData) {
scriptData = new Map();
targetData.set(scriptIdOrUrl, scriptData);
}
scriptData.set(line, (scriptData.get(line) || 0) + data);
}
scheduleUpdate() {
if (this._updateTimer) {
return;
}
this._updateTimer = setTimeout(() => {
this._updateTimer = null;
this._doUpdate();
}, 0);
}
_doUpdate() {
this._locationPool.disposeAll();
Workspace.workspace.uiSourceCodes().forEach(uiSourceCode => uiSourceCode.removeDecorationsForType(this._type));
for (const targetToScript of this._lineData) {
const target = /** @type {?SDK.Target} */ (targetToScript[0]);
const debuggerModel = target ? target.model(SDK.DebuggerModel) : null;
const scriptToLineMap = /** @type {!Map<string|number, !Map<number, number>>} */ (targetToScript[1]);
for (const scriptToLine of scriptToLineMap) {
const scriptIdOrUrl = /** @type {string|number} */ (scriptToLine[0]);
const lineToDataMap = /** @type {!Map<number, number>} */ (scriptToLine[1]);
// debuggerModel is null when the profile is loaded from file.
// Try to get UISourceCode by the URL in this case.
const uiSourceCode = !debuggerModel && typeof scriptIdOrUrl === 'string' ?
Workspace.workspace.uiSourceCodeForURL(scriptIdOrUrl) :
null;
if (!debuggerModel && !uiSourceCode) {
continue;
}
for (const lineToData of lineToDataMap) {
const line = /** @type {number} */ (lineToData[0]) - 1;
const data = /** @type {number} */ (lineToData[1]);
if (uiSourceCode) {
uiSourceCode.addLineDecoration(line, this._type, data);
continue;
}
const rawLocation = typeof scriptIdOrUrl === 'string' ?
debuggerModel.createRawLocationByURL(scriptIdOrUrl, line, 0) :
debuggerModel.createRawLocationByScriptId(String(scriptIdOrUrl), line, 0);
if (rawLocation) {
new PerfUI.LineLevelProfile.Presentation(rawLocation, this._type, data, this._locationPool);
}
}
}
}
}
};
PerfUI.LineLevelProfile.Presentation = class {
/**
* @param {!SDK.DebuggerModel.Location} rawLocation
* @param {string} type
* @param {number} time
* @param {!Bindings.LiveLocationPool} locationPool
*/
constructor(rawLocation, type, time, locationPool) {
this._type = type;
this._time = time;
this._uiLocation = null;
Bindings.debuggerWorkspaceBinding.createLiveLocation(rawLocation, this.updateLocation.bind(this), locationPool);
}
/**
* @param {!Bindings.LiveLocation} liveLocation
*/
updateLocation(liveLocation) {
if (this._uiLocation) {
this._uiLocation.uiSourceCode.removeDecorationsForType(this._type);
}
this._uiLocation = liveLocation.uiLocation();
if (this._uiLocation) {
this._uiLocation.uiSourceCode.addLineDecoration(this._uiLocation.lineNumber, this._type, this._time);
}
}
};
/**
* @implements {SourceFrame.LineDecorator}
*/
PerfUI.LineLevelProfile.LineDecorator = class {
/**
* @override
* @param {!Workspace.UISourceCode} uiSourceCode
* @param {!TextEditor.CodeMirrorTextEditor} textEditor
* @param {string} type
*/
decorate(uiSourceCode, textEditor, type) {
const gutterType = `CodeMirror-gutter-${type}`;
const decorations = uiSourceCode.decorationsForType(type);
textEditor.uninstallGutter(gutterType);
if (!decorations || !decorations.size) {
return;
}
textEditor.installGutter(gutterType, false);
for (const decoration of decorations) {
const value = /** @type {number} */ (decoration.data());
const element = this._createElement(type, value);
textEditor.setGutterDecoration(decoration.range().startLine, gutterType, element);
}
}
/**
* @param {string} type
* @param {number} value
* @return {!Element}
*/
_createElement(type, value) {
const element = createElementWithClass('div', 'text-editor-line-marker-text');
if (type === 'performance') {
const intensity = Number.constrain(Math.log10(1 + 10 * value) / 5, 0.02, 1);
element.textContent = Common.UIString('%.1f', value);
element.style.backgroundColor = `hsla(44, 100%, 50%, ${intensity.toFixed(3)})`;
element.createChild('span', 'line-marker-units').textContent = ls`ms`;
} else {
const intensity = Number.constrain(Math.log10(1 + 2e-3 * value) / 5, 0.02, 1);
element.style.backgroundColor = `hsla(217, 100%, 70%, ${intensity.toFixed(3)})`;
value /= 1e3;
let units;
let fractionDigits;
if (value >= 1e3) {
units = ls`MB`;
value /= 1e3;
fractionDigits = value >= 20 ? 0 : 1;
} else {
units = ls`KB`;
fractionDigits = 0;
}
element.textContent = Common.UIString(`%.${fractionDigits}f`, value);
element.createChild('span', 'line-marker-units').textContent = units;
}
return element;
}
};