v8/tools/system-analyzer/view/profiler-panel.mjs
Camillo Bruni 00b30232ae [tools] Improve system-analyzer profiler panel
Bug: v8:10644
Change-Id: Ie14c5055a4d24d064def7435fee2cde480844e8e
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/3717985
Reviewed-by: Patrick Thier <pthier@chromium.org>
Commit-Queue: Camillo Bruni <cbruni@chromium.org>
Cr-Commit-Position: refs/heads/main@{#81352}
2022-06-24 12:22:49 +00:00

308 lines
11 KiB
JavaScript

// Copyright 2022 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import {CodeEntry} from '../../codemap.mjs';
import {delay, simpleHtmlEscape} from '../helper.mjs';
import {DeoptLogEntry} from '../log/code.mjs';
import {TickLogEntry} from '../log/tick.mjs';
import {Flame, FlameBuilder, ProfileNode} from '../profiling.mjs';
import {Timeline} from '../timeline.mjs';
import {FocusEvent, SelectRelatedEvent, ToolTipEvent} from './events.mjs';
import {CollapsableElement, CSSColor, DOM, LazyTable} from './helper.mjs';
import {Track} from './timeline/timeline-overview.mjs';
DOM.defineCustomElement('view/profiler-panel',
(templateText) =>
class ProfilerPanel extends CollapsableElement {
/** @type {Timeline<TickLogEntry>} */
_timeline;
/** @type {Timeline<TickLogEntry> | TickLogEntry[]} */
_displayedLogEntries;
/** @type {Timeline<TickLogEntry> | TickLogEntry[]} */
_selectedLogEntries;
/** @type {ProfileNode[]} */
_profileNodes = [];
/** @type {Map<CodeEntry, ProfileNode>} */
_profileNodeMap;
constructor() {
super(templateText);
this._tableNode = this.$('#table');
this._tableNode.onclick = this._handleRowClick.bind(this);
this._showAllRadio = this.$('#show-all');
this._showAllRadio.onclick = _ => this._showEntries(this._timeline);
this._showTimeRangeRadio = this.$('#show-timerange');
this._showTimeRangeRadio.onclick = _ =>
this._showEntries(this._timeline.selectionOrSelf);
this._showSelectionRadio = this.$('#show-selection');
this._showSelectionRadio.onclick = _ =>
this._showEntries(this._selectedLogEntries);
/** @type {TimelineOverview<TickLogEntry>} */
this._timelineOverview = this.$('#overview');
this._timelineOverview.countCallback = (tick, /* trick,*/ track) => {
let count = 0;
for (let j = 0; j < tick.stack.length; j++) {
if (track.hasEntry(tick.stack[j])) count++;
}
return count;
};
this._flameChart = this.$('#flameChart');
this._flameChart.onmousemove = this._handleFlameChartMouseMove.bind(this);
this._flameChart.onclick = this._handleFlameChartClick.bind(this);
}
/** @param {Timeline<TickLogEntry>} timeline */
set timeline(timeline) {
this._timeline = timeline;
this._timelineOverview.timeline = timeline;
}
/** @param {Timeline<TickLogEntry> | TickLogEntry[]} entries */
set selectedLogEntries(entries) {
if (entries === this._timeline) {
this._showAllRadio.click();
} else if (entries === this._timeline.selection) {
this._showTimeRangeRadio.click();
} else {
this._selectedLogEntries = entries;
this._showSelectionRadio.click();
}
}
/** @param {Timeline<TickLogEntry> | TickLogEntry[]} entries */
_showEntries(entries) {
this._displayedLogEntries = entries;
this.requestUpdate();
}
_update() {
this._profileNodeMap = new Map();
const entries = this._displayedLogEntries ?
(this._displayedLogEntries.values ?? []) :
(this._timeline?.values ?? []);
let totalDuration = 0;
let totalEntries = 0;
for (let i = 0; i < entries.length; i++) {
/** @type {TickLogEntry} */
const tick = entries[i];
totalDuration += tick.duration;
const stack = tick.stack;
let prevCodeEntry;
let prevStatsEntry;
for (let j = 0; j < stack.length; j++) {
const codeEntry = stack[j];
totalEntries++;
let statsEntry = this._profileNodeMap.get(codeEntry);
if (statsEntry === undefined) {
statsEntry = new ProfileNode(codeEntry);
this._profileNodeMap.set(codeEntry, statsEntry);
}
statsEntry.ticksAndPosition.push(tick, j);
if (prevCodeEntry !== undefined) {
statsEntry.inCodeEntries.push(prevCodeEntry);
prevStatsEntry.outCodeEntries.push(codeEntry);
}
prevCodeEntry = codeEntry;
prevStatsEntry = statsEntry;
}
}
this._profileNodes = Array.from(this._profileNodeMap.values());
this._profileNodes.sort((a, b) => b.selfCount() - a.selfCount());
const body = DOM.tbody();
let buffer = [];
for (let id = 0; id < this._profileNodes.length; id++) {
/** @type {ProfileNode} */
const node = this._profileNodes[id];
/** @type {CodeEntry} */
const codeEntry = node.codeEntry;
buffer.push(`<tr data-id=${id} class=clickable >`);
buffer.push(`<td class=r >${node.selfCount()}</td>`);
const selfPercent = (node.selfCount() / entries.length * 100).toFixed(1);
buffer.push(`<td class=r >${selfPercent}%</td>`);
buffer.push(`<td class=r >${node.totalCount()}</td>`);
const totalPercent = (node.totalCount() / totalEntries * 100).toFixed(1);
buffer.push(`<td class=r >${totalPercent}%</td>`);
if (node.isLeaf()) {
buffer.push('<td></td>');
} else {
buffer.push('<td class=aC >▸</td>');
}
if (typeof codeEntry === 'number') {
buffer.push('<td></td>');
buffer.push(`<td>${codeEntry}</td>`);
buffer.push('<td></td>');
} else {
const logEntry = codeEntry.logEntry;
let sourcePositionString = logEntry.sourcePosition?.toString() ?? '';
if (logEntry.type == 'SHARED_LIB') {
sourcePositionString = logEntry.name;
}
buffer.push(`<td>${logEntry.type}</td>`);
buffer.push(
`<td class=nm >${simpleHtmlEscape(logEntry.shortName)}</td>`);
buffer.push(
`<td class=sp >${simpleHtmlEscape(sourcePositionString)}</td>`);
}
buffer.push('</tr>');
}
body.innerHTML = buffer.join('');
this._tableNode.replaceChild(body, this._tableNode.tBodies[0]);
this._updateOverview(this._profileNodes[0])
}
_handleRowClick(e) {
let node = e.target;
let dataId = null;
try {
while (dataId === null) {
dataId = node.getAttribute('data-id');
node = node.parentNode;
if (!node) return;
}
} catch (e) {
// getAttribute can throw, this is the lazy way out if we click on the
// title (or anywhere that doesn't have a data-it on any parent).
return;
}
const profileNode = this._profileNodes[dataId];
const className = e.target.className;
if (className == 'aC') {
e.target.className = 'aO';
return;
} else if (className == 'aO') {
e.target.className = 'aC';
return;
} else if (className == 'sp' || className == 'nm') {
// open source position
const codeEntry = profileNode?.codeEntry;
if (codeEntry) {
if (e.shiftKey) {
this.dispatchEvent(new SelectRelatedEvent(codeEntry));
return;
} else if (codeEntry.sourcePosition) {
this.dispatchEvent(new FocusEvent(codeEntry.sourcePosition));
return;
}
}
}
// Default operation: show overview
this._updateOverview(profileNode);
this._updateFlameChart(profileNode);
}
_updateOverview(profileNode) {
if (profileNode === undefined) {
this._timelineOverview.tracks = [];
return;
}
const mainCode = profileNode.codeEntry;
const secondaryCodeEntries = [];
const deopts = [];
const codeCreation = typeof mainCode == 'number' ? [] : [mainCode.logEntry];
if (mainCode.func?.codeEntries.size > 1) {
for (let dynamicCode of mainCode.func.codeEntries) {
for (let related of dynamicCode.logEntry.relatedEntries()) {
if (related instanceof DeoptLogEntry) deopts.push(related);
}
if (dynamicCode === profileNode.codeEntry) continue;
codeCreation.push(dynamicCode.logEntry);
secondaryCodeEntries.push(dynamicCode);
}
}
this._timelineOverview.tracks = [
Track.continuous([mainCode], CSSColor.primaryColor),
Track.continuous(secondaryCodeEntries, CSSColor.secondaryColor),
Track.discrete(deopts, CSSColor.red),
Track.discrete(codeCreation, CSSColor.green),
];
}
async _updateFlameChart(profileNode) {
await delay(100);
const codeEntry = profileNode.codeEntry;
const stacksIn = profileNode.stacksIn();
// Reverse the stack so the FlameBuilder groups the top-most frame
for (let i = 0; i < stacksIn.length; i++) {
stacksIn[i].reverse();
}
const stacksOut = profileNode.stacksOut();
const flameBuilderIn = FlameBuilder.forTicks(stacksIn);
const flameBuilderOut = FlameBuilder.forTicks(stacksOut);
let fragment = new DocumentFragment();
const kItemHeight = 12;
// One empty line at the beginning
const maxInDepth = Math.max(2, flameBuilderIn.maxDepth + 1);
let centerDepth = maxInDepth;
for (const flame of flameBuilderIn.flames) {
// Ignore padded frames.
if (flame.logEntry === undefined) continue;
const codeEntry = flame.logEntry.entry;
const flameProfileNode = this._profileNodeMap.get(codeEntry);
const y = (centerDepth - flame.depth - 1) * kItemHeight;
fragment.appendChild(
this._createFlame(flame, flameProfileNode, y, 'fsIn'));
}
// Add spacing:
centerDepth++;
const y = centerDepth * kItemHeight;
// Create fake Flame for the main entry;
const centerFlame =
new Flame(0, codeEntry.logEntry, 0, profileNode.totalCount());
fragment.appendChild(
this._createFlame(centerFlame, profileNode, y, 'fsMain'));
// Add spacing:
centerDepth += 2;
for (const flame of flameBuilderOut.flames) {
if (flame.logEntry === undefined) continue;
const codeEntry = flame.logEntry.entry;
const flameProfileNode = this._profileNodeMap.get(codeEntry);
const y = (flame.depth + centerDepth) * kItemHeight;
fragment.appendChild(
this._createFlame(flame, flameProfileNode, y, 'fsOut'));
}
this.$('#flameChartFlames').replaceChildren(fragment);
this.$('#flameChartIn').style.height = (maxInDepth * kItemHeight) + 'px';
this.$('#flameChartSelected').style.top =
((maxInDepth + 1) * kItemHeight) + 'px';
this.$('#flameChartOut').style.top = (centerDepth * kItemHeight) + 'px';
this.$('#flameChartOut').style.height =
(flameBuilderOut.maxDepth * kItemHeight) + 'px';
}
_createFlame(flame, profileNode, y, className) {
const ticksToPixel = 4;
const x = flame.time * ticksToPixel;
const width = flame.duration * ticksToPixel;
const div = DOM.div(className);
div.style = `left:${x}px;top:${y}px;width:${width}px`;
div.innerText = flame.name;
div.data = profileNode;
return div;
}
_handleFlameChartMouseMove(e) {
const profileNode = e.target.data;
if (!profileNode) return;
const logEntry = profileNode.codeEntry.logEntry;
this.dispatchEvent(new ToolTipEvent(logEntry, e.target));
}
_handleFlameChartClick(e) {
const profileNode = e.target.data;
if (!profileNode) return;
this._updateOverview(profileNode);
this._updateFlameChart(profileNode)
}
});