Commit 8ff6a214 authored by Camillo Bruni's avatar Camillo Bruni Committed by V8 LUCI CQ

[tools][system-analyzer] Support profiling ticks

This adds a first crude version of displaying a flamechart in the
system-analyzer.

- Basic function types are distinguishable by colors.
- Tooltip information is available as well

Bug: v8:11835
Change-Id: I87e092f749d4c16aa5017af39df8d2f7bd7e2edd
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/2928179
Commit-Queue: Camillo Bruni <cbruni@chromium.org>
Reviewed-by: 's avatarPatrick Thier <pthier@chromium.org>
Cr-Commit-Position: refs/heads/master@{#74860}
parent 81181a8a
...@@ -184,7 +184,7 @@ export class CodeMap { ...@@ -184,7 +184,7 @@ export class CodeMap {
result = this.findInTree_(this.libraries_, addr); result = this.findInTree_(this.libraries_, addr);
if (!result) return null; if (!result) return null;
} }
return { entry : result.value, offset : addr - result.key }; return {entry: result.value, offset: addr - result.key};
} }
const min = this.dynamics_.findMin(); const min = this.dynamics_.findMin();
const max = this.dynamics_.findMax(); const max = this.dynamics_.findMax();
...@@ -197,7 +197,7 @@ export class CodeMap { ...@@ -197,7 +197,7 @@ export class CodeMap {
entry.name = this.dynamicsNameGen_.getName(entry.name); entry.name = this.dynamicsNameGen_.getName(entry.name);
entry.nameUpdated_ = true; entry.nameUpdated_ = true;
} }
return { entry, offset : addr - dynaEntry.key }; return {entry, offset: addr - dynaEntry.key};
} }
return null; return null;
} }
......
...@@ -160,7 +160,7 @@ export class LogReader { ...@@ -160,7 +160,7 @@ export class LogReader {
} else if (firstChar != 'o') { } else if (firstChar != 'o') {
fullStack.push(parseInt(frame, 16)); fullStack.push(parseInt(frame, 16));
} else { } else {
this.printError(`dropping: ${frame}`); console.error(`dropping: ${frame}`);
} }
} }
return fullStack; return fullStack;
......
...@@ -158,6 +158,14 @@ class SourceInfo { ...@@ -158,6 +158,14 @@ class SourceInfo {
} }
} }
class Tick {
constructor(time_ns, vmState, processedStack) {
this.time = time_ns;
this.state = vmState;
this.stack = processedStack;
}
}
/** /**
* Creates a profile object for processing profiling-related events * Creates a profile object for processing profiling-related events
* and calculating function execution times. * and calculating function execution times.
...@@ -216,6 +224,23 @@ export class Profile { ...@@ -216,6 +224,23 @@ export class Profile {
TURBOFAN: 5, TURBOFAN: 5,
} }
static VMState = {
JS: 0,
GC: 1,
PARSER: 2,
BYTECODE_COMPILER: 3,
// TODO(cbruni): add BASELINE_COMPILER
COMPILER: 4,
OTHER: 5,
EXTERNAL: 6,
IDLE: 7,
}
static CodeType = {
CPP: 0,
SHARED_LIB: 1
}
/** /**
* Parser for dynamic code optimization state. * Parser for dynamic code optimization state.
*/ */
...@@ -461,10 +486,11 @@ export class Profile { ...@@ -461,10 +486,11 @@ export class Profile {
* @param {Array<number>} stack Stack sample. * @param {Array<number>} stack Stack sample.
*/ */
recordTick(time_ns, vmState, stack) { recordTick(time_ns, vmState, stack) {
const processedStack = this.resolveAndFilterFuncs_(stack); const {nameStack, entryStack} = this.resolveAndFilterFuncs_(stack);
this.bottomUpTree_.addPath(processedStack); this.bottomUpTree_.addPath(nameStack);
processedStack.reverse(); nameStack.reverse();
this.topDownTree_.addPath(processedStack); this.topDownTree_.addPath(nameStack);
this.ticks_.push(new Tick(time_ns, vmState, entryStack));
} }
/** /**
...@@ -474,12 +500,15 @@ export class Profile { ...@@ -474,12 +500,15 @@ export class Profile {
* @param {Array<number>} stack Stack sample. * @param {Array<number>} stack Stack sample.
*/ */
resolveAndFilterFuncs_(stack) { resolveAndFilterFuncs_(stack) {
const result = []; const nameStack = [];
const entryStack = [];
let last_seen_c_function = ''; let last_seen_c_function = '';
let look_for_first_c_function = false; let look_for_first_c_function = false;
for (let i = 0; i < stack.length; ++i) { for (let i = 0; i < stack.length; ++i) {
const entry = this.codeMap_.findEntry(stack[i]); const pc = stack[i];
const entry = this.codeMap_.findEntry(pc);
if (entry) { if (entry) {
entryStack.push(entry);
const name = entry.getName(); const name = entry.getName();
if (i === 0 && (entry.type === 'CPP' || entry.type === 'SHARED_LIB')) { if (i === 0 && (entry.type === 'CPP' || entry.type === 'SHARED_LIB')) {
look_for_first_c_function = true; look_for_first_c_function = true;
...@@ -488,16 +517,15 @@ export class Profile { ...@@ -488,16 +517,15 @@ export class Profile {
last_seen_c_function = name; last_seen_c_function = name;
} }
if (!this.skipThisFunction(name)) { if (!this.skipThisFunction(name)) {
result.push(name); nameStack.push(name);
} }
} else { } else {
this.handleUnknownCode(Profile.Operation.TICK, stack[i], i); this.handleUnknownCode(Profile.Operation.TICK, pc, i);
if (i === 0) result.push("UNKNOWN"); if (i === 0) nameStack.push("UNKNOWN");
entryStack.push(pc);
} }
if (look_for_first_c_function && if (look_for_first_c_function && i > 0 &&
i > 0 && (!entry || entry.type !== 'CPP') && last_seen_c_function !== '') {
(!entry || entry.type !== 'CPP') &&
last_seen_c_function !== '') {
if (this.c_entries_[last_seen_c_function] === undefined) { if (this.c_entries_[last_seen_c_function] === undefined) {
this.c_entries_[last_seen_c_function] = 0; this.c_entries_[last_seen_c_function] = 0;
} }
...@@ -505,7 +533,7 @@ export class Profile { ...@@ -505,7 +533,7 @@ export class Profile {
look_for_first_c_function = false; // Found it, we're done. look_for_first_c_function = false; // Found it, we're done.
} }
} }
return result; return {nameStack, entryStack};
} }
/** /**
......
...@@ -55,6 +55,7 @@ found in the LICENSE file. --> ...@@ -55,6 +55,7 @@ found in the LICENSE file. -->
<section id="container" class="initial"> <section id="container" class="initial">
<timeline-panel id="timeline-panel"> <timeline-panel id="timeline-panel">
<timeline-track id="sample-track" title="Samples"></timeline-track>
<timeline-track id="map-track" title="Map"></timeline-track> <timeline-track id="map-track" title="Map"></timeline-track>
<timeline-track id="ic-track" title="IC"></timeline-track> <timeline-track id="ic-track" title="IC"></timeline-track>
<timeline-track id="deopt-track" title="Deopt"></timeline-track> <timeline-track id="deopt-track" title="Deopt"></timeline-track>
......
...@@ -12,6 +12,7 @@ import {IcLogEntry} from './log/ic.mjs'; ...@@ -12,6 +12,7 @@ import {IcLogEntry} from './log/ic.mjs';
import {LogEntry} from './log/log.mjs'; import {LogEntry} from './log/log.mjs';
import {MapLogEntry} from './log/map.mjs'; import {MapLogEntry} from './log/map.mjs';
import {Processor} from './processor.mjs'; import {Processor} from './processor.mjs';
import {Timeline} from './timeline.mjs'
import {FocusEvent, SelectionEvent, SelectRelatedEvent, SelectTimeEvent, ToolTipEvent,} from './view/events.mjs'; import {FocusEvent, SelectionEvent, SelectRelatedEvent, SelectTimeEvent, ToolTipEvent,} from './view/events.mjs';
import {$, CSSColor, groupBy} from './view/helper.mjs'; import {$, CSSColor, groupBy} from './view/helper.mjs';
...@@ -26,6 +27,7 @@ class App { ...@@ -26,6 +27,7 @@ class App {
logFileReader: $('#log-file-reader'), logFileReader: $('#log-file-reader'),
timelinePanel: $('#timeline-panel'), timelinePanel: $('#timeline-panel'),
sampleTrack: $('#sample-track'),
mapTrack: $('#map-track'), mapTrack: $('#map-track'),
icTrack: $('#ic-track'), icTrack: $('#ic-track'),
deoptTrack: $('#deopt-track'), deoptTrack: $('#deopt-track'),
...@@ -334,6 +336,12 @@ class App { ...@@ -334,6 +336,12 @@ class App {
} }
refreshTimelineTrackView() { refreshTimelineTrackView() {
const ticks = this._state.profile.ticks_;
if (ticks.length > 0) {
this._view.sampleTrack.data = new Timeline(
Object, ticks, ticks[0].time, ticks[ticks.length - 1].time);
this._view.sampleTrack.ticks = ticks;
}
this._view.mapTrack.data = this._state.mapTimeline; this._view.mapTrack.data = this._state.mapTimeline;
this._view.icTrack.data = this._state.icTimeline; this._view.icTrack.data = this._state.icTimeline;
this._view.deoptTrack.data = this._state.deoptTimeline; this._view.deoptTrack.data = this._state.deoptTimeline;
......
...@@ -50,6 +50,7 @@ export class CodeLogEntry extends LogEntry { ...@@ -50,6 +50,7 @@ export class CodeLogEntry extends LogEntry {
this._kind = kind; this._kind = kind;
this._kindName = kindName; this._kindName = kindName;
this._entry = entry; this._entry = entry;
entry.logEntry = this;
} }
get kind() { get kind() {
...@@ -92,7 +93,7 @@ export class CodeLogEntry extends LogEntry { ...@@ -92,7 +93,7 @@ export class CodeLogEntry extends LogEntry {
static get propertyNames() { static get propertyNames() {
return [ return [
'type', 'kind', 'kindName', 'size', 'functionName', 'sourcePosition', 'functionName', 'sourcePosition', 'kindName', 'size', 'type', 'kind',
'script', 'source', 'code' 'script', 'source', 'code'
]; ];
} }
......
...@@ -78,6 +78,16 @@ export class Processor extends LogReader { ...@@ -78,6 +78,16 @@ export class Processor extends LogReader {
}, },
'sfi-move': 'sfi-move':
{parsers: [parseInt, parseInt], processor: this.processFunctionMove}, {parsers: [parseInt, parseInt], processor: this.processFunctionMove},
'tick': {
parsers:
[parseInt, parseInt, parseInt, parseInt, parseInt, parseVarArgs],
processor: this.processTick
},
'active-runtime-timer': undefined,
'heap-sample-begin': undefined,
'heap-sample-end': undefined,
'timer-event-start': undefined,
'timer-event-end': undefined,
'map-create': 'map-create':
{parsers: [parseInt, parseString], processor: this.processMapCreate}, {parsers: [parseInt, parseString], processor: this.processMapCreate},
'map': { 'map': {
...@@ -264,6 +274,29 @@ export class Processor extends LogReader { ...@@ -264,6 +274,29 @@ export class Processor extends LogReader {
this._profile.moveFunc(from, to); this._profile.moveFunc(from, to);
} }
processTick(
pc, ns_since_start, is_external_callback, tos_or_external_callback,
vmState, stack) {
if (is_external_callback) {
// Don't use PC when in external callback code, as it can point
// inside callback's code, and we will erroneously report
// that a callback calls itself. Instead we use tos_or_external_callback,
// as simply resetting PC will produce unaccounted ticks.
pc = tos_or_external_callback;
tos_or_external_callback = 0;
} else if (tos_or_external_callback) {
// Find out, if top of stack was pointing inside a JS function
// meaning that we have encountered a frameless invocation.
const funcEntry = this._profile.findEntry(tos_or_external_callback);
if (!funcEntry?.isJSFunction?.()) {
tos_or_external_callback = 0;
}
}
this._profile.recordTick(
ns_since_start, vmState,
this.processStack(pc, tos_or_external_callback, stack));
}
processCodeSourceInfo( processCodeSourceInfo(
start, scriptId, startPos, endPos, sourcePositions, inliningPositions, start, scriptId, startPos, endPos, sourcePositions, inliningPositions,
inlinedFunctions) { inlinedFunctions) {
......
...@@ -25,7 +25,7 @@ found in the LICENSE file. --> ...@@ -25,7 +25,7 @@ found in the LICENSE file. -->
opacity: 0.5; opacity: 0.5;
} }
#timelineChunks { #timelineSamples, #timelineChunks {
height: 200px; height: 200px;
position: absolute; position: absolute;
margin-right: 100px; margin-right: 100px;
...@@ -152,7 +152,6 @@ found in the LICENSE file. --> ...@@ -152,7 +152,6 @@ found in the LICENSE file. -->
position: absolute; position: absolute;
} }
.content { .content {
display: flex; display: flex;
position: relative; position: relative;
...@@ -172,6 +171,22 @@ found in the LICENSE file. --> ...@@ -172,6 +171,22 @@ found in the LICENSE file. -->
flex: initial; flex: initial;
} }
.flame {
background-color: gray;
overflow: hidden;
height: 10px;
position: absolute;
font-size: 8px;
}
.flame.Opt{
background-color: red;
}
.flame.Baseline{
background-color: blue;
}
.flame.default {
background-color: black;
}
</style> </style>
<div class="content"> <div class="content">
...@@ -187,6 +202,7 @@ found in the LICENSE file. --> ...@@ -187,6 +202,7 @@ found in the LICENSE file. -->
</div> </div>
<div id="timelineLabel">Frequency</div> <div id="timelineLabel">Frequency</div>
<div id="timelineChunks"></div> <div id="timelineChunks"></div>
<div id="timelineSamples"></div>
<canvas id="timelineCanvas"></canvas> <canvas id="timelineCanvas"></canvas>
</div> </div>
......
...@@ -2,11 +2,25 @@ ...@@ -2,11 +2,25 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import {Profile} from '../../../profile.mjs'
import {delay} from '../../helper.mjs';
import {kChunkHeight, kChunkWidth} from '../../log/map.mjs'; import {kChunkHeight, kChunkWidth} from '../../log/map.mjs';
import {MapLogEntry} from '../../log/map.mjs'; import {MapLogEntry} from '../../log/map.mjs';
import {FocusEvent, SelectionEvent, SelectTimeEvent, SynchronizeSelectionEvent, ToolTipEvent,} from '../events.mjs'; import {FocusEvent, SelectionEvent, SelectTimeEvent, SynchronizeSelectionEvent, ToolTipEvent,} from '../events.mjs';
import {CSSColor, DOM, gradientStopsFromGroups, V8CustomElement} from '../helper.mjs'; import {CSSColor, DOM, gradientStopsFromGroups, V8CustomElement} from '../helper.mjs';
class Flame {
constructor(time, entry) {
this.start = time;
this.end = this.start;
this.entry = entry;
}
stop(time) {
this.end = time;
this.duration = time - this.start
}
}
DOM.defineCustomElement('view/timeline/timeline-track', DOM.defineCustomElement('view/timeline/timeline-track',
(templateText) => (templateText) =>
class TimelineTrack extends V8CustomElement { class TimelineTrack extends V8CustomElement {
...@@ -21,6 +35,7 @@ DOM.defineCustomElement('view/timeline/timeline-track', ...@@ -21,6 +35,7 @@ DOM.defineCustomElement('view/timeline/timeline-track',
_chunkMouseMoveHandler = this._handleChunkMouseMove.bind(this); _chunkMouseMoveHandler = this._handleChunkMouseMove.bind(this);
_chunkClickHandler = this._handleChunkClick.bind(this); _chunkClickHandler = this._handleChunkClick.bind(this);
_chunkDoubleClickHandler = this._handleChunkDoubleClick.bind(this); _chunkDoubleClickHandler = this._handleChunkDoubleClick.bind(this);
_flameMouseOverHandler = this._handleFlameMouseOver.bind(this);
constructor() { constructor() {
super(templateText); super(templateText);
...@@ -91,6 +106,10 @@ DOM.defineCustomElement('view/timeline/timeline-track', ...@@ -91,6 +106,10 @@ DOM.defineCustomElement('view/timeline/timeline-track',
return this.$('#timelineChunks'); return this.$('#timelineChunks');
} }
get timelineSamples() {
return this.$('#timelineSamples');
}
get timelineNode() { get timelineNode() {
return this.$('#timeline'); return this.$('#timeline');
} }
...@@ -98,6 +117,63 @@ DOM.defineCustomElement('view/timeline/timeline-track', ...@@ -98,6 +117,63 @@ DOM.defineCustomElement('view/timeline/timeline-track',
_update() { _update() {
this._updateTimeline(); this._updateTimeline();
this._legend.update(); this._legend.update();
// TODO(cbruni: Create separate component for profile ticks
if (this.ticks) {
this.drawFlamechart()
}
}
async drawFlamechart() {
await delay(100);
// TODO(cbruni): Use dynamic drawing strategy here that scales
DOM.removeAllChildren(this.timelineSamples);
const stack = [];
const kMaxNumOfDisplayedFlames = 2000
const kIncrement = Math.max(1, this.ticks.length / 5000) | 0;
for (let i = 0; i < this.ticks.length; i += kIncrement) {
if (i % 500 == 0) {
await delay(10);
}
const tick = this.ticks[i];
for (let j = 0; j < tick.stack.length; j++) {
const entry = tick.stack[j];
if (typeof entry !== 'object') continue;
if (stack.length <= j) {
stack.push(new Flame(tick.time, entry));
} else {
const flame = stack[j];
if (flame.entry !== entry) {
for (let k = j; k < stack.length; k++) {
stack[k].stop(tick.time);
this.drawFlame(stack[k], k);
}
stack.length = j;
stack[j] = new Flame(tick.time, entry);
}
}
}
}
}
drawFlame(flame, depth) {
let type = 'default';
if (flame.entry.state) {
type = Profile.getKindFromState(flame.entry.state);
}
const node = DOM.div(['flame', type]);
node.data = flame.entry;
node.onmouseover = this._flameMouseOverHandler
const style = node.style;
style.top = `${depth * 10}px`;
style.left = `${flame.start * this._timeToPixel}px`;
style.width = `${flame.duration * this._timeToPixel}px`;
node.innerText = flame.entry.getRawName();
this.timelineSamples.appendChild(node);
}
_handleFlameMouseOver(event) {
const codeEntry = event.target.data;
this.dispatchEvent(new ToolTipEvent(codeEntry.logEntry, event.target));
} }
set nofChunks(count) { set nofChunks(count) {
...@@ -163,6 +239,8 @@ DOM.defineCustomElement('view/timeline/timeline-track', ...@@ -163,6 +239,8 @@ DOM.defineCustomElement('view/timeline/timeline-track',
let duration = end - start; let duration = end - start;
this._timeToPixel = chunks.length * kChunkWidth / duration; this._timeToPixel = chunks.length * kChunkWidth / duration;
this._timeStartOffset = start * this._timeToPixel; this._timeStartOffset = start * this._timeToPixel;
// TODO(cbruni: Create separate component for profile ticks
if (this.ticks) return;
for (let i = 0; i < chunks.length; i++) { for (let i = 0; i < chunks.length; i++) {
let chunk = chunks[i]; let chunk = chunks[i];
let height = (chunk.size() / max * kChunkHeight); let height = (chunk.size() / max * kChunkHeight);
......
...@@ -36,6 +36,7 @@ found in the LICENSE file. --> ...@@ -36,6 +36,7 @@ found in the LICENSE file. -->
#body { #body {
display: none; display: none;
position: absolute; position: absolute;
z-index: 99999;
--tip-offset: 10px; --tip-offset: 10px;
--tip-width: 10px; --tip-width: 10px;
--tip-height: 15px; --tip-height: 15px;
...@@ -84,6 +85,9 @@ found in the LICENSE file. --> ...@@ -84,6 +85,9 @@ found in the LICENSE file. -->
.properties > tbody > tr > td:nth-child(2n+1):after { .properties > tbody > tr > td:nth-child(2n+1):after {
content: ':'; content: ':';
} }
.properties {
min-width: 350px;
}
</style> </style>
<div id="body"> <div id="body">
......
...@@ -34,9 +34,7 @@ DOM.defineCustomElement( ...@@ -34,9 +34,7 @@ DOM.defineCustomElement(
rect.x += rect.width / 2; rect.x += rect.width / 2;
let atRight = this._useRight(rect.x); let atRight = this._useRight(rect.x);
let atBottom = this._useBottom(rect.y); let atBottom = this._useBottom(rect.y);
if (atBottom) { if (atBottom) rect.y += rect.height;
rect.y += rect.height;
}
this._setPosition(rect, atRight, atBottom); this._setPosition(rect, atRight, atBottom);
this.requestUpdate(true); this.requestUpdate(true);
} }
...@@ -174,6 +172,7 @@ class TableBuilder { ...@@ -174,6 +172,7 @@ class TableBuilder {
_addFooter() { _addFooter() {
if (this._instance === undefined) return; if (this._instance === undefined) return;
const td = this._table.createTFoot().insertRow().insertCell(); const td = this._table.createTFoot().insertRow().insertCell();
td.colSpan = 2;
let button = let button =
td.appendChild(DOM.button('Show', this._tooltip._logEntryClickHandler)); td.appendChild(DOM.button('Show', this._tooltip._logEntryClickHandler));
button.data = this._instance; button.data = this._instance;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment