Upload Kmake

This commit is contained in:
Gorochu
2026-05-26 23:36:42 -07:00
parent ba051b2f74
commit 555ec72358
41615 changed files with 13344630 additions and 1 deletions

View File

@ -0,0 +1,159 @@
// Copyright 2020 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.
class State {
_timeSelection = {start: 0, end: Infinity};
_map;
_ic;
_selectedMapLogEntries;
_selectedIcLogEntries;
_selectedDeoptLogEntries;
_selecteCodeLogEntries;
_selectedSourcePositions;
_nofChunks;
_chunks;
_icTimeline;
_mapTimeline;
_deoptTimeline;
_codeTimeline;
_tickTimeline;
_timerTimeline;
_minStartTime = Number.POSITIVE_INFINITY;
_maxEndTime = Number.NEGATIVE_INFINITY;
get minStartTime() {
return this._minStartTime;
}
get maxEndTime() {
return this._maxEndTime;
}
selectTimeRange(start, end) {
this.timeSelection.start = start;
this.timeSelection.end = end;
if (start == 0 && end == Infinity) {
this.timelines.forEach(each => each.clearSelection());
} else {
this.timelines.forEach(each => each.selectTimeRange(start, end));
}
}
setTimelines(
mapTimeline, icTimeline, deoptTimeline, codeTimeline, tickTimeline,
timerTimeline) {
this._mapTimeline = mapTimeline;
this._icTimeline = icTimeline;
this._deoptTimeline = deoptTimeline;
this._codeTimeline = codeTimeline;
this._tickTimeline = tickTimeline;
this._timerTimeline = timerTimeline;
for (let timeline of arguments) {
if (timeline === undefined) return;
this._minStartTime = Math.min(this._minStartTime, timeline.startTime);
this._maxEndTime = Math.max(this._maxEndTime, timeline.endTime);
}
for (let timeline of arguments) {
timeline.startTime = this._minStartTime;
timeline.endTime = this._maxEndTime;
}
}
get mapTimeline() {
return this._mapTimeline;
}
get icTimeline() {
return this._icTimeline;
}
get deoptTimeline() {
return this._deoptTimeline;
}
get codeTimeline() {
return this._codeTimeline;
}
get tickTimeline() {
return this._tickTimeline;
}
get timerTimeline() {
return this._timerTimeline;
}
get timelines() {
return [
this._mapTimeline, this._icTimeline, this._deoptTimeline,
this._codeTimeline, this._tickTimeline, this._timerTimeline
];
}
set chunks(value) {
// TODO(zcankara) split up between maps and ics, and every timeline track
this._chunks = value;
}
get chunks() {
// TODO(zcankara) split up between maps and ics, and every timeline track
return this._chunks;
}
get nofChunks() {
return this._nofChunks;
}
set nofChunks(count) {
this._nofChunks = count;
}
get map() {
// TODO(zcankara) rename as selectedMapEvents, array of selected events
return this._map;
}
set map(value) {
// TODO(zcankara) rename as selectedMapEvents, array of selected events
if (!value) return;
this._map = value;
}
get ic() {
// TODO(zcankara) rename selectedICEvents, array of selected events
return this._ic;
}
set ic(value) {
// TODO(zcankara) rename selectedIcEvents, array of selected events
if (!value) return;
this._ic = value;
}
get selectedMapLogEntries() {
return this._selectedMapLogEntries;
}
set selectedMapLogEntries(value) {
if (!value) return;
this._selectedMapLogEntries = value;
}
get selectedSourcePositions() {
return this._selectedSourcePositions;
}
set selectedSourcePositions(value) {
this._selectedSourcePositions = value;
}
get selectedIcLogEntries() {
return this._selectedIcLogEntries;
}
set selectedIcLogEntries(value) {
if (!value) return;
this._selectedIcLogEntries = value;
}
get timeSelection() {
return this._timeSelection;
}
get entries() {
if (!this.map) return {};
return {
map: this.map.id, time: this.map.time
}
}
}
export {State};

View File

@ -0,0 +1,88 @@
// Copyright 2020 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.
export class Group {
constructor(key, id, parentTotal, entries, length = 0) {
this.key = key;
this.id = id;
this.entries = entries;
this.length = length || entries.length;
this.parentTotal = parentTotal;
}
get percent() {
return this.length / this.parentTotal * 100;
}
add() {
this.length++;
}
addEntry(entry) {
this.length++;
this.entries.push(entry);
}
}
export function groupBy(array, keyFunction, collect = false) {
if (array.length === 0) return [];
if (keyFunction === undefined) keyFunction = each => each;
const keyToGroup = new Map();
const groups = [];
const sharedEmptyArray = [];
let id = 0;
// This is performance critical, resorting to for-loop
for (let each of array) {
const key = keyFunction(each);
let group = keyToGroup.get(key);
if (group !== undefined) {
collect ? group.addEntry(each) : group.add();
continue;
}
let entries = collect ? [each] : sharedEmptyArray;
group = new Group(key, id++, array.length, entries, 1);
groups.push(group);
keyToGroup.set(key, group);
}
// Sort by length
return groups.sort((a, b) => b.length - a.length);
}
export function arrayEquals(left, right, compareFn) {
if (left == right) return true;
if (left.length != right.length) return false;
if (compareFn === undefined) {
for (let i = 0; i < left.length; i++) {
if (left[i] != right[i]) return false;
}
} else {
for (let i = 0; i < left.length; i++) {
if (!compareFn(left[i], right[i])) return false;
}
}
return true;
}
export function entriesEquals(left, right) {
if (left == right) return true;
if (left == undefined) return right == undefined;
const leftEntries = Object.entries(left);
const rightEntries = Object.entries(right);
if (leftEntries.length !== rightEntries.length) return false;
for (let i = 0; i < leftEntries.length; i++) {
const l = leftEntries[i];
const r = rightEntries[i];
if (l[0] != r[0]) return false;
if (l[1] != r[1]) return false;
}
return true;
}
export function keysEquals(left, right) {
if (left == right) return true;
if (left == undefined) return right == undefined;
return arrayEquals(Object.keys(left), Object.keys(right));
}
export * from '../js/helper.mjs'

289
deps/v8/tools/system-analyzer/index.css vendored Normal file
View File

@ -0,0 +1,289 @@
:root {
--code-font: Consolas, Monaco, Menlo, monospace;
--code-font-size: 12px;
--background-color: #000000;
--surface-color-rgb: 18, 18, 18;
--surface-color: rgb(var(--surface-color-rgb));
--primary-color: #bb86fc;
--secondary-color: #03dac6;
--on-surface-color: #ffffff;
--on-background-color: #f5f0f0;
--on-primary-color: #000000;
--on-secondary-color: #000000;
--default-color: #9b6edc;
--error-color: #cf6679;
--map-background-color: #5e5454;
--timeline-background-color: #1f1f1f;
--file-reader-background-color: #ffffff80;
--file-reader-border-color: #ffffff;
--red: #dc6eae;
--green: #aedc6e;
--yellow: #eeff41;
--blue: #6e77dc;
--orange: #dc9b6e;
--violet: #d26edc;
--selection-color: rgba(133, 68, 163, 0.5);
--border-color-rgb: 128, 128, 128;
--border-color: rgba(var(--border-color-rgb), 0.2);
scrollbar-color: rgba(128, 128, 128, 0.5) rgba(0, 0, 0, 0.0);
}
::-webkit-scrollbar, ::-webkit-scrollbar-track, ::-webkit-scrollbar-corner {
background-color: rgba(0, 0, 0, 0.0);
}
::-webkit-scrollbar, ::-webkit-scrollbar-track {
width: 10px;
height: 10px;
}
::-webkit-scrollbar-thumb {
background-color: rgba(128, 128, 128, 0.5);
border-radius: 8px;
cursor: pointer;
}
::-webkit-scrollbar-thumb:hover {
background-color: rgba(128, 128, 128, 0.8);
}
body {
font-family: sans-serif;
font-size: 14px;
color: var(--on-background-color);
margin: 10px 10px 0 10px;
background-color: var(--background-color);
}
h3 {
margin-block-end: 0.3em;
}
section {
margin-bottom: 10px;
}
kbd {
color: var(--on-primary-color);
background-color: var(--primary-color);
border-radius: 3px;
border: 1px solid var(--on-primary-color);
display: inline-block;
font-size: .9em;
font-weight: bold;
padding: 0px 4px 2px 4px;
white-space: nowrap;
}
kbd, code, pre {
font-family: var(--code-font);
}
code {
font-size: var(--code-font-size);
white-space: pre;
}
a {
color: var(--primary-color);
text-decoration: none;
}
a:hover {
color: var(--secondary-color);
}
a:link {
color: var(--secondary-color);
}
dl {
display: grid;
grid-template-columns: min-content auto;
grid-gap: 5px;
}
dt {
text-align: right;
white-space: nowrap;
}
dd {
margin: 0;
}
.panel {
background-color: var(--surface-color);
color: var(--on-surface-color);
padding: 10px;
border-radius: 10px;
border: 3px solid var(--border-color);
overflow: hidden;
}
.panelBody {
position: relative;
max-height: 800px;
overflow-y: scroll;
margin: 0 -10px -10px 0;
}
.panel > h2, .panelTitle {
margin: -10px -10px 0 -10px;
padding: 5px 10px 5px 10px;
background-color: var(--border-color);
border-radius: 7px 7px 0 0;
font-weight: 400;
}
.panel > select {
width: calc(100% + 20px);
margin: 0 -10px 10px -10px;
}
.panel > .selection {
display: flex;
margin: 0 -10px 0 -10px;
}
.panel > .selection input {
display: none;
}
.panel > .selection label {
flex: 1;
padding: 5px;
cursor: pointer;
background-color: var( --surface-color);
font-weight: normal;
text-align: center;
}
.panel > .selection label ~ label {
border-left: 2px var(--border-color) solid;
}
.panel > .selection label:hover {
background-color: var(--primary-color);
}
.panel > .selection [type=radio]:checked + label {
background-color: var(--border-color);
}
.panel > .selection select {
flex: 1;
width: 50%;
}
button {
cursor: pointer;
}
input,
select,
button {
background-color: var(--surface-color);
color: var(--on-surface-color);
border: 2px solid rgba(var(--border-color-rgb), 0.4);
border-radius: 5px;
padding: 2px;
}
input:hover,
select:hover,
button:hover {
border: 2px solid rgba(var(--border-color-rgb), 0.6);
}
.colorbox {
display: inline-block;
width: 8px;
height: 8px;
border: 2px var(--background-color) solid;
border-radius: 50%;
}
.primary {
background-color: var(--default-color);
}
.red {
background-color: var(--red);
}
.green {
background-color: var(--green);
}
.yellow {
background-color: var(--yellow);
color: var(--map-background-color);
}
.blue {
background-color: var(--blue);
}
.orange {
background-color: var(--orange);
}
.violet {
background-color: var(--violet);
color: var(--map-background-color);
}
.success {
background-color: var(--secondary-color);
}
.failure {
background-color: var(--error-color);
}
.highlight {
background-color: var(--primary-color);
color: var(--on-primary-color);
}
button:hover,
.clickable:hover,
.mark:hover,
.clickable:active,
.mark:active {
border-radius: 3px;
background-color: var(--primary-color);
color: var(--on-primary-color);
cursor: pointer;
}
.legend {
position: absolute;
right: 0px;
top: 0px;
background-color: var(--surface-color);
border-radius: 5px;
border: 3px solid var(--border-color);
padding: 0 10px 0 10px;
}
.legend dt {
font-family: var(--code-font);
}
.legend h3 {
margin-top: 10px;
}
.panelCloserLabel {
float: left;
cursor: zoom-out;
margin: 0 5px 0 0;
transition: transform 0.2s ease-out;
user-select: none;
}
.panelCloserInput {
display: none;
}
.panel .panelCloserInput:checked ~ .panelCloserLabel,
.panelCloserInput:checked ~ .panelCloserLabel {
cursor: zoom-in;
transform: rotate(-90deg);
display: inherit;
}
.panel .panelCloserInput:checked ~ h2 {
display: inherit;
border-radius: 7px;
margin-bottom: -20px;
padding-bottom: 10px;
}
.panelCloserInput:checked ~ * {
display: none;
}

221
deps/v8/tools/system-analyzer/index.html vendored Normal file
View File

@ -0,0 +1,221 @@
<!DOCTYPE html>
<!-- Copyright 2020 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. -->
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Indicium</title>
<!-- <link rel="icon" type="image/png" href="/images/favicon.png"/> -->
<link rel="modulepreload" href="./helper.mjs" >
<link rel="modulepreload" href="./view/log-file-reader.mjs" >
<link rel="modulepreload" href="./view/helper.mjs" >
<link rel="preload" href="../js/log-file-reader-template.html" as="fetch" crossorigin="anonymous">
<script type="module">
// Force instantiating the log-reader before anything else.
import "./view/log-file-reader.mjs";
// Delay loading of the main App
(async function() {
let module = await import('./index.mjs');
globalThis.app = new module.App();
})();
</script>
<link rel="stylesheet" type="text/css" href="./index.css">
<style>
#container.initial {
display: none;
}
#timeline-panel {
width: 100%;
}
.panels {
display: grid;
align-content: center;
grid-template-columns: repeat(auto-fill, minmax(600px, 1fr));
grid-auto-flow: row dense;
grid-gap: 10px;
margin-top: 10px;
}
dt::after {
content: ":";
}
</style>
</head>
<body>
<tool-tip id="tool-tip" style="will-change: transform"></tool-tip>
<section id="file-reader">
<log-file-reader id="log-file-reader"></log-file-reader>
</section>
<section id="container" class="initial">
<timeline-panel id="timeline-panel">
<timeline-track-tick id="tick-track" title="Samples"></timeline-track-tick>
<timeline-track-timer id="timer-track" title="Timers"></timeline-track-timer>
<timeline-track-map id="map-track" title="Map"></timeline-track-map>
<timeline-track id="ic-track" title="IC"></timeline-track>
<timeline-track id="deopt-track" title="Deopt"></timeline-track>
<timeline-track id="code-track" title="Code"></timeline-track>
</timeline-panel>
<div class="panels">
<script-panel id="script-panel"></script-panel>
<code-panel id="code-panel"></code-panel>
<map-panel id="map-panel"></map-panel>
<list-panel id="ic-list" title="IC List">
<div class="legend">
<h3>Legend</h3>
<dl>
<dt>0</dt>
<dd>uninitialized</dd>
<dt>X</dt>
<dd>no feedback</dd>
<dt>1</dt>
<dd>monomorphic</dd>
<dt>^</dt>
<dd>recompute handler</dd>
<dt>P</dt>
<dd>polymorphic</dd>
<dt>N</dt>
<dd>megamorphic</dd>
<dt>G</dt>
<dd>generic</dd>
</dl>
</div>
</list-panel>
<list-panel id="map-list" title="Map Events"></list-panel>
<list-panel id="deopt-list" title="Deopt Events"></list-panel>
<list-panel id="code-list" title="Code Events"></list-panel>
<profiler-panel id="profiler-panel"></profiler-panel>
</div>
</section>
<div class="panels">
<section id="instructions" class="panel">
<h2>Instructions</h2>
<div class="panelBody">
<p>
Unified web interface to analyse runtime information stored in the v8 log.
</p>
For generating a v8.log file from <a href="https://v8.dev/docs/build">d8</a>:
<ul>
<li>
<code>/path/do/d8 $LOG_FLAGS $FILE</code>
</li>
</ul>
For generating a v8.log file from Chrome:
<ul>
<li>
<code>/path/to/chrome --user-data-dir=/var/tmp/chr$RANDOM --no-sandbox
--js-flags='$LOG_FLAGS’
$WEBSITE_URL</code>
</li>
</ul>
<h3><code>LOG_FLAGS</code>:</h3>
<dl class="d8-options">
<dt>
<a href="https://source.chromium.org/search?q=v8_flags.log_all">
<code>--log-all</code>
</a>
</dt>
<dd>Enable all V8 logging options.</dd>
<dt>
<a href="https://source.chromium.org/search?q=v8_flags.trace_maps">
<code>--log-maps</code>
</a>
</dt>
<dd>
Log <a href="https://v8.dev/blog/fast-properties">Maps</a>
</dd>
<dt>
<a href="https://source.chromium.org/search?q=v8_flags.log_ic">
<code>--log-ic</code>
</a>
</dt>
<dd>
Log <a href="https://mathiasbynens.be/notes/shapes-ics">ICs</a>
</dd>
<dt>
<a href="https://source.chromium.org/search?q=v8_flags.log_source_code">
<code>--log-source-code</code>
</a>
</dt>
<dd>Log source code</dd>
<dt>
<a href="https://source.chromium.org/search?q=v8_flags.log_code_disassemble">
<code>--log-code-disassemble</code>
</a>
</dt>
<dd>Log detailed generated generated code</dd>
<dt>
<a href="https://source.chromium.org/search?q=v8_flags.log_code">
<code>--log-code</code>
</a>
</dt>
<dd>Log details about deoptimized code</dd>
<dt>
<a href="https://source.chromium.org/search?q=v8_flags.log_deopt">
<code>--log-deopt</code>
</a>
</dt>
<dd>Log various API uses.</dd>
<dt>
<a href="https://source.chromium.org/search?q=v8_flags.prof">
<code>--prof</code>
</a>
</dt>
<dd>Log ticks from the sampling profiler.</dd>
</dl>
<h3>Local Symbol Server</h3>
To get detailed C++ symbols for local binaries you can host the
system-analyzer locally using <code>tools/system-analyzer/local-server.sh</code>.
You can then access the analyzer on
<a href="http://localhost:8000/system-analyzer/index.html">localhost</a>.
<h3>Keyboard Shortcuts for Navigation</h3>
<dl>
<dt><kbd>CTRL</kbd> + <kbd>Mouse Move</kbd></dt>
<dd>Show tooltips immediately</dd>
<dt><kbd>A</kbd></dt>
<dd>Scroll left</dd>
<dt><kbd>D</kbd></dt>
<dd>Scroll right</dd>
<dt><kbd>SHIFT</kbd> + <kbd>Arrow Up</kbd></dt>
<dd>Follow Map transition forward (first child)</dd>
<dt><kbd>SHIFT</kbd> + <kbd>Arrow Down</kbd></dt>
<dd>Follow Map transition backwards</dd>
<dt><kbd>Arrow Up</kbd></dt>
<dd>Go to previous Map chunk</dd>
<dt><kbd>Arrow Down</kbd></dt>
<dd>Go to next Map in chunk</dd>
<dt><kbd>Arrow Left</kbd></dt>
<dd>Go to previous chunk</dd>
<dt><kbd>Arrow Right</kbd></dt>
<dd>Go to next chunk</dd>
<dt><kbd>+</kbd></dt>
<dd>Timeline zoom in</dd>
<dt><kbd>-</kbd></dt>
<dd>Timeline zoom out</dd>
</dl>
</div>
</section>
</div>
</body>
</html>

457
deps/v8/tools/system-analyzer/index.mjs vendored Normal file
View File

@ -0,0 +1,457 @@
// Copyright 2020 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 {Script, SourcePosition} from '../profile.mjs';
import {State} from './app-model.mjs';
import {CodeLogEntry, DeoptLogEntry, SharedLibLogEntry} from './log/code.mjs';
import {IcLogEntry} from './log/ic.mjs';
import {LogEntry} from './log/log.mjs';
import {MapLogEntry} from './log/map.mjs';
import {TickLogEntry} from './log/tick.mjs';
import {TimerLogEntry} from './log/timer.mjs';
import {Processor} from './processor.mjs';
import {FocusEvent, SelectionEvent, SelectRelatedEvent, SelectTimeEvent, ToolTipEvent,} from './view/events.mjs';
import {$, groupBy} from './view/helper.mjs';
class App {
_state;
_view;
_navigation;
_startupPromise;
constructor() {
this._view = {
__proto__: null,
logFileReader: $('#log-file-reader'),
timelinePanel: $('#timeline-panel'),
tickTrack: $('#tick-track'),
mapTrack: $('#map-track'),
icTrack: $('#ic-track'),
deoptTrack: $('#deopt-track'),
codeTrack: $('#code-track'),
timerTrack: $('#timer-track'),
icList: $('#ic-list'),
mapList: $('#map-list'),
codeList: $('#code-list'),
deoptList: $('#deopt-list'),
mapPanel: $('#map-panel'),
codePanel: $('#code-panel'),
profilerPanel: $('#profiler-panel'),
scriptPanel: $('#script-panel'),
toolTip: $('#tool-tip'),
};
this._view.logFileReader.addEventListener(
'fileuploadstart', this.handleFileUploadStart.bind(this));
this._view.logFileReader.addEventListener(
'fileuploadchunk', this.handleFileUploadChunk.bind(this));
this._view.logFileReader.addEventListener(
'fileuploadend', this.handleFileUploadEnd.bind(this));
this._startupPromise = this._loadCustomElements();
this._view.codeTrack.svg = true;
}
static get allEventTypes() {
return new Set([
SourcePosition,
MapLogEntry,
IcLogEntry,
CodeLogEntry,
DeoptLogEntry,
SharedLibLogEntry,
TickLogEntry,
TimerLogEntry,
]);
}
async _loadCustomElements() {
await Promise.all([
import('./view/list-panel.mjs'),
import('./view/timeline-panel.mjs'),
import('./view/timeline/timeline-overview.mjs'),
import('./view/map-panel.mjs'),
import('./view/script-panel.mjs'),
import('./view/code-panel.mjs'),
import('./view/property-link-table.mjs'),
import('./view/tool-tip.mjs'),
import('./view/profiler-panel.mjs'),
]);
this._addEventListeners();
}
_addEventListeners() {
document.addEventListener(
'keydown', e => this._navigation?.handleKeyDown(e));
document.addEventListener(
SelectRelatedEvent.name, this.handleSelectRelatedEntries.bind(this));
document.addEventListener(
SelectionEvent.name, this.handleSelectEntries.bind(this))
document.addEventListener(
FocusEvent.name, this.handleFocusLogEntry.bind(this));
document.addEventListener(
SelectTimeEvent.name, this.handleTimeRangeSelect.bind(this));
document.addEventListener(ToolTipEvent.name, this.handleToolTip.bind(this));
}
handleSelectRelatedEntries(e) {
e.stopImmediatePropagation();
this.selectRelatedEntries(e.entry);
}
selectRelatedEntries(entry) {
let entries = [entry];
switch (entry.constructor) {
case SourcePosition:
entries = entries.concat(entry.entries);
break;
case MapLogEntry:
entries = this._state.icTimeline.filter(each => each.map === entry);
break;
case IcLogEntry:
if (entry.map) entries.push(entry.map);
break;
case DeoptLogEntry:
// TODO select map + code entries
if (entry.fileSourcePosition) entries.push(entry.fileSourcePosition);
break;
case Script:
entries = entry.entries.concat(entry.sourcePositions);
break;
case TimerLogEntry:
case CodeLogEntry:
case TickLogEntry:
case SharedLibLogEntry:
break;
default:
throw new Error(`Unknown selection type: ${entry.constructor?.name}`);
}
if (entry.sourcePosition) {
entries.push(entry.sourcePosition);
// TODO: find the matching Code log entries.
}
this.selectEntries(entries);
}
static isClickable(object) {
if (typeof object !== 'object') return false;
if (object instanceof LogEntry) return true;
if (object instanceof SourcePosition) return true;
if (object instanceof Script) return true;
return false;
}
handleSelectEntries(e) {
e.stopImmediatePropagation();
this.selectEntries(e.entries);
}
selectEntries(entries) {
const missingTypes = App.allEventTypes;
groupBy(entries, each => each.constructor, true).forEach(group => {
this.selectEntriesOfSingleType(group.entries);
missingTypes.delete(group.key);
});
missingTypes.forEach(
type => this.selectEntriesOfSingleType([], type, false));
}
selectEntriesOfSingleType(entries, type, focusView = true) {
const entryType = entries[0]?.constructor ?? type;
switch (entryType) {
case Script:
entries = entries.flatMap(script => script.sourcePositions);
return this.showSourcePositions(entries, focusView);
case SourcePosition:
return this.showSourcePositions(entries, focusView);
case MapLogEntry:
return this.showMapEntries(entries, focusView);
case IcLogEntry:
return this.showIcEntries(entries, focusView);
case CodeLogEntry:
return this.showCodeEntries(entries, focusView);
case DeoptLogEntry:
return this.showDeoptEntries(entries, focusView);
case SharedLibLogEntry:
return this.showSharedLibEntries(entries, focusView);
case TimerLogEntry:
case TickLogEntry:
break;
default:
throw new Error(`Unknown selection type: ${entryType?.name}`);
}
}
showMapEntries(entries, focusView = true) {
this._view.mapPanel.selectedLogEntries = entries;
this._view.mapList.selectedLogEntries = entries;
if (focusView) this._view.mapPanel.show();
}
showIcEntries(entries, focusView = true) {
this._view.icList.selectedLogEntries = entries;
if (focusView) this._view.icList.show();
}
showDeoptEntries(entries, focusView = true) {
this._view.deoptList.selectedLogEntries = entries;
if (focusView) this._view.deoptList.show();
}
showSharedLibEntries(entries, focusView = true) {}
showCodeEntries(entries, focusView = true) {
this._view.codePanel.selectedEntries = entries;
this._view.codeList.selectedLogEntries = entries;
if (focusView) this._view.codePanel.show();
}
showTickEntries(entries, focusView = true) {
this._view.profilerPanel.selectedLogEntries = entries;
if (focusView) this._view.profilerPanel.show();
}
showTimerEntries(entries, focusView = true) {}
showSourcePositions(entries, focusView = true) {
this._view.scriptPanel.selectedSourcePositions = entries
if (focusView) this._view.scriptPanel.show();
}
handleTimeRangeSelect(e) {
e.stopImmediatePropagation();
this.selectTimeRange(e.start, e.end, e.focus, e.zoom);
}
selectTimeRange(start, end, focus = false, zoom = false) {
this._state.selectTimeRange(start, end);
this.showMapEntries(this._state.mapTimeline.selectionOrSelf, false);
this.showIcEntries(this._state.icTimeline.selectionOrSelf, false);
this.showDeoptEntries(this._state.deoptTimeline.selectionOrSelf, false);
this.showCodeEntries(this._state.codeTimeline.selectionOrSelf, false);
this.showTickEntries(this._state.tickTimeline.selectionOrSelf, false);
this.showTimerEntries(this._state.timerTimeline.selectionOrSelf, false);
this._view.timelinePanel.timeSelection = {start, end, focus, zoom};
}
handleFocusLogEntry(e) {
e.stopImmediatePropagation();
this.focusLogEntry(e.entry);
}
focusLogEntry(entry) {
switch (entry.constructor) {
case Script:
return this.focusSourcePosition(entry.sourcePositions[0]);
case SourcePosition:
return this.focusSourcePosition(entry);
case MapLogEntry:
return this.focusMapLogEntry(entry);
case IcLogEntry:
return this.focusIcLogEntry(entry);
case CodeLogEntry:
return this.focusCodeLogEntry(entry);
case DeoptLogEntry:
return this.focusDeoptLogEntry(entry);
case SharedLibLogEntry:
return this.focusDeoptLogEntry(entry);
case TickLogEntry:
return this.focusTickLogEntry(entry);
case TimerLogEntry:
return this.focusTimerLogEntry(entry);
default:
throw new Error(`Unknown selection type: ${entry.constructor?.name}`);
}
}
focusMapLogEntry(entry, focusSourcePosition = true) {
this._state.map = entry;
this._view.mapTrack.focusedEntry = entry;
this._view.mapPanel.map = entry;
if (focusSourcePosition) {
this.focusCodeLogEntry(entry.code, false);
this.focusSourcePosition(entry.sourcePosition);
}
this._view.mapPanel.show();
}
focusIcLogEntry(entry) {
this._state.ic = entry;
this.focusMapLogEntry(entry.map, false);
this.focusCodeLogEntry(entry.code, false);
this.focusSourcePosition(entry.sourcePosition);
}
focusCodeLogEntry(entry, focusSourcePosition = true) {
this._state.code = entry;
this._view.codePanel.entry = entry;
if (focusSourcePosition) this.focusSourcePosition(entry.sourcePosition);
this._view.codePanel.show();
}
focusDeoptLogEntry(entry) {
this._state.deoptLogEntry = entry;
this.focusCodeLogEntry(entry.code, false);
this.focusSourcePosition(entry.sourcePosition);
}
focusSharedLibLogEntry(entry) {
// no-op.
}
focusTickLogEntry(entry) {
this._state.tickLogEntry = entry;
this._view.tickTrack.focusedEntry = entry;
}
focusTimerLogEntry(entry) {
this._state.timerLogEntry = entry;
this._view.timerTrack.focusedEntry = entry;
}
focusSourcePosition(sourcePosition) {
if (!sourcePosition) return;
this._view.scriptPanel.focusedSourcePositions = [sourcePosition];
this._view.scriptPanel.show();
}
handleToolTip(event) {
let content = event.content;
if (typeof content !== 'string' &&
!(content?.nodeType && content?.nodeName)) {
content = content?.toolTipDict;
}
if (!content) {
throw new Error(
`Unknown tooltip content type: ${content.constructor?.name}`);
}
this._view.toolTip.data = {
content: content,
positionOrTargetNode: event.positionOrTargetNode,
immediate: event.immediate,
};
}
restartApp() {
this._state = new State();
this._navigation = new Navigation(this._state, this._view);
}
handleFileUploadStart(e) {
this.restartApp();
$('#container').className = 'initial';
this._processor = new Processor();
this._processor.setProgressCallback(
e.detail.totalSize, e.detail.progressCallback);
}
async handleFileUploadChunk(e) {
this._processor.processChunk(e.detail);
}
async handleFileUploadEnd(e) {
try {
const processor = this._processor;
await processor.finalize();
await this._startupPromise;
this._state.profile = processor.profile;
const mapTimeline = processor.mapTimeline;
const icTimeline = processor.icTimeline;
const deoptTimeline = processor.deoptTimeline;
const codeTimeline = processor.codeTimeline;
const tickTimeline = processor.tickTimeline;
const timerTimeline = processor.timerTimeline;
this._state.setTimelines(
mapTimeline, icTimeline, deoptTimeline, codeTimeline, tickTimeline,
timerTimeline);
this._view.mapPanel.timeline = mapTimeline;
this._view.icList.timeline = icTimeline;
this._view.mapList.timeline = mapTimeline;
this._view.deoptList.timeline = deoptTimeline;
this._view.codeList.timeline = codeTimeline;
this._view.scriptPanel.scripts = processor.scripts;
this._view.codePanel.timeline = codeTimeline;
this._view.codePanel.timeline = codeTimeline;
this._view.profilerPanel.timeline = tickTimeline;
this.refreshTimelineTrackView();
} catch (e) {
this._view.logFileReader.error = 'Log file contains errors!'
throw (e);
} finally {
$('#container').className = 'loaded';
this.fileLoaded = true;
}
}
refreshTimelineTrackView() {
this._view.mapTrack.data = this._state.mapTimeline;
this._view.icTrack.data = this._state.icTimeline;
this._view.deoptTrack.data = this._state.deoptTimeline;
this._view.codeTrack.data = this._state.codeTimeline;
this._view.tickTrack.data = this._state.tickTimeline;
this._view.timerTrack.data = this._state.timerTimeline;
}
}
class Navigation {
_view;
constructor(state, view) {
this.state = state;
this._view = view;
}
get map() {
return this.state.map
}
set map(value) {
this.state.map = value
}
get chunks() {
return this.state.mapTimeline.chunks;
}
increaseTimelineResolution() {
this._view.timelinePanel.nofChunks *= 1.5;
this.state.nofChunks *= 1.5;
}
decreaseTimelineResolution() {
this._view.timelinePanel.nofChunks /= 1.5;
this.state.nofChunks /= 1.5;
}
updateUrl() {
let entries = this.state.entries;
let params = new URLSearchParams(entries);
window.history.pushState(entries, '', '?' + params.toString());
}
scrollLeft() {}
scrollRight() {}
handleKeyDown(event) {
switch (event.key) {
case 'd':
this.scrollLeft();
return false;
case 'a':
this.scrollRight();
return false;
case '+':
this.increaseTimelineResolution();
return false;
case '-':
this.decreaseTimelineResolution();
return false;
}
}
}
export {App};

68
deps/v8/tools/system-analyzer/local-server.sh vendored Executable file
View File

@ -0,0 +1,68 @@
#!/bin/bash
# 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.
for i in "$@"; do
case $i in
-h|--help)
echo "Starts a local server for V8's system anaylizer"
echo "It's accessible http://localhost:8000"
echo "Note: The server also exposes local binary information via 'nm'"
exit;
;;
*)
echo "Invalid option: $i"
exit 1;
;;
esac
done
if ! npm list local-web-server > /dev/null; then
echo "'ws' not found!"
echo "Please install local-web-server:"
echo "npm install local-web-server"
echo ""
exit
fi
TOOLS_DIR=`readlink "$0"` || TOOLS_DIR="$0";
TOOLS_DIR=`dirname "$TOOLS_DIR"`;
cd "$TOOLS_DIR/.."
TOOLS_DIR=`pwd -P`
# Check if port 8000 is in use
if [[ "$OSTYPE" == "msys" ]]; then
if netstat -ano | findstr :8000 > /dev/null; then
echo "localhost:8000 is already in use. You can kill it with:"
echo "netstat -aon | findstr :8000"
echo "taskkill /PID <PID>"
exit 1
fi
else
if lsof -t -i TCP:8000; then
echo "localhost:8000 is already in use. You can kill it with:"
echo "lsof -t -i TCP:8000 | xargs kill"
exit 1
fi
fi
echo "Starting local symbol server"
npm run start-webserver -- \
--stack $TOOLS_DIR/system-analyzer/lws-middleware.js \
lws-static \
lws-cors \
lws-index & PID=$!
# Kill server after 1h
for i in `seq 3600`; do
if ps -p $PID > /dev/null 2>&1; then
sleep 1;
fi
done
if ps -p $PID > /dev/null 2>&1; then
echo "Automatically killing the local server after timeout"
kill $PID
fi

View File

@ -0,0 +1,281 @@
// Copyright 2020 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 {formatBytes} from '../helper.mjs';
import {LogEntry} from './log.mjs';
class CodeString {
constructor(string) {
if (typeof string !== 'string') {
throw new Error('Expected string');
}
this.string = string;
}
get isCode() {
return true;
}
toString() {
return this.string;
}
}
export class DeoptLogEntry extends LogEntry {
constructor(
type, time, entry, deoptReason, deoptLocation, scriptOffset,
instructionStart, codeSize, inliningId) {
super(type, time);
/** @type {CodeEntry} */
this._entry = entry;
/** @type {string} */
this._reason = deoptReason;
/** @type {SourcePosition} */
this._location = deoptLocation;
/** @type {number} */
this._scriptOffset = scriptOffset;
/** @type {number} */
this._instructionStart = instructionStart;
/** @type {number} */
this._codeSize = codeSize;
/** @type {string} */
this._inliningId = inliningId;
this.fileSourcePosition = undefined;
}
get reason() {
return this._reason;
}
get location() {
return this._location;
}
get entry() {
return this._entry;
}
get code() {
return this._entry?.logEntry;
}
get functionName() {
return this._entry.functionName;
}
static get propertyNames() {
return [
'type', 'reason', 'functionName', 'sourcePosition',
'functionSourcePosition', 'script', 'code'
];
}
}
class CodeLikeLogEntry extends LogEntry {
constructor(type, time, profilerEntry) {
super(type, time);
/** @type {CodeEntry} */
this._entry = profilerEntry;
profilerEntry.logEntry = this;
/** @type {LogEntry[]} */
this._relatedEntries = [];
}
get entry() {
return this._entry;
}
add(entry) {
this._relatedEntries.push(entry);
}
relatedEntries() {
return this._relatedEntries;
}
}
export class CodeLogEntry extends CodeLikeLogEntry {
constructor(type, time, kindName, kind, name, profilerEntry) {
super(type, time, profilerEntry);
this._kind = kind;
/** @type {string} */
this._kindName = kindName;
/** @type {?FeedbackVectorEntry} */
this._feedbackVector = undefined;
/** @type {string} */
this._name = name;
}
get name() {
return this._name;
}
get kind() {
return this._kind;
}
get isBuiltinKind() {
return this._kindName === 'Builtin';
}
get isBytecodeKind() {
return this._kindName === 'Unopt';
}
get isScript() {
return this._type.startsWith('Script');
}
get kindName() {
return this._kindName;
}
get functionName() {
return this._entry.functionName ?? this._entry.getRawName();
}
get shortName() {
if (this.isScript) {
let url = this.sourcePosition?.script?.name ?? '';
return url.substring(url.lastIndexOf('/') + 1);
}
return this.functionName;
}
get size() {
return this._entry.size;
}
get source() {
return this._entry?.getSourceCode() ?? '';
}
get code() {
return this._entry?.source?.disassemble;
}
get variants() {
const entries = Array.from(this.entry?.sfi?.codeEntries ?? []);
return entries.map(each => each.logEntry);
}
get feedbackVector() {
return this._feedbackVector;
}
setFeedbackVector(fbv) {
if (this._feedbackVector) {
throw new Error('Double setting FeedbackVector');
}
this._feedbackVector = fbv;
}
toString() {
return `Code(${this.type})`;
}
get toolTipDict() {
const dict = super.toolTipDict;
dict.size = formatBytes(dict.size);
dict.source = new CodeString(dict.source);
if (dict.code) dict.code = new CodeString(dict.code);
return dict;
}
static get propertyNames() {
return [
'functionName', 'sourcePosition', 'kindName', 'size', 'type', 'kind',
'script', 'source', 'code', 'feedbackVector', 'variants'
];
}
}
export class FeedbackVectorEntry extends LogEntry {
constructor(
timestamp, codeEntry, fbvAddress, length, optimizationMarker,
optimizationTier, invocationCount, profilerTicks, string) {
super('FeedbackVector', timestamp);
this._length = length;
this._code = codeEntry;
this._string = string;
this._optimizationMarker = optimizationMarker;
this._optimizationTier = optimizationTier;
this._invocationCount = invocationCount;
this._profilerTicks = profilerTicks;
}
toString() {
return `FeedbackVector(l=${this.length})`
}
get length() {
return this._length;
}
get code() {
return this._code;
}
get string() {
return this._string;
}
get optimizationMarker() {
return this._optimizationMarker;
}
get optimizationTier() {
return this._optimizationTier;
}
get invocationCount() {
return this._invocationCount;
}
get profilerTicks() {
return this._profilerTicks;
}
static get propertyNames() {
return [
'length', 'length', 'code', 'optimizationMarker', 'optimizationTier',
'invocationCount', 'profilerTicks', 'string'
];
}
}
export class BaseCPPLogEntry extends CodeLikeLogEntry {
constructor(prefix, profilerEntry) {
super(prefix, 0, profilerEntry);
}
get name() {
return this._entry.name;
}
get shortName() {
let name = this.name;
return name.substring(name.lastIndexOf('/') + 1);
}
toString() {
return `SharedLib`;
}
static get propertyNames() {
return ['name'];
}
}
export class CPPCodeLogEntry extends BaseCPPLogEntry {
constructor(profilerEntry) {
super('CPP', profilerEntry);
}
}
export class SharedLibLogEntry extends BaseCPPLogEntry {
constructor(profilerEntry) {
super('SHARED_LIB', profilerEntry);
}
}

View File

@ -0,0 +1,69 @@
// Copyright 2020 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 {LogEntry} from './log.mjs';
export class IcLogEntry extends LogEntry {
constructor(
type, fn_file, time, line, column, key, oldState, newState, map, reason,
modifier, codeEntry) {
super(type, time);
this.category = 'other';
if (this.type.indexOf('Store') !== -1) {
this.category = 'Store';
} else if (this.type.indexOf('Load') !== -1) {
this.category = 'Load';
}
const parts = fn_file.split(' ');
this.functionName = parts[0];
this.file = parts[1];
this.oldState = oldState;
this.newState = newState;
this.key = key;
this.map = map;
this.reason = reason;
this.modifier = modifier;
this.codeEntry = codeEntry;
}
get state() {
return this.oldState + ' → ' + this.newState;
}
get code() {
return this.codeEntry?.logEntry;
}
parseMapProperties(parts, offset) {
let next = parts[++offset];
if (!next.startsWith('dict')) return offset;
this.propertiesMode = next.substr(5) == '0' ? 'fast' : 'slow';
this.numberOfOwnProperties = parts[++offset].substr(4);
next = parts[++offset];
this.instanceType = next.substr(5, next.length - 6);
return offset;
}
parsePositionAndFile(parts, start) {
// find the position of 'at' in the parts array.
let offset = start;
for (let i = start + 1; i < parts.length; i++) {
offset++;
if (parts[i] == 'at') break;
}
if (parts[offset] !== 'at') return -1;
this.position = parts.slice(start, offset).join(' ');
offset += 1;
this.isNative = parts[offset] == 'native'
offset += this.isNative ? 1 : 0;
this.file = parts[offset];
return offset;
}
static get propertyNames() {
return [
'type', 'category', 'functionName', 'script', 'sourcePosition', 'code',
'state', 'key', 'map', 'reason'
];
}
}

View File

@ -0,0 +1,59 @@
// Copyright 2020 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.
export class LogEntry {
constructor(type, time) {
/** @type {number} */
this._time = time;
this._type = type;
/** @type {?SourcePosition} */
this.sourcePosition = undefined;
}
get time() {
return this._time;
}
get type() {
return this._type;
}
get script() {
return this.sourcePosition?.script;
}
toString() {
let name = this.constructor.name;
const index = name.lastIndexOf('LogEntry');
if (index > 0) {
name = name.substr(0, index);
}
return `${name}(${this._type})`;
}
get toolTipDict() {
const toolTipDescription = {
__proto__: null,
__this__: this,
title: this.toString()
};
for (let key of this.constructor.propertyNames) {
toolTipDescription[key] = this[key];
}
return toolTipDescription;
}
// Returns an Array of all possible #type values.
/** @return {string[]} */
static get allTypes() {
throw new Error('Not implemented.');
}
// Returns an array of public property names.
/** @return {string[]} */
static get propertyNames() {
throw new Error('Not implemented.');
}
}

View File

@ -0,0 +1,329 @@
// Copyright 2020 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 {LogEntry} from './log.mjs';
// ===========================================================================
// Map Log Events
export const kChunkHeight = 200;
export const kChunkWidth = 10;
export const kChunkVisualWidth = 6;
function define(prototype, name, fn) {
Object.defineProperty(prototype, name, {value: fn, enumerable: false});
}
define(Array.prototype, 'max', function(fn) {
if (this.length === 0) return undefined;
if (fn === undefined) fn = (each) => each;
let max = fn(this[0]);
for (let i = 1; i < this.length; i++) {
max = Math.max(max, fn(this[i]));
}
return max;
})
define(Array.prototype, 'first', function() {
return this[0]
});
define(Array.prototype, 'last', function() {
return this[this.length - 1]
});
// ===========================================================================
// Map Log Events
export class MapLogEntry extends LogEntry {
constructor(id, time) {
if (!time) throw new Error('Invalid time');
// Use MapLogEntry.type getter instead of property, since we only know the
// type lazily from the incoming transition.
super(undefined, time);
this.id = id;
MapLogEntry.set(id, this);
this.edge = undefined;
this.children = [];
this.depth = 0;
this._isDeprecated = false;
this.deprecatedTargets = null;
this.leftId = 0;
this.rightId = 0;
this.entry = undefined;
this.description = '';
}
get functionName() {
return this.entry?.functionName;
}
get code() {
return this.entry?.logEntry;
}
toString() {
return `Map(${this.id})`;
}
finalizeRootMap(id) {
let stack = [this];
while (stack.length > 0) {
let current = stack.pop();
if (current.leftId !== 0) {
console.warn('Skipping potential parent loop between maps:', current)
continue;
}
current.finalize(id);
id += 1;
current.children.forEach(edge => stack.push(edge.to));
// TODO implement rightId
}
return id;
}
finalize(id) {
// Initialize preorder tree traversal Ids for fast subtree inclusion checks
if (id <= 0) throw 'invalid id';
let currentId = id;
this.leftId = currentId
}
get parent() {
return this.edge?.from;
}
isDeprecated() {
return this._isDeprecated;
}
deprecate() {
this._isDeprecated = true;
}
isRoot() {
return this.edge === undefined || this.edge.from === undefined;
}
contains(map) {
return this.leftId < map.leftId && map.rightId < this.rightId;
}
addEdge(edge) {
this.children.push(edge);
}
chunkIndex(chunks) {
// Did anybody say O(n)?
for (let i = 0; i < chunks.length; i++) {
let chunk = chunks[i];
if (chunk.isEmpty()) continue;
if (chunk.last().time < this.time) continue;
return i;
}
return -1;
}
position(chunks) {
const index = this.chunkIndex(chunks);
if (index === -1) return [0, 0];
const xFrom = (index * kChunkWidth + kChunkVisualWidth / 2) | 0;
const yFrom = kChunkHeight - chunks[index].yOffset(this) | 0;
return [xFrom, yFrom];
}
transitions() {
let transitions = Object.create(null);
let current = this;
while (current) {
let edge = current.edge;
if (edge && edge.isTransition()) {
transitions[edge.name] = edge;
}
current = current.parent;
}
return transitions;
}
get type() {
return this.edge?.type ?? 'new';
}
get reason() {
return this.edge?.reason;
}
get property() {
return this.edge?.name;
}
isBootstrapped() {
return this.edge === undefined;
}
getParents() {
let parents = [];
let current = this.parent;
while (current) {
parents.push(current);
current = current.parent;
}
return parents;
}
static get(id, time = undefined) {
let maps = this.cache.get(id);
if (maps === undefined) return undefined;
if (time !== undefined) {
for (let i = 1; i < maps.length; i++) {
if (maps[i].time > time) {
return maps[i - 1];
}
}
}
// default return the latest
return maps[maps.length - 1];
}
static set(id, map) {
if (this.cache.has(id)) {
this.cache.get(id).push(map);
} else {
this.cache.set(id, [map]);
}
}
static get propertyNames() {
return [
'type', 'reason', 'property', 'parent', 'functionName', 'sourcePosition',
'script', 'code', 'id', 'description'
];
}
}
MapLogEntry.cache = new Map();
// ===========================================================================
export class Edge {
constructor(type, name, reason, time, from, to) {
this.type = type;
this.name = name;
this.reason = reason;
this.time = time;
this.from = from;
this.to = to;
}
updateFrom(edge) {
if (this.to !== edge.to || this.from !== edge.from) {
throw new Error('Invalid Edge updated', this, to);
}
this.type = edge.type;
this.name = edge.name;
this.reason = edge.reason;
this.time = edge.time;
}
finishSetup() {
const from = this.from;
const to = this.to;
if (to?.time < from?.time) {
// This happens for map deprecation where the transition tree is converted
// in reverse order.
console.warn('Invalid time order');
}
if (from) from.addEdge(this);
if (to === undefined) return;
to.edge = this;
if (from === undefined) return;
if (to === from) throw 'From and to must be distinct.';
let newDepth = from.depth + 1;
if (to.depth > 0 && to.depth != newDepth) {
console.warn('Depth has already been initialized');
}
to.depth = newDepth;
}
chunkIndex(chunks) {
// Did anybody say O(n)?
for (let i = 0; i < chunks.length; i++) {
let chunk = chunks[i];
if (chunk.isEmpty()) continue;
if (chunk.last().time < this.time) continue;
return i;
}
return -1;
}
parentEdge() {
if (!this.from) return undefined;
return this.from.edge;
}
chainLength() {
let length = 0;
let prev = this;
while (prev) {
prev = this.parent;
length++;
}
return length;
}
isTransition() {
return this.type === 'Transition'
}
isFastToSlow() {
return this.type === 'Normalize'
}
isSlowToFast() {
return this.type === 'SlowToFast'
}
isInitial() {
return this.type === 'InitialMap'
}
isBootstrapped() {
return this.type === 'new'
}
isReplaceDescriptors() {
return this.type === 'ReplaceDescriptors'
}
isCopyAsPrototype() {
return this.reason === 'CopyAsPrototype'
}
isOptimizeAsPrototype() {
return this.reason === 'OptimizeAsPrototype'
}
symbol() {
if (this.isTransition()) return '+';
if (this.isFastToSlow()) return '⊡';
if (this.isSlowToFast()) return '⊛';
if (this.isReplaceDescriptors()) {
if (this.name) return '+';
return '∥';
}
return '';
}
toString() {
let s = this.symbol();
if (this.isTransition()) return s + this.name;
if (this.isFastToSlow()) return s + this.reason;
if (this.isCopyAsPrototype()) return s + 'Copy as Prototype';
if (this.isOptimizeAsPrototype()) {
return s + 'Optimize as Prototype';
}
if (this.isReplaceDescriptors() && this.name) {
return this.type + ' ' + this.symbol() + this.name;
}
return this.type + ' ' + (this.reason ? this.reason : '') + ' ' +
(this.name ? this.name : '')
}
}

View File

@ -0,0 +1,62 @@
// Copyright 2021 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 {Profile} from '../../profile.mjs'
import {LogEntry} from './log.mjs';
export class TickLogEntry extends LogEntry {
constructor(time, vmState, processedStack) {
super(TickLogEntry.extractType(vmState, processedStack), time);
/** @type {string} */
this.state = vmState;
/** @type {CodeEntry[]} */
this.stack = processedStack;
/** @type {number} */
this._endTime = time;
}
end(time) {
if (this.isInitialized) throw new Error('Invalid timer change');
this._endTime = time;
}
get isInitialized() {
return this._endTime !== this._time;
}
get startTime() {
return this._time;
}
get endTime() {
return this._endTime;
}
get duration() {
return this._endTime - this._time;
}
static extractType(vmState, processedStack) {
if (processedStack.length == 0 || vmState == Profile.VMState.IDLE) {
return 'Idle';
}
const topOfStack = processedStack[0];
if (typeof topOfStack === 'number' || typeof topOfStack === 'bigint') {
// TODO(cbruni): Handle VmStack and native ticks better.
return 'Other';
}
if (vmState != Profile.VMState.JS) {
topOfStack.vmState = vmState;
}
return this.extractCodeEntryType(topOfStack);
}
static extractCodeEntryType(entry) {
if (entry?.state !== undefined) {
return 'JS ' + Profile.getKindFromState(entry.state);
}
if (entry?.vmState) return Profile.vmStateString(entry.vmState);
return 'Other';
}
}

View File

@ -0,0 +1,56 @@
// Copyright 2021 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 {formatDurationMicros} from '../helper.mjs';
import {LogEntry} from './log.mjs';
export class TimerLogEntry extends LogEntry {
constructor(type, startTime, endTime = -1) {
super(type, startTime);
this._endTime = endTime;
this.depth = 0;
}
end(time) {
if (this.isInitialized) throw new Error('Invalid timer change');
this._endTime = time;
}
get isInitialized() {
return this._endTime !== -1;
}
get startTime() {
return this._time;
}
get endTime() {
return this._endTime;
}
get duration() {
return Math.max(0, this._endTime - this._time);
}
covers(time) {
return this._time <= time && time <= this._endTime;
}
get toolTipDict() {
const dict = super.toolTipDict;
dict.startTime = formatDurationMicros(dict.startTime);
dict.endTime = formatDurationMicros(dict.endTime);
dict.duration = formatDurationMicros(dict.duration);
return dict;
}
static get propertyNames() {
return [
'type',
'startTime',
'endTime',
'duration',
];
}
}

View File

@ -0,0 +1,123 @@
// Copyright 2021 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.
const util = require('util');
const execFile = util.promisify(require('child_process').execFile);
const fs = require('fs')
async function sh(cmd, ...params) {
console.log(cmd, params.join(' '));
const options = {maxBuffer: 256 * 1024 * 1024};
const {stdout} = await execFile(cmd, params, options);
return stdout;
}
class Symbolizer {
constructor() {
this.nmExec = 'nm';
this.objdumpExec = 'objdump';
}
optionDefinitions() {
return [
{
name: 'apk-embedded-library',
type: String,
description:
'Specify the path of the embedded library for Android traces',
},
{
name: 'target',
type: String,
description: 'Specify the target root directory for cross environment',
}
]
}
middleware(config) {
return async (ctx, next) => {
if (ctx.path == '/v8/loadVMSymbols') {
await this.parseVMSymbols(ctx)
} else if (ctx.path == '/v8/info/platform') {
ctx.response.type = 'json';
ctx.response.body = JSON.stringify({
'name': process.platform,
'nmExec': this.nmExec,
'objdumpExec': this.objdumpExec,
'targetRootFS': config.target,
'apkEmbeddedLibrary': config.apkEmbeddedLibrary
});
}
await next();
}
}
async parseVMSymbols(ctx) {
const query = ctx.request.query;
const result = {
libName: query.libName,
symbols: ['', ''],
error: undefined,
fileOffsetMinusVma: 0,
};
switch (query.platform) {
case 'macos':
await this.loadVMSymbolsMacOS(query, result);
break;
case 'linux':
await this.loadVMSymbolsLinux(query, result);
break;
default:
ctx.response.status = '500';
return;
}
ctx.response.type = 'json';
ctx.response.body = JSON.stringify(result);
}
async loadVMSymbolsMacOS(query, result) {
let libName =
(query.targetRootFS ? query.targetRootFS : '') + query.libName;
try {
// Fast skip files that don't exist.
if (libName.indexOf('/') === -1 || !fs.existsSync(libName)) return;
result.symbols = [await sh(this.nmExec, '--demangle', '-n', libName), ''];
} catch (e) {
result.error = e.message;
}
}
async loadVMSymbolsLinux(query, result) {
let libName = query.libName;
if (query.apkEmbeddedLibrary && libName.endsWith('.apk')) {
libName = query.apkEmbeddedLibrary;
}
if (query.targetRootFS) {
libName = libName.substring(libName.lastIndexOf('/') + 1);
libName = query.targetRootFS + libName;
}
try {
// Fast skip files that don't exist.
if (libName.indexOf('/') === -1 || !fs.existsSync(libName)) return;
result.symbols = [
await sh(this.nmExec, '-C', '-n', '-S', libName),
await sh(this.nmExec, '-C', '-n', '-S', '-D', libName)
];
const objdumpOutput = await sh(this.objdumpExec, '-h', libName);
for (const line of objdumpOutput.split('\n')) {
const [, sectionName, , vma, , fileOffset] = line.trim().split(/\s+/);
if (sectionName === '.text') {
result.fileOffsetMinusVma =
parseInt(fileOffset, 16) - parseInt(vma, 16);
}
}
} catch (e) {
console.log(e);
result.error = e.message;
}
}
}
module.exports = Symbolizer

View File

@ -0,0 +1,674 @@
// Copyright 2020 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 {LogReader, parseString, parseVarArgs} from '../logreader.mjs';
import {Profile} from '../profile.mjs';
import {RemoteLinuxCppEntriesProvider, RemoteMacOSCppEntriesProvider} from '../tickprocessor.mjs'
import {CodeLogEntry, CPPCodeLogEntry, DeoptLogEntry, FeedbackVectorEntry, SharedLibLogEntry} from './log/code.mjs';
import {IcLogEntry} from './log/ic.mjs';
import {Edge, MapLogEntry} from './log/map.mjs';
import {TickLogEntry} from './log/tick.mjs';
import {TimerLogEntry} from './log/timer.mjs';
import {Timeline} from './timeline.mjs';
// ===========================================================================
class AsyncConsumer {
constructor(consumer_fn) {
this._chunks = [];
this._consumer = consumer_fn;
this._pendingWork = Promise.resolve();
this._isConsuming = false;
}
get pendingWork() {
return this._pendingWork;
}
push(chunk) {
this._chunks.push(chunk);
this.consumeAll();
}
async consumeAll() {
if (!this._isConsuming) this._pendingWork = this._consumeAll();
return await this._pendingWork;
}
async _consumeAll() {
this._isConsuming = true;
while (this._chunks.length > 0) {
await this._consumer(this._chunks.shift());
}
this._isConsuming = false;
}
}
export class Processor extends LogReader {
_codeTimeline = new Timeline();
_deoptTimeline = new Timeline();
_icTimeline = new Timeline();
_mapTimeline = new Timeline();
_tickTimeline = new Timeline();
_timerTimeline = new Timeline();
_formatPCRegexp = /(.*):[0-9]+:[0-9]+$/;
_lastTimestamp = 0;
_lastCodeLogEntry;
_lastTickLogEntry;
_cppEntriesProvider;
_chunkRemainder = '';
_lineNumber = 1;
_totalInputBytes = 0;
_processedInputChars = 0;
_progressCallback;
MAJOR_VERSION = 7;
MINOR_VERSION = 6;
constructor(useBigIntAddresses = false) {
super(false, false, useBigIntAddresses);
this.useBigIntAddresses = useBigIntAddresses;
this.kZero = useBigIntAddresses ? 0n : 0;
this.parseAddress = useBigIntAddresses ? BigInt : parseInt;
this._chunkConsumer =
new AsyncConsumer((chunk) => this._processChunk(chunk));
this._profile = new Profile(useBigIntAddresses);
const propertyICParser = [
this.parseAddress, parseInt, parseInt, parseInt, parseString, parseString,
parseString, parseString, parseString, parseString
];
this.setDispatchTable({
__proto__: null,
'v8-version': {
parsers: [
parseInt,
parseInt,
],
processor: this.processV8Version,
},
'shared-library': {
parsers: [
parseString, this.parseAddress, this.parseAddress, this.parseAddress
],
processor: this.processSharedLibrary.bind(this),
isAsync: true,
},
'code-creation': {
parsers: [
parseString, parseInt, parseInt, this.parseAddress, this.parseAddress,
parseString, parseVarArgs
],
processor: this.processCodeCreation
},
'code-deopt': {
parsers: [
parseInt, parseInt, this.parseAddress, parseInt, parseInt,
parseString, parseString, parseString
],
processor: this.processCodeDeopt
},
'code-move': {
parsers: [this.parseAddress, this.parseAddress],
processor: this.processCodeMove
},
'code-source-info': {
parsers: [
this.parseAddress, parseInt, parseInt, parseInt, parseString,
parseString, parseString
],
processor: this.processCodeSourceInfo
},
'code-disassemble': {
parsers: [this.parseAddress, parseString, parseString],
processor: this.processCodeDisassemble
},
'feedback-vector': {
parsers: [
parseInt, parseString, parseInt, this.parseAddress, parseString,
parseString, parseInt, parseInt, parseString
],
processor: this.processFeedbackVector
},
'script-source': {
parsers: [parseInt, parseString, parseString],
processor: this.processScriptSource
},
'sfi-move': {
parsers: [this.parseAddress, this.parseAddress],
processor: this.processSFIMove
},
'tick': {
parsers: [
this.parseAddress, parseInt, parseInt, this.parseAddress, parseInt,
parseVarArgs
],
processor: this.processTick
},
'active-runtime-timer': undefined,
'heap-sample-begin': undefined,
'heap-sample-end': undefined,
'timer-event-start': {
parsers: [parseString, parseInt],
processor: this.processTimerEventStart
},
'timer-event-end': {
parsers: [parseString, parseInt],
processor: this.processTimerEventEnd
},
'map-create':
{parsers: [parseInt, parseString], processor: this.processMapCreate},
'map': {
parsers: [
parseString, parseInt, parseString, parseString, this.parseAddress,
parseInt, parseInt, parseString, parseString
],
processor: this.processMap
},
'map-details': {
parsers: [parseInt, parseString, parseString],
processor: this.processMapDetails
},
'LoadGlobalIC': {
parsers: propertyICParser,
processor: this.processPropertyIC.bind(this, 'LoadGlobalIC')
},
'StoreGlobalIC': {
parsers: propertyICParser,
processor: this.processPropertyIC.bind(this, 'StoreGlobalIC')
},
'LoadIC': {
parsers: propertyICParser,
processor: this.processPropertyIC.bind(this, 'LoadIC')
},
'StoreIC': {
parsers: propertyICParser,
processor: this.processPropertyIC.bind(this, 'StoreIC')
},
'KeyedLoadIC': {
parsers: propertyICParser,
processor: this.processPropertyIC.bind(this, 'KeyedLoadIC')
},
'KeyedStoreIC': {
parsers: propertyICParser,
processor: this.processPropertyIC.bind(this, 'KeyedStoreIC')
},
'StoreInArrayLiteralIC': {
parsers: propertyICParser,
processor: this.processPropertyIC.bind(this, 'StoreInArrayLiteralIC')
},
'api': {
parsers: [parseString, parseVarArgs],
processor: this.processApiEvent
},
});
}
printError(str) {
console.error(str);
throw str
}
processChunk(chunk) {
this._chunkConsumer.push(chunk)
}
setProgressCallback(totalSize, callback) {
this._totalInputBytes = totalSize;
this._progressCallback = callback;
}
async _updateProgress() {
if (!this._progressCallback) return;
// We use chars and bytes interchangeably for simplicity. This causes us to
// slightly underestimate progress.
this._progressCallback(
this._processedInputChars / this._totalInputBytes,
this._processedInputChars);
}
async _processChunk(chunk) {
const prevProcessedInputChars = this._processedInputChars;
let end = chunk.length;
let current = 0;
let next = 0;
let line;
try {
while (current < end) {
next = chunk.indexOf('\n', current);
if (next === -1) {
this._chunkRemainder += chunk.substring(current);
break;
}
line = chunk.substring(current, next);
if (this._chunkRemainder) {
line = this._chunkRemainder + line;
this._chunkRemainder = '';
}
current = next + 1;
this._lineNumber++;
await this.processLogLine(line);
this._processedInputChars = prevProcessedInputChars + current;
}
this._updateProgress();
} catch (e) {
console.error(`Could not parse log line ${
this._lineNumber}, trying to continue: ${e}`);
}
}
async processLogFile(fileName) {
this.collectEntries = true;
this.lastLogFileName_ = fileName;
let i = 1;
let line;
try {
while (line = readline()) {
await this.processLogLine(line);
i++;
}
} catch (e) {
console.error(
`Error occurred during parsing line ${i}` +
', trying to continue: ' + e);
}
this.finalize();
}
async finalize() {
await this._chunkConsumer.consumeAll();
if (this._profile.warnings.size > 0) {
console.warn('Found profiler warnings:', this._profile.warnings);
}
// TODO(cbruni): print stats;
this._mapTimeline.transitions = new Map();
let id = 0;
this._mapTimeline.forEach(map => {
if (map.isRoot()) id = map.finalizeRootMap(id + 1);
if (map.edge && map.edge.name) {
const edge = map.edge;
const list = this._mapTimeline.transitions.get(edge.name);
if (list === undefined) {
this._mapTimeline.transitions.set(edge.name, [edge]);
} else {
list.push(edge);
}
}
});
}
processV8Version(majorVersion, minorVersion) {
if ((majorVersion == this.MAJOR_VERSION &&
minorVersion <= this.MINOR_VERSION) ||
(majorVersion < this.MAJOR_VERSION)) {
window.alert(
`Unsupported version ${majorVersion}.${minorVersion}. \n` +
`Please use the matching tool for given the V8 version.`);
}
}
async processSharedLibrary(name, startAddr, endAddr, aslrSlide) {
const entry = this._profile.addLibrary(name, startAddr, endAddr);
entry.logEntry = new SharedLibLogEntry(entry);
// Many events rely on having a script around, creating fake entries for
// shared libraries.
this._profile.addScriptSource(-1, name, '');
if (this._cppEntriesProvider == undefined) {
await this._setupCppEntriesProvider();
}
await this._cppEntriesProvider.parseVmSymbols(
name, startAddr, endAddr, aslrSlide, (fName, fStart, fEnd) => {
const entry = this._profile.addStaticCode(fName, fStart, fEnd);
entry.logEntry = new CPPCodeLogEntry(entry);
});
}
async _setupCppEntriesProvider() {
// Probe the local symbol server for the platform:
const url = new URL('http://localhost:8000/v8/info/platform')
let platform = {name: 'linux'};
try {
const response = await fetch(url, {timeout: 1});
if (response.status == 404) {
throw new Error(
`Local symbol server returned 404: ${await response.text()}`);
}
platform = await response.json();
} catch (e) {
console.warn(`Local symbol server is not running on ${url}`);
console.warn(e);
}
let CppEntriesProvider = RemoteLinuxCppEntriesProvider;
if (platform.name === 'darwin') {
CppEntriesProvider = RemoteMacOSCppEntriesProvider;
}
this._cppEntriesProvider = new CppEntriesProvider(
platform.nmExec, platform.objdumpExec, platform.targetRootFS,
platform.apkEmbeddedLibrary);
}
processCodeCreation(
type, kind, timestamp, start, size, nameAndPosition, maybe_func) {
this._lastTimestamp = timestamp;
let profilerEntry;
let stateName = '';
if (type != 'RegExp' && maybe_func.length) {
const sfiAddr = this.parseAddress(maybe_func[0]);
stateName = maybe_func[1] ?? '';
const state = Profile.parseState(maybe_func[1]);
profilerEntry = this._profile.addFuncCode(
type, nameAndPosition, timestamp, start, size, sfiAddr, state);
} else {
profilerEntry =
this._profile.addCode(type, nameAndPosition, timestamp, start, size);
}
const name = nameAndPosition.slice(0, nameAndPosition.indexOf(' '));
this._lastCodeLogEntry = new CodeLogEntry(
type + stateName, timestamp,
Profile.getKindFromState(Profile.parseState(stateName)), kind, name,
profilerEntry);
this._codeTimeline.push(this._lastCodeLogEntry);
}
processCodeDeopt(
timestamp, codeSize, instructionStart, inliningId, scriptOffset,
deoptKind, deoptLocation, deoptReason) {
this._lastTimestamp = timestamp;
const profCodeEntry = this._profile.findEntry(instructionStart);
const logEntry = new DeoptLogEntry(
deoptKind, timestamp, profCodeEntry, deoptReason, deoptLocation,
scriptOffset, instructionStart, codeSize, inliningId);
profCodeEntry.logEntry.add(logEntry);
this._deoptTimeline.push(logEntry);
this.addSourcePosition(profCodeEntry, logEntry);
logEntry.functionSourcePosition = logEntry.sourcePosition;
// custom parse deopt location
if (deoptLocation === '<unknown>') return;
// Handle deopt location for inlined code: <location> inlined at <location>
const inlinedPos = deoptLocation.indexOf(' inlined at ');
if (inlinedPos > 0) {
deoptLocation = deoptLocation.substring(0, inlinedPos)
}
const script = this.getProfileEntryScript(profCodeEntry);
if (!script) return;
const colSeparator = deoptLocation.lastIndexOf(':');
const rowSeparator = deoptLocation.lastIndexOf(':', colSeparator - 1);
const line =
parseInt(deoptLocation.substring(rowSeparator + 1, colSeparator));
const column = parseInt(
deoptLocation.substring(colSeparator + 1, deoptLocation.length - 1));
logEntry.sourcePosition = script.addSourcePosition(line, column, logEntry);
}
processFeedbackVector(
timestamp, fbv_address, fbv_length, instructionStart, optimization_marker,
optimization_tier, invocation_count, profiler_ticks, fbv_string) {
const profCodeEntry = this._profile.findEntry(instructionStart);
if (!profCodeEntry) {
console.warn('Didn\'t find code for FBV', {fbv_string, instructionStart});
return;
}
const fbv = new FeedbackVectorEntry(
timestamp, profCodeEntry.logEntry, fbv_address, fbv_length,
optimization_marker, optimization_tier, invocation_count,
profiler_ticks, fbv_string);
profCodeEntry.logEntry.setFeedbackVector(fbv);
}
processScriptSource(scriptId, url, source) {
this._profile.addScriptSource(scriptId, url, source);
}
processCodeMove(from, to) {
this._profile.moveCode(from, to);
}
processCodeDelete(start) {
this._profile.deleteCode(start);
}
processSFIMove(from, to) {
this._profile.moveSharedFunctionInfo(from, to);
}
processTick(
pc, time_ns, 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 = this.kZero;
} 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 = this.kZero;
}
}
const entryStack = this._profile.recordTick(
time_ns, vmState,
this.processStack(pc, tos_or_external_callback, stack));
const newEntry = new TickLogEntry(time_ns, vmState, entryStack);
this._tickTimeline.push(newEntry);
if (this._lastTickLogEntry !== undefined) {
this._lastTickLogEntry.end(time_ns);
}
this._lastTickLogEntry = newEntry;
}
processCodeSourceInfo(
start, scriptId, startPos, endPos, sourcePositions, inliningPositions,
inlinedFunctions) {
this._profile.addSourcePositions(
start, scriptId, startPos, endPos, sourcePositions, inliningPositions,
inlinedFunctions);
if (this._lastCodeLogEntry === undefined) return;
let profileEntry = this._profile.findEntry(start);
if (profileEntry !== this._lastCodeLogEntry._entry) return;
this.addSourcePosition(profileEntry, this._lastCodeLogEntry);
this._lastCodeLogEntry = undefined;
}
addSourcePosition(profileEntry, logEntry) {
let script = this.getProfileEntryScript(profileEntry);
const parts = profileEntry.getRawName().split(':');
if (parts.length < 3) return;
const line = parseInt(parts[parts.length - 2]);
const column = parseInt(parts[parts.length - 1]);
logEntry.sourcePosition = script.addSourcePosition(line, column, logEntry);
}
processCodeDisassemble(start, kind, disassemble) {
this._profile.addDisassemble(start, kind, disassemble);
}
processPropertyIC(
type, pc, time, line, column, old_state, new_state, mapId, key, modifier,
slow_reason) {
this._lastTimestamp = time;
const codeEntry = this._profile.findEntry(pc);
const fnName = this.formatProfileEntry(codeEntry);
const script = this.getProfileEntryScript(codeEntry);
const map = this.getOrCreateMapEntry(mapId, time);
// TODO: Use SourcePosition here directly
let entry = new IcLogEntry(
type, fnName, time, line, column, key, old_state, new_state, map,
slow_reason, modifier, codeEntry);
if (script) {
entry.sourcePosition = script.addSourcePosition(line, column, entry);
}
this._icTimeline.push(entry);
}
formatProfileEntry(profileEntry, line, column) {
if (!profileEntry) return '<unknown>';
if (profileEntry.type === 'Builtin') return profileEntry.name;
const name = profileEntry.sfi.getName();
const array = this._formatPCRegexp.exec(name);
const formatted =
(array === null) ? name : profileEntry.getState() + array[1];
if (line === undefined || column === undefined) return formatted;
return `${formatted}:${line}:${column}`;
}
getProfileEntryScript(profileEntry) {
if (!profileEntry) return undefined;
if (profileEntry.type === 'Builtin') return undefined;
const script = profileEntry.source?.script;
if (script !== undefined) return script;
let fileName;
if (profileEntry.type === 'SHARED_LIB') {
fileName = profileEntry.name;
} else {
// Slow path, try to get the script from the url:
const fnName = this.formatProfileEntry(profileEntry);
let parts = fnName.split(' ');
fileName = parts[parts.length - 1];
}
return this.getScript(fileName);
}
processMap(type, time, from, to, pc, line, column, reason, name) {
this._lastTimestamp = time;
const time_ = parseInt(time);
if (type === 'Deprecate') return this.deprecateMap(type, time_, from);
// Skip normalized maps that were cached so we don't introduce multiple
// edges with the same source and target map.
if (type === 'NormalizeCached') return;
const from_ = this.getOrCreateMapEntry(from, time_);
const to_ = this.getOrCreateMapEntry(to, time_);
if (type === 'Normalize') {
// Fix a bug where we log "Normalize" transitions for maps created from
// the NormalizedMapCache.
if (to_.parent?.id === from || to_.time < from_.time || to_.depth > 0) {
console.log(`Skipping transition to cached normalized map`);
return;
}
}
if (pc) {
const profCodeEntry = this._profile.findEntry(pc);
if (profCodeEntry) {
to_.entry = profCodeEntry;
profCodeEntry.logEntry.add(to_);
let script = this.getProfileEntryScript(profCodeEntry);
if (script) {
to_.sourcePosition = script.addSourcePosition(line, column, to_);
}
}
}
let edge = new Edge(type, name, reason, time, from_, to_);
if (to_.parent !== undefined && to_.parent === from_) {
// Fix bug where we double log transitions.
console.warn('Fixing up double transition');
to_.edge.updateFrom(edge);
} else {
edge.finishSetup();
}
}
deprecateMap(type, time, id) {
this._lastTimestamp = time;
this.getOrCreateMapEntry(id, time).deprecate();
}
processMapCreate(time, id) {
// map-create events might override existing maps if the addresses get
// recycled. Hence we do not check for existing maps.
this._lastTimestamp = time;
this.createMapEntry(id, time);
}
processMapDetails(time, id, string) {
// TODO(cbruni): fix initial map logging.
const map = this.getOrCreateMapEntry(id, time);
map.description = string;
}
createMapEntry(id, time) {
this._lastTimestamp = time;
const map = new MapLogEntry(id, time);
this._mapTimeline.push(map);
return map;
}
getOrCreateMapEntry(id, time) {
if (id === '0x000000000000') return undefined;
const map = MapLogEntry.get(id, time);
if (map !== undefined) return map;
console.warn(`No map details provided: id=${id}`);
// Manually patch in a map to continue running.
return this.createMapEntry(id, time);
}
getScript(url) {
const script = this._profile.getScript(url);
// TODO create placeholder script for empty urls.
if (script === undefined) {
console.error(`Could not find script for url: '${url}'`)
}
return script;
}
processApiEvent(type, varArgs) {
// legacy events that are no longer supported
}
processTimerEventStart(type, time) {
const entry = new TimerLogEntry(type, time);
this._timerTimeline.push(entry);
}
processTimerEventEnd(type, time) {
// Timer-events are infrequent, and not deeply nested, doing a linear walk
// is usually good enough.
for (let i = this._timerTimeline.length - 1; i >= 0; i--) {
const timer = this._timerTimeline.at(i);
if (timer.type == type && !timer.isInitialized) {
timer.end(time);
return;
}
}
console.error('Couldn\'t find matching timer event start', {type, time});
}
get icTimeline() {
return this._icTimeline;
}
get mapTimeline() {
return this._mapTimeline;
}
get deoptTimeline() {
return this._deoptTimeline;
}
get codeTimeline() {
return this._codeTimeline;
}
get tickTimeline() {
return this._tickTimeline;
}
get timerTimeline() {
return this._timerTimeline;
}
get scripts() {
return this._profile.scripts_.filter(script => script !== undefined);
}
get profile() {
return this._profile;
}
}

View File

@ -0,0 +1,335 @@
// 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 {TickLogEntry} from './log/tick.mjs';
const kForward = 1;
const kBackwards = -1;
/**
* The StackSorter sorts ticks by recursively grouping the most frequent frames
* at each stack-depth to the beginning. This is used to generate flame graphs.
*
* Example
* tick1 = [a1, b1]
* tick2 = [a0, b0]
* tick3 = [a1, b1, c1]
* tick4 = [a1, b0, c0, d0]
*
* If we sort this recursively from the beginning we get:
* tick1 = [a1, b1]
* tick3 = [a1, b1, c1]
* tick4 = [a1, b0, c0, d0]
* tick2 = [a0, b0]
*
* The idea is to group common stacks together to generate easily readable
* graphs where we can quickly discover which function is the highest incoming
* or outgoing contributor.
*/
export class StackSorter {
static fromTop(array, maxDepth) {
return new this(array, maxDepth, kForward).sorted();
}
static fromBottom(array, maxDepth) {
return new this(array, maxDepth, kBackwards).sorted();
}
constructor(array, maxDepth, direction) {
this.stacks = array;
this.maxDepth = maxDepth;
if (direction !== kForward && direction !== kBackwards) {
throw new Error('Invalid direction');
}
this.direction = direction;
}
sorted() {
const startLevel = this.direction == kForward ? 0 : this.maxDepth - 1;
this._sort(0, this.stacks.length, startLevel);
return this.stacks;
}
_sort(start, end, stackIndex) {
if (stackIndex >= this.maxDepth || stackIndex < 0) return;
const length = end - start;
if (length <= 1) return;
let counts;
{
const kNoFrame = -1;
let bag = new Map();
for (let i = start; i < end; i++) {
let code = this.stacks[i][stackIndex] ?? kNoFrame;
const count = bag.get(code) ?? 0;
bag.set(code, count + 1);
}
// If all the frames are the same at the current stackIndex, check the
// next stackIndex.
if (bag.size === 1) {
return this._sort(start, end, stackIndex + this.direction);
}
counts = Array.from(bag)
counts.sort((a, b) => b[1] - a[1]);
let currentIndex = start;
// Reuse bag since we've copied out the counts;
let insertionIndices = bag;
for (let i = 0; i < counts.length; i++) {
const pair = counts[i];
const code = pair[0];
const count = pair[1];
insertionIndices.set(code, currentIndex);
currentIndex += count;
}
// TODO do copy-less
let stacksSegment = this.stacks.slice(start, end);
for (let i = 0; i < length; i++) {
const stack = stacksSegment[i];
const entry = stack[stackIndex] ?? kNoFrame;
const insertionIndex = insertionIndices.get(entry);
if (!Number.isFinite(insertionIndex)) {
throw 'Invalid insertionIndex: ' + insertionIndex;
}
if (insertionIndex < start || insertionIndex >= end) {
throw 'Invalid insertionIndex: ' + insertionIndex;
}
this.stacks[insertionIndex] = stack;
insertionIndices.set(entry, insertionIndex + 1);
}
// Free up variables before recursing.
insertionIndices = bag = stacksSegment = undefined;
}
// Sort sub-segments
let segmentStart = start;
let segmentEnd = start;
for (let i = 0; i < counts.length; i++) {
const segmentLength = counts[i][1];
segmentEnd = segmentStart + segmentLength - 1;
this._sort(segmentStart, segmentEnd, stackIndex + this.direction);
segmentStart = segmentEnd;
}
}
}
export class ProfileNode {
constructor(codeEntry) {
this.codeEntry = codeEntry;
this.inCodeEntries = [];
// [tick0, framePos0, tick1, framePos1, ...]
this.ticksAndPosition = [];
this.outCodeEntries = [];
this._selfDuration = 0;
this._totalDuration = 0;
}
stacksOut() {
const slicedStacks = [];
let maxDepth = 0;
for (let i = 0; i < this.ticksAndPosition.length; i += 2) {
// tick.stack = [topN, ..., top0, this.codeEntry, bottom0, ..., bottomN];
const tick = this.ticksAndPosition[i];
const stackIndex = this.ticksAndPosition[i + 1];
// slice = [topN, ..., top0]
const slice = tick.stack.slice(0, stackIndex);
maxDepth = Math.max(maxDepth, slice.length);
slicedStacks.push(slice);
}
// Before:
// stack1 = [f4, f3, f2, f1]
// stack2 = [f2, f1]
// After:
// stack1 = [f4, f3, f2, f1]
// stack2 = [ , , f2, f1]
for (let i = 0; i < slicedStacks.length; i++) {
const stack = slicedStacks[i];
const length = stack.length;
if (length < maxDepth) {
// Pad stacks at the beginning
stack.splice(maxDepth - length, 0, undefined);
}
}
// Start sorting at the bottom-most frame: top0 => topN / f1 => fN
return StackSorter.fromBottom(slicedStacks, maxDepth);
}
stacksIn() {
const slicedStacks = [];
let maxDepth = 0;
for (let i = 0; i < this.ticksAndPosition.length; i += 2) {
// tick.stack = [topN, ..., top0, this.codeEntry, bottom0..., bottomN];
const tick = this.ticksAndPosition[i];
const stackIndex = this.ticksAndPosition[i + 1];
// slice = [bottom0, ..., bottomN]
const slice = tick.stack.slice(stackIndex + 1);
maxDepth = Math.max(maxDepth, slice.length);
slicedStacks.push(slice);
}
// Start storting at the top-most frame: bottom0 => bottomN
return StackSorter.fromTop(slicedStacks, maxDepth);
}
startTime() {
return this.ticksAndPosition[0].startTime;
}
endTime() {
return this.ticksAndPosition[this.ticksAndPosition.length - 2].endTime;
}
duration() {
return this.endTime() - this.startTime();
}
selfCount() {
return this.totalCount() - this.outCodeEntries.length;
}
totalCount() {
return this.ticksAndPosition.length / 2;
}
isLeaf() {
return this.selfCount() == this.totalCount();
}
totalDuration() {
let duration = 0;
for (let entry of this.ticksAndPosition) duration += entry.duration;
return duration;
}
selfDuration() {
let duration = this.totalDuration();
for (let entry of this.outCodeEntries) duration -= entry.duration;
return duration;
}
}
export class Flame {
constructor(time, logEntry, depth, duration = -1) {
this._time = time;
this._logEntry = logEntry;
this.depth = depth;
this._duration = duration;
this.parent = undefined;
this.children = [];
}
static add(time, logEntry, stack, flames) {
const depth = stack.length;
const newFlame = new Flame(time, logEntry, depth);
if (depth > 0) {
const parent = stack[depth - 1];
newFlame.parent = parent;
parent.children.push(newFlame);
}
flames.push(newFlame);
stack.push(newFlame);
}
stop(time) {
if (this._duration !== -1) throw new Error('Already stopped');
this._duration = time - this._time
}
get time() {
return this._time;
}
get logEntry() {
return this._logEntry;
}
get startTime() {
return this._time;
}
get endTime() {
return this._time + this._duration;
}
get duration() {
return this._duration;
}
get type() {
return TickLogEntry.extractCodeEntryType(this._logEntry?.entry);
}
get name() {
return this._logEntry?.name;
}
}
export class FlameBuilder {
static forTime(ticks, reverseDepth) {
return new this(ticks, true, reverseDepth);
}
static forTicks(ticks, reverseDepth = false) {
return new this(ticks, false, reverseDepth);
}
constructor(ticks, useTime, reverseDepth) {
this.maxDepth = 0;
let tempFlames = this.flames = [];
this.reverseDepth = reverseDepth;
if (ticks.length == 0) return;
if (!(ticks[0] instanceof TickLogEntry) && !Array.isArray(ticks[0])) {
throw new Error(
'Expected ticks array: `[TickLogEntry, ..]`, or raw stacks: `[[CodeEntry, ...], [...]]`');
}
// flameStack = [bottom, ..., top];
const flameStack = [];
let maxDepth = 0;
const ticksLength = ticks.length;
for (let tickIndex = 0; tickIndex < ticksLength; tickIndex++) {
const tick = ticks[tickIndex];
// tick is either a Tick log entry, or an Array
const tickStack = tick.stack ?? tick;
const tickStackLength = tickStack.length;
const timeValue = useTime ? tick.time : tickIndex;
maxDepth = Math.max(maxDepth, tickStackLength);
// tick.stack = [top, .... , bottom];
for (let stackIndex = tickStackLength - 1; stackIndex >= 0;
stackIndex--) {
const codeEntry = tickStack[stackIndex];
// Assume that all higher stack entries are undefined as well.
if (codeEntry === undefined) break;
// codeEntry is either a CodeEntry or a raw pc.
const logEntry = codeEntry?.logEntry ?? codeEntry;
const flameStackIndex = tickStackLength - stackIndex - 1;
if (flameStackIndex < flameStack.length) {
if (flameStack[flameStackIndex].logEntry === logEntry) continue;
// A lower frame changed, close all higher Flames.
for (let k = flameStackIndex; k < flameStack.length; k++) {
flameStack[k].stop(timeValue);
}
flameStack.length = flameStackIndex;
}
Flame.add(timeValue, logEntry, flameStack, tempFlames);
}
// Stop all Flames that are deeper nested than the current stack.
if (tickStackLength < flameStack.length) {
for (let k = tickStackLength; k < flameStack.length; k++) {
flameStack[k].stop(timeValue);
}
flameStack.length = tickStackLength;
}
}
this.maxDepth = maxDepth;
const lastTime = useTime ? ticks[ticksLength - 1].time : ticksLength - 1;
for (let k = 0; k < flameStack.length; k++) {
flameStack[k].stop(lastTime);
}
}
}

View File

@ -0,0 +1,342 @@
// Copyright 2020 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 {groupBy} from './helper.mjs'
/** @template T */
class Timeline {
/** Class T */
_model;
/** @type {T[]} */
_values;
// Current selection, subset of #values:
/** @type {?Timeline<T>} */
_selection;
_breakdown;
constructor(model, values = [], startTime = null, endTime = null) {
this._model = model;
this._values = values;
/** @type {number} */
this.startTime = startTime;
/** @type {number} */
this.endTime = endTime;
if (values.length > 0) {
if (startTime === null) this.startTime = values[0].time;
if (endTime === null) this.endTime = values[values.length - 1].time;
} else {
if (startTime === null) this.startTime = 0;
if (endTime === null) this.endTime = 0;
}
}
get model() {
return this._model;
}
get all() {
return this._values;
}
get selection() {
return this._selection;
}
/** @returns {Timeline<T>} */
get selectionOrSelf() {
return this._selection ?? this;
}
/** @param {Timeline<T>} value */
set selection(value) {
this._selection = value;
}
selectTimeRange(startTime, endTime) {
const items = this.range(startTime, endTime);
this._selection = new Timeline(this._model, items, startTime, endTime);
}
clearSelection() {
this._selection = undefined;
}
getChunks(windowSizeMs) {
return this.chunkSizes(windowSizeMs);
}
/** @returns {T[]} */
get values() {
return this._values;
}
/** @returns {number} */
count(filter) {
return this.all.reduce((sum, each) => {
return sum + (filter(each) === true ? 1 : 0);
}, 0);
}
filter(predicate) {
return this.all.filter(predicate);
}
/** @param {T} event */
push(event) {
let time = event.time;
if (!this.isEmpty() && this.last().time > time) {
// Invalid insertion order, might happen without --single-process,
// finding insertion point.
let insertionPoint = this.find(time);
this._values.splice(insertionPoint, event);
} else {
this._values.push(event);
}
if (time > 0) {
this.endTime = Math.max(this.endTime, time);
if (this.startTime === 0) {
this.startTime = time;
} else {
this.startTime = Math.min(this.startTime, time);
}
}
}
/** @returns {T} */
at(index) {
return this._values[index];
}
isEmpty() {
return this.size() === 0;
}
size() {
return this._values.length;
}
get length() {
return this._values.length;
}
/** @returns {T[]} */
slice(startIndex, endIndex) {
return this._values.slice(startIndex, endIndex);
}
/** @returns {T} */
first() {
return this._values[0];
}
/** @returns {T} */
last() {
return this._values[this._values.length - 1];
}
* [Symbol.iterator]() {
yield* this._values;
}
/** @returns {number} */
duration() {
return this.endTime - this.startTime;
}
forEachChunkSize(count, fn) {
if (this.isEmpty()) return;
const increment = this.duration() / count;
let currentTime = this.startTime;
let index = 0;
for (let i = 0; i < count - 1; i++) {
const nextTime = currentTime + increment;
const nextIndex = this.findLast(nextTime, index);
fn(index, nextIndex, currentTime, nextTime);
index = nextIndex + 1;
currentTime = nextTime;
}
fn(index, this._values.length - 1, currentTime, this.endTime);
}
/** @returns {number[]} */
chunkSizes(count) {
const chunks = [];
this.forEachChunkSize(count, (start, end) => chunks.push(end - start));
return chunks;
}
/** @returns {Chunk<T>[]} */
chunks(count, predicate = undefined) {
const chunks = [];
this.forEachChunkSize(count, (start, end, startTime, endTime) => {
let items = this._values.slice(start, end + 1);
if (predicate !== undefined) items = items.filter(predicate);
chunks.push(new Chunk(chunks.length, startTime, endTime, items));
});
return chunks;
}
/**
* Return all entries in ({startTime}, {endTime}]
* @returns {T[]}
**/
range(startTime, endTime) {
const firstIndex = this.find(startTime);
if (firstIndex < 0) return [];
const lastIndex = this.find(endTime, firstIndex + 1);
return this._values.slice(firstIndex, lastIndex);
}
// Return the first index with element.time >= time.
find(time, offset = 0) {
return this.findFirst(time, offset);
}
findFirst(time, offset = 0) {
return this._find(this._values, each => each.time - time, offset);
}
// Return the last index with element.time <= time.
findLast(time, offset = 0) {
const nextTime = time + 1;
let index = (this.last().time <= nextTime) ?
this.length :
this.findFirst(nextTime + 1, offset);
// Typically we just have to look at the previous element.
while (index > 0) {
index--;
if (this._values[index].time <= time) return index;
}
return -1;
}
// Return the first index for which compareFn(item) is >= 0;
_find(array, compareFn, offset = 0) {
let minIndex = offset;
let maxIndex = array.length - 1;
while (minIndex < maxIndex) {
const midIndex = minIndex + (((maxIndex - minIndex) / 2) | 0);
if (compareFn(array[midIndex]) < 0) {
minIndex = midIndex + 1;
} else {
maxIndex = midIndex;
}
}
return minIndex;
}
getBreakdown(keyFunction, collect = false) {
if (keyFunction || collect) {
if (!keyFunction) {
keyFunction = each => each.type;
}
return groupBy(this._values, keyFunction, collect);
}
if (this._breakdown === undefined) {
this._breakdown = groupBy(this._values, each => each.type);
}
return this._breakdown;
}
depthHistogram() {
return this._values.histogram(each => each.depth);
}
fanOutHistogram() {
return this._values.histogram(each => each.children.length);
}
forEach(fn) {
return this._values.forEach(fn);
}
}
// ===========================================================================
/** @template T */
class Chunk {
constructor(index, start, end, items) {
this.index = index;
this.start = start;
this.end = end;
/** @type {T[]} */
this.items = items;
this.height = 0;
}
isEmpty() {
return this.items.length === 0;
}
/** @returns {T} */
last() {
return this.at(this.size() - 1);
}
/** @returns {T} */
first() {
return this.at(0);
}
/** @returns {T} */
at(index) {
return this.items[index];
}
size() {
return this.items.length;
}
get length() {
return this.items.length;
}
/** @param {T} event */
yOffset(event) {
// items[0] == oldest event, displayed at the top of the chunk
// items[n-1] == youngest event, displayed at the bottom of the chunk
return ((this.indexOf(event) + 0.5) / this.size()) * this.height;
}
/** @param {T} event */
indexOf(event) {
return this.items.indexOf(event);
}
/** @param {T} event */
has(event) {
if (this.isEmpty()) return false;
return this.first().time <= event.time && event.time <= this.last().time;
}
/** @param {Chunk<T>[]} chunks */
next(chunks) {
return this.findChunk(chunks, 1);
}
/** @param {Chunk<T>[]} chunks */
prev(chunks) {
return this.findChunk(chunks, -1);
}
findChunk(chunks, delta) {
let i = this.index + delta;
let chunk = chunks[i];
while (chunk && chunk.size() === 0) {
i += delta;
chunk = chunks[i];
}
return chunk;
}
getBreakdown(keyFunction) {
return groupBy(this.items, keyFunction);
}
/** @returns {T[]} */
filter() {
return this.items.filter(map => !map.parent || !this.has(map.parent));
}
}
export {Timeline, Chunk};

View File

@ -0,0 +1,48 @@
<!-- Copyright 2020 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. -->
<head>
<link href="./index.css" rel="stylesheet">
</head>
<style>
#sourceCode {
white-space: pre-line;
}
.reg, .addr {
border-bottom: 1px dashed;
border-radius: 2px;
}
.reg:hover, .addr:hover {
background-color: var(--border-color);
}
.reg.selected, .addr.selected {
color: var(--default-color);
background-color: var(--border-color);
}
.addr:hover {
cursor: pointer;
}
.basicBlock:hover {
background-color: var(--border-color);
border-radius: 2px;
}
</style>
<div class="panel">
<input type="checkbox" id="closer" class="panelCloserInput" checked>
<label class="panelCloserLabel" for="closer">▼</label>
<h2 class="title">Code Panel</h2>
<div class="selection">
<select id="codeSelect"></select>
<button id="selectedRelatedButton">Select Related Events</button>
</div>
<div class="panelBody">
<h3>Properties</h3>
<property-link-table id="properties"></property-link-table>
<h3>FeedbackVector</h3>
<property-link-table id="feedbackVector"></property-link-table>
<h3>Disassembly</h3>
<code id="disassembly"></code>
</div>
</div>

View File

@ -0,0 +1,368 @@
// Copyright 2020 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 {SelectRelatedEvent} from './events.mjs';
import {CollapsableElement, DOM, formatBytes, formatMicroSeconds} from './helper.mjs';
DOM.defineCustomElement(
'view/code-panel',
(templateText) => class CodePanel extends CollapsableElement {
_timeline;
_selectedEntries;
_entry;
constructor() {
super(templateText);
this._propertiesNode = this.$('#properties');
this._codeSelectNode = this.$('#codeSelect');
this._disassemblyNode = this.$('#disassembly');
this._feedbackVectorNode = this.$('#feedbackVector');
this._selectionHandler = new SelectionHandler(this._disassemblyNode);
this._codeSelectNode.onchange = this._handleSelectCode.bind(this);
this.$('#selectedRelatedButton').onclick =
this._handleSelectRelated.bind(this)
}
set timeline(timeline) {
this._timeline = timeline;
this.$('.panel').style.display =
timeline.isEmpty() ? 'none' : 'inherit';
this.requestUpdate();
}
set selectedEntries(entries) {
this._selectedEntries = entries;
this.entry = entries.first();
}
set entry(entry) {
this._entry = entry;
if (!entry) {
this._propertiesNode.propertyDict = {};
} else {
this._propertiesNode.propertyDict = {
'__this__': entry,
functionName: entry.functionName,
size: formatBytes(entry.size),
creationTime: formatMicroSeconds(entry.time / 1000),
sourcePosition: entry.sourcePosition,
script: entry.script,
type: entry.type,
kind: entry.kindName,
variants: entry.variants.length > 1 ?
[undefined, ...entry.variants] :
undefined,
};
}
this.requestUpdate();
}
_update() {
this._updateSelect();
this._updateDisassembly();
this._updateFeedbackVector();
}
_updateFeedbackVector() {
if (!this._entry?.feedbackVector) {
this._feedbackVectorNode.propertyDict = {};
} else {
const dict = this._entry.feedbackVector.toolTipDict;
delete dict.title;
delete dict.code;
this._feedbackVectorNode.propertyDict = dict;
}
}
_updateDisassembly() {
this._disassemblyNode.innerText = '';
if (!this._entry?.code) return;
try {
this._disassemblyNode.appendChild(
new AssemblyFormatter(this._entry).fragment);
} catch (e) {
console.error(e);
this._disassemblyNode.innerText = this._entry.code;
}
}
_updateSelect() {
const select = this._codeSelectNode;
if (select.data === this._selectedEntries) return;
select.data = this._selectedEntries;
select.options.length = 0;
const sorted =
this._selectedEntries.slice().sort((a, b) => a.time - b.time);
for (const code of this._selectedEntries) {
const option = DOM.element('option');
option.text = this._entrySummary(code);
option.data = code;
select.add(option);
}
}
_entrySummary(code) {
if (code.isBuiltinKind) {
return `${code.functionName}(...) t=${
formatMicroSeconds(code.time)} size=${formatBytes(code.size)}`;
}
return `${code.functionName}(...) t=${
formatMicroSeconds(code.time)} size=${
formatBytes(code.size)} script=${code.script?.toString()}`;
}
_handleSelectCode() {
this.entry = this._codeSelectNode.selectedOptions[0].data;
}
_handleSelectRelated(e) {
if (!this._entry) return;
this.dispatchEvent(new SelectRelatedEvent(this._entry));
}
});
const kRegisters = ['rsp', 'rbp', 'rax', 'rbx', 'rcx', 'rdx', 'rsi', 'rdi'];
// Make sure we dont match register on bytecode: Star1 or Star2
const kAvoidBytecodeOpsRegexpSource = '(.*?[^a-zA-Z])'
// Look for registers in strings like: movl rbx,[rcx-0x30]
const kRegisterRegexpSource = `(?<register>${kRegisters.join('|')}|r[0-9]+)`
const kRegisterSplitRegexp =
new RegExp(`${kAvoidBytecodeOpsRegexpSource}${kRegisterRegexpSource}`)
const kIsRegisterRegexp = new RegExp(`^${kRegisterRegexpSource}$`);
const kFullAddressRegexp = /(0x[0-9a-f]{8,})/;
const kRelativeAddressRegexp = /([+-]0x[0-9a-f]+)/;
const kAnyAddressRegexp = /(?<address>[+-]?0x[0-9a-f]+)/;
const kJmpRegexp = new RegExp(`jmp ${kRegisterRegexpSource}`);
const kMovRegexp =
new RegExp(`mov. ${kRegisterRegexpSource},${kAnyAddressRegexp.source}`);
class AssemblyFormatter {
constructor(codeLogEntry) {
this._fragment = new DocumentFragment();
this._entry = codeLogEntry;
this._lines = new Map();
this._previousLine = undefined;
this._parseLines();
this._format();
}
get fragment() {
return this._fragment;
}
_format() {
let block = DOM.div(['basicBlock', 'header']);
this._lines.forEach(line => {
if (!block || line.isBlockStart) {
this._fragment.appendChild(block);
block = DOM.div('basicBlock');
}
block.appendChild(line.format())
});
this._fragment.appendChild(block);
}
_parseLines() {
this._entry.code.split('\n').forEach(each => this._parseLine(each));
this._findBasicBlocks();
}
_parseLine(line) {
const parts = line.split(' ');
// Use unique placeholder for address:
let lineAddress = -this._lines.size;
for (let part of parts) {
if (kFullAddressRegexp.test(part)) {
lineAddress = parseInt(part);
break;
}
}
const newLine = new AssemblyLine(lineAddress, parts);
// special hack for: mov reg 0x...; jmp reg;
if (lineAddress <= 0 && this._previousLine) {
const jmpMatch = line.match(kJmpRegexp);
if (jmpMatch) {
const register = jmpMatch.groups.register;
const movMatch = this._previousLine.line.match(kMovRegexp);
if (movMatch.groups.register === register) {
newLine.outgoing.push(movMatch.groups.address);
}
}
}
this._lines.set(lineAddress, newLine);
this._previousLine = newLine;
}
_findBasicBlocks() {
const lines = Array.from(this._lines.values());
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
let forceBasicBlock = i == 0;
if (i > 0 && i < lines.length - 1) {
const prevHasAddress = lines[i - 1].address > 0;
const currentHasAddress = lines[i].address > 0;
const nextHasAddress = lines[i + 1].address > 0;
if (prevHasAddress !== currentHasAddress &&
currentHasAddress == nextHasAddress) {
forceBasicBlock = true;
}
}
if (forceBasicBlock) {
// Add fake-incoming address to mark a block start.
line.addIncoming(0);
}
line.outgoing.forEach(address => {
const outgoing = this._lines.get(address);
if (outgoing) outgoing.addIncoming(line.address);
})
}
}
}
class AssemblyLine {
constructor(address, parts) {
this.address = address;
this.outgoing = [];
this.incoming = [];
parts.forEach(part => {
const fullMatch = part.match(kFullAddressRegexp);
if (fullMatch) {
let inlineAddress = parseInt(fullMatch[0]);
if (inlineAddress != this.address) this.outgoing.push(inlineAddress);
if (Number.isNaN(inlineAddress)) throw 'invalid address';
} else if (kRelativeAddressRegexp.test(part)) {
this.outgoing.push(this._toAbsoluteAddress(part));
}
});
this.line = parts.join(' ');
}
get isBlockStart() {
return this.incoming.length > 0;
}
addIncoming(address) {
this.incoming.push(address);
}
format() {
const content = DOM.span({textContent: this.line + '\n'});
let formattedCode = content.innerHTML.split(kRegisterSplitRegexp)
.map(part => this._formatRegisterPart(part))
.join('');
formattedCode =
formattedCode.split(kAnyAddressRegexp)
.map((part, index) => this._formatAddressPart(part, index))
.join('');
// Let's replace the base-address since it doesn't add any value.
// TODO
content.innerHTML = formattedCode;
return content;
}
_formatRegisterPart(part) {
if (!kIsRegisterRegexp.test(part)) return part;
return `<span class="reg ${part}">${part}</span>`
}
_formatAddressPart(part, index) {
if (kFullAddressRegexp.test(part)) {
// The first or second address must be the line address
if (index <= 1) {
return `<span class="addr line" data-addr="${part}">${part}</span>`;
}
return `<span class=addr data-addr="${part}">${part}</span>`;
} else if (kRelativeAddressRegexp.test(part)) {
return `<span class=addr data-addr="0x${
this._toAbsoluteAddress(part).toString(16)}">${part}</span>`;
} else {
return part;
}
}
_toAbsoluteAddress(part) {
return this.address + parseInt(part);
}
}
class SelectionHandler {
_currentRegisterHovered;
_currentRegisterClicked;
constructor(node) {
this._node = node;
this._node.onmousemove = this._handleMouseMove.bind(this);
this._node.onclick = this._handleClick.bind(this);
}
$(query) {
return this._node.querySelectorAll(query);
}
_handleClick(event) {
const target = event.target;
if (target.classList.contains('addr')) {
return this._handleClickAddress(target);
} else if (target.classList.contains('reg')) {
this._handleClickRegister(target);
} else {
this._clearRegisterSelection();
}
}
_handleClickAddress(target) {
let targetAddress = target.getAttribute('data-addr') ?? target.innerText;
// Clear any selection
for (let addrNode of this.$('.addr.selected')) {
addrNode.classList.remove('selected');
}
// Highlight all matching addresses
let lineAddrNode;
for (let addrNode of this.$(`.addr[data-addr="${targetAddress}"]`)) {
addrNode.classList.add('selected');
if (addrNode.classList.contains('line') && lineAddrNode == undefined) {
lineAddrNode = addrNode;
}
}
// Jump to potential target address.
if (lineAddrNode) {
lineAddrNode.scrollIntoView({behavior: 'smooth', block: 'nearest'});
}
}
_handleClickRegister(target) {
this._setRegisterSelection(target.innerText);
this._currentRegisterClicked = this._currentRegisterHovered;
}
_handleMouseMove(event) {
if (this._currentRegisterClicked) return;
const target = event.target;
if (!target.classList.contains('reg')) {
this._clearRegisterSelection();
} else {
this._setRegisterSelection(target.innerText);
}
}
_clearRegisterSelection() {
if (!this._currentRegisterHovered) return;
for (let node of this.$('.reg.selected')) {
node.classList.remove('selected');
}
this._currentRegisterClicked = undefined;
this._currentRegisterHovered = undefined;
}
_setRegisterSelection(register) {
if (register == this._currentRegisterHovered) return;
this._clearRegisterSelection();
this._currentRegisterHovered = register;
for (let node of this.$(`.reg.${register}`)) {
node.classList.add('selected');
}
}
}

View File

@ -0,0 +1,100 @@
// Copyright 2020 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.
class AppEvent extends CustomEvent {
constructor(name) {
super(name, {bubbles: true, composed: true});
}
}
export class SelectionEvent extends AppEvent {
// TODO: turn into static class fields once Safari supports it.
static get name() {
return 'select';
}
constructor(entries) {
super(SelectionEvent.name);
if (!Array.isArray(entries) || entries.length == 0) {
throw new Error('No valid entries selected!');
}
this.entries = entries;
}
}
export class SelectRelatedEvent extends AppEvent {
static get name() {
return 'selectrelated';
}
constructor(entry) {
super(SelectRelatedEvent.name);
this.entry = entry;
}
}
export class FocusEvent extends AppEvent {
static get name() {
return 'showentrydetail';
}
constructor(entry) {
super(FocusEvent.name);
this.entry = entry;
}
}
export class SelectTimeEvent extends AppEvent {
static get name() {
return 'timerangeselect';
}
constructor(start = 0, end = Infinity, focus = false, zoom = false) {
super(SelectTimeEvent.name);
this.start = start;
this.end = end;
this.focus = focus;
this.zoom = zoom;
}
}
export class SynchronizeSelectionEvent extends AppEvent {
static get name() {
return 'syncselection';
}
constructor(start, end) {
super(SynchronizeSelectionEvent.name);
this.start = start;
this.end = end;
}
}
export class ToolTipEvent extends AppEvent {
static get name() {
return 'showtooltip';
}
constructor(content, positionOrTargetNode, immediate) {
super(ToolTipEvent.name);
if (!positionOrTargetNode) {
throw Error('Either provide a valid position or targetNode');
}
this._content = content;
this._positionOrTargetNode = positionOrTargetNode;
this._immediate = immediate;
}
get content() {
return this._content;
}
get positionOrTargetNode() {
return this._positionOrTargetNode;
}
get immediate() {
return this._immediate;
}
}

View File

@ -0,0 +1,348 @@
// Copyright 2020 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.
export class CSSColor {
static _cache = new Map();
static get(name) {
let color = this._cache.get(name);
if (color !== undefined) return color;
const style = getComputedStyle(document.body);
color = style.getPropertyValue(`--${name}`);
if (color === undefined) {
throw new Error(`CSS color does not exist: ${name}`);
}
color = color.trim();
this._cache.set(name, color);
return color;
}
static reset() {
this._cache.clear();
}
static get backgroundColor() {
return this.get('background-color');
}
static get surfaceColor() {
return this.get('surface-color');
}
static get primaryColor() {
return this.get('primary-color');
}
static get secondaryColor() {
return this.get('secondary-color');
}
static get onSurfaceColor() {
return this.get('on-surface-color');
}
static get onBackgroundColor() {
return this.get('on-background-color');
}
static get onPrimaryColor() {
return this.get('on-primary-color');
}
static get onSecondaryColor() {
return this.get('on-secondary-color');
}
static get defaultColor() {
return this.get('default-color');
}
static get errorColor() {
return this.get('error-color');
}
static get mapBackgroundColor() {
return this.get('map-background-color');
}
static get timelineBackgroundColor() {
return this.get('timeline-background-color');
}
static get red() {
return this.get('red');
}
static get green() {
return this.get('green');
}
static get yellow() {
return this.get('yellow');
}
static get blue() {
return this.get('blue');
}
static get orange() {
return this.get('orange');
}
static get violet() {
return this.get('violet');
}
static at(index) {
return this.list[index % this.list.length];
}
static darken(hexColorString, amount = -50) {
if (hexColorString[0] !== '#') {
throw new Error(`Unsupported color: ${hexColorString}`);
}
let color = parseInt(hexColorString.substring(1), 16);
let b = Math.min(Math.max((color & 0xFF) + amount, 0), 0xFF);
let g = Math.min(Math.max(((color >> 8) & 0xFF) + amount, 0), 0xFF);
let r = Math.min(Math.max(((color >> 16) & 0xFF) + amount, 0), 0xFF);
color = (r << 16) + (g << 8) + b;
return `#${color.toString(16).padStart(6, '0')}`;
}
static get list() {
if (!this._colors) {
this._colors = [
this.green,
this.violet,
this.orange,
this.yellow,
this.primaryColor,
this.red,
this.blue,
this.yellow,
this.secondaryColor,
this.darken(this.green),
this.darken(this.violet),
this.darken(this.orange),
this.darken(this.yellow),
this.darken(this.primaryColor),
this.darken(this.red),
this.darken(this.blue),
this.darken(this.yellow),
this.darken(this.secondaryColor),
];
}
return this._colors;
}
}
import {DOM} from '../../js/web-api-helper.mjs';
const SVGNamespace = 'http://www.w3.org/2000/svg';
export class SVG {
static element(type, classes) {
const node = document.createElementNS(SVGNamespace, type);
if (classes !== undefined) DOM.addClasses(node, classes);
return node;
}
static svg(classes) {
return this.element('svg', classes);
}
static rect(classes) {
return this.element('rect', classes);
}
static path(classes) {
return this.element('path', classes);
}
static g(classes) {
return this.element('g', classes);
}
}
export function $(id) {
return document.querySelector(id)
}
import {V8CustomElement} from '../../js/web-api-helper.mjs'
export class CollapsableElement extends V8CustomElement {
constructor(templateText) {
super(templateText);
this._hasPendingUpdate = false;
this._closer.onclick = _ => this._requestUpdateIfVisible();
}
get _closer() {
return this.$('#closer');
}
get _contentIsVisible() {
return !this._closer.checked;
}
hide() {
if (this._contentIsVisible) {
this._closer.checked = true;
this._requestUpdateIfVisible();
}
}
show() {
if (!this._contentIsVisible) {
this._closer.checked = false;
this._requestUpdateIfVisible();
}
this.scrollIntoView({behavior: 'smooth', block: 'center'});
}
requestUpdate(useAnimation = false) {
// A pending update will be resolved later, no need to try again.
if (this._hasPendingUpdate) return;
this._hasPendingUpdate = true;
this._requestUpdateIfVisible(useAnimation);
}
_requestUpdateIfVisible(useAnimation = true) {
if (!this._contentIsVisible) return;
return super.requestUpdate(useAnimation);
}
forceUpdate() {
this._hasPendingUpdate = false;
super.forceUpdate();
}
}
export class ExpandableText {
constructor(node, string, limit = 200) {
this._node = node;
this._string = string;
this._delta = limit / 2;
this._start = 0;
this._end = string.length;
this._button = this._createExpandButton();
this.expand();
}
_createExpandButton() {
const button = DOM.element('button');
button.innerText = '...';
button.onclick = (e) => {
e.stopImmediatePropagation();
this.expand(e.shiftKey);
};
button.title = 'Expand text. Use SHIFT-click to show all.'
return button;
}
expand(showAll = false) {
DOM.removeAllChildren(this._node);
this._start = this._start + this._delta;
this._end = this._end - this._delta;
if (this._start >= this._end || showAll) {
this._node.innerText = this._string;
this._button.onclick = undefined;
return;
}
this._node.appendChild(DOM.text(this._string.substring(0, this._start)));
this._node.appendChild(this._button);
this._node.appendChild(
DOM.text(this._string.substring(this._end, this._string.length)));
}
}
export class Chunked {
constructor(iterable, limit) {
this._iterator = iterable[Symbol.iterator]();
this._limit = limit;
}
* next(limit = undefined) {
for (let i = 0; i < (limit ?? this._limit); i++) {
const {value, done} = this._iterator.next();
if (done) {
this._iterator = undefined;
return;
};
yield value;
}
}
get hasMore() {
return this._iterator !== undefined;
}
}
export class LazyTable {
constructor(table, rowData, rowElementCreator, limit = 100) {
this._table = table;
this._chunkedRowData = new Chunked(rowData, limit);
this._rowElementCreator = rowElementCreator;
if (table.tBodies.length == 0) {
table.appendChild(DOM.tbody());
} else {
table.replaceChild(DOM.tbody(), table.tBodies[0]);
}
if (!table.tFoot) this._addFooter();
table.tFoot.addEventListener('click', this._clickHandler);
this._addMoreRows();
}
_addFooter() {
const td = DOM.td();
td.setAttribute('colspan', 100);
for (let addCount of [10, 100, 250, 500]) {
const button = DOM.element('button');
button.innerText = `+${addCount}`;
button.onclick = (e) => this._addMoreRows(addCount);
td.appendChild(button);
}
this._table.appendChild(DOM.element('tfoot'))
.appendChild(DOM.tr())
.appendChild(td);
}
_addMoreRows(count = undefined) {
const fragment = new DocumentFragment();
for (let row of this._chunkedRowData.next(count)) {
const tr = this._rowElementCreator(row);
fragment.appendChild(tr);
}
this._table.tBodies[0].appendChild(fragment);
if (!this._chunkedRowData.hasMore) {
DOM.removeAllChildren(this._table.tFoot);
}
}
}
export function gradientStopsFromGroups(
totalLength, maxHeight, groups, colorFn) {
const kMaxHeight = maxHeight === '%' ? 100 : maxHeight;
const kUnit = maxHeight === '%' ? '%' : 'px';
let increment = 0;
let lastHeight = 0.0;
const stops = [];
for (let group of groups) {
const color = colorFn(group.key);
increment += group.length;
const height = (increment / totalLength * kMaxHeight) | 0;
stops.push(`${color} ${lastHeight}${kUnit} ${height}${kUnit}`)
lastHeight = height;
}
return stops;
}
export class Debouncer {
constructor(callback, timeout = 250) {
this._callback = callback;
this._timeout = timeout;
this._timeoutId = 0;
}
callNow(...args) {
this.clear();
return this._callback(...args);
}
call(...args) {
this.clear();
this._timeoutId = window.setTimeout(this._callback, this._timeout, ...args);
}
clear() {
clearTimeout(this._timeoutId);
}
}
export * from '../helper.mjs';
export * from '../../js/web-api-helper.mjs'

View File

@ -0,0 +1,80 @@
<!-- Copyright 2020 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. -->
<head>
<link href="./index.css" rel="stylesheet">
</head>
<style>
.count {
text-align: right;
width: 5em;
}
.percentage {
text-align: right;
width: 4em;
}
.key {
padding-left: 1em;
}
.drilldown-group-title {
font-weight: bold;
padding: 0.5em 0 0.2em 0;
}
.toggle {
width: 1em;
text-align: left;
cursor: -webkit-zoom-in;
color: rgba(var(--border-color), 1);
user-select: none;
}
.toggle::before {
content: "▶";
}
.open .toggle::before {
content: "▼";
}
.panel {
position: relative;
}
.panelBody {
height: 400px;
}
.scroller {
max-height: 800px;
overflow-y: scroll;
}
.legend {
top: 40px;
}
</style>
<div class="panel">
<input type="checkbox" id="closer" class="panelCloserInput" checked>
<label class="panelCloserLabel" for="closer">▼</label>
<h2 id="title"></h2>
<div class="selection">
<input type="radio" id="show-all" name="selectionType" value="all">
<label for="show-all">All</label>
<input type="radio" id="show-timerange" name="selectionType" value="timerange">
<label for="show-timerange">Time Range</label>
<input type="radio" id="show-selection" name="selectionType" value="selection">
<label for="show-selection">Last Selection</label>
</div>
<select id="group-key"></select>
<div id="content" class="panelBody">
<slot></slot>
<table id="table" width="100%">
</table>
</div>
</div>

View File

@ -0,0 +1,198 @@
// Copyright 2020 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 {App} from '../index.mjs'
import {FocusEvent, ToolTipEvent} from './events.mjs';
import {CollapsableElement, DOM, groupBy, LazyTable} from './helper.mjs';
DOM.defineCustomElement(
'view/list-panel',
(templateText) => class ListPanel extends CollapsableElement {
_selectedLogEntries = [];
_displayedLogEntries = [];
_timeline;
_detailsClickHandler = this._handleDetailsClick.bind(this);
_logEntryClickHandler = this._handleLogEntryClick.bind(this);
_logEntryMouseOverHandler = this._logEntryMouseOverHandler.bind(this);
constructor() {
super(templateText);
this.groupKey.addEventListener('change', e => this.requestUpdate());
this.showAllRadio.onclick = _ => this._showEntries(this._timeline);
this.showTimerangeRadio.onclick = _ =>
this._showEntries(this._timeline.selectionOrSelf);
this.showSelectionRadio.onclick = _ =>
this._showEntries(this._selectedLogEntries);
}
static get observedAttributes() {
return ['title'];
}
attributeChangedCallback(name, oldValue, newValue) {
if (name == 'title') {
this.$('#title').innerHTML = newValue;
}
}
set timeline(timeline) {
console.assert(timeline !== undefined, 'timeline undefined!');
this._timeline = timeline;
this.$('.panel').style.display =
timeline.isEmpty() ? 'none' : 'inherit';
this._initGroupKeySelect();
}
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();
}
}
get entryClass() {
return this._timeline.at(0)?.constructor;
}
get groupKey() {
return this.$('#group-key');
}
get table() {
return this.$('#table');
}
get showAllRadio() {
return this.$('#show-all');
}
get showTimerangeRadio() {
return this.$('#show-timerange');
}
get showSelectionRadio() {
return this.$('#show-selection');
}
get _propertyNames() {
return this.entryClass?.propertyNames ?? [];
}
_initGroupKeySelect() {
const select = this.groupKey;
select.options.length = 0;
for (const propertyName of this._propertyNames) {
const option = DOM.element('option');
option.text = propertyName;
select.add(option);
}
}
_showEntries(entries) {
this._displayedLogEntries = entries;
this.requestUpdate();
}
_update() {
if (this._timeline.isEmpty()) return;
DOM.removeAllChildren(this.table);
if (this._displayedLogEntries.length == 0) return;
const propertyName = this.groupKey.selectedOptions[0].text;
const groups = groupBy(
this._displayedLogEntries, each => each[propertyName], true);
this._render(groups, this.table);
}
createSubgroups(group) {
const map = new Map();
const tempGroups = [];
for (let propertyName of this._propertyNames) {
map.set(
propertyName,
groupBy(group.entries, each => each[propertyName], true));
}
return map;
}
_handleLogEntryClick(e) {
const group = e.currentTarget.group;
this.dispatchEvent(new FocusEvent(group.key));
}
_logEntryMouseOverHandler(e) {
const group = e.currentTarget.group;
this.dispatchEvent(
new ToolTipEvent(group.key, e.currentTarget, e.ctrlKey));
}
_handleDetailsClick(event) {
event.stopPropagation();
const tr = event.target.parentNode;
const group = tr.group;
// Create subgroup in-place if the don't exist yet.
if (tr.groups === undefined) {
const groups = tr.groups = this.createSubgroups(group);
this.renderDrilldown(groups, tr);
}
const detailsTr = tr.nextSibling;
if (tr.classList.contains('open')) {
tr.classList.remove('open');
detailsTr.style.display = 'none';
} else {
tr.classList.add('open');
detailsTr.style.display = 'table-row';
}
}
renderDrilldown(groups, previousSibling) {
const tr = DOM.tr('entry-details');
tr.style.display = 'none';
// indent by one td.
tr.appendChild(DOM.td());
const td = DOM.td();
td.colSpan = 3;
groups.forEach((group, key) => {
this.renderDrilldownGroup(td, group, key);
});
tr.appendChild(td);
// Append the new TR after previousSibling.
previousSibling.parentNode.insertBefore(
tr, previousSibling.nextSibling);
}
renderDrilldownGroup(td, groups, key) {
const div = DOM.div('drilldown-group-title');
div.textContent = `Grouped by ${key}: ${groups[0]?.parentTotal ?? 0}#`;
td.appendChild(div);
const table = DOM.table();
this._render(groups, table, false)
td.appendChild(table);
}
_render(groups, table) {
let last;
new LazyTable(table, groups, group => {
last = group;
const tr = DOM.tr();
tr.group = group;
const details = tr.appendChild(DOM.td('', 'toggle'));
details.onclick = this._detailsClickHandler;
tr.appendChild(DOM.td(`${group.percent.toFixed(2)}%`, 'percentage'));
tr.appendChild(DOM.td(group.length, 'count'));
const valueTd = tr.appendChild(DOM.td(group.key?.toString(), 'key'));
if (App.isClickable(group.key)) {
tr.onclick = this._logEntryClickHandler;
tr.onmouseover = this._logEntryMouseOverHandler;
valueTd.classList.add('clickable');
}
return tr;
}, 10);
}
});

View File

@ -0,0 +1,12 @@
// Copyright 2020 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 {DOM, FileReader} from './helper.mjs';
DOM.defineCustomElement(
'../js/log-file-reader',
(templateText) => class LogFileReader extends FileReader {
constructor() {
super(templateText);
}
});

View File

@ -0,0 +1,36 @@
<!-- Copyright 2020 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. -->
<head>
<link href="./index.css" rel="stylesheet">
</head>
<style>
#searchBarInput {
width: 200px;
}
</style>
<div class="panel">
<input type="checkbox" id="closer" class="panelCloserInput" checked>
<label class="panelCloserLabel" for="closer">▼</label>
<h2>Map Panel</h2>
<div class="selection">
<input type="radio" id="show-all" name="selectionType" value="all">
<label for="show-all">All</label>
<input type="radio" id="show-timerange" name="selectionType" value="timerange">
<label for="show-timerange">Time Range</label>
<input type="radio" id="show-selection" name="selectionType" value="selection">
<label for="show-selection">Last Selection</label>
</div>
<h3>All Transitions</h3>
<map-transitions id="map-transitions"></map-transitions>
<h3>Search Map by Address</h3>
<section id="searchBar"></section>
<input type="search" id="searchBarInput"></input>
<button id="searchBarBtn">Search</button>
<h3>Details Transitions</h3>
<map-transitions id="map-details-transitions"></map-transitions>
<h3>Details</h3>
<map-details id="map-details"></map-details>
</div>

View File

@ -0,0 +1,110 @@
// Copyright 2020 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 './map-panel/map-details.mjs';
import './map-panel/map-transitions.mjs';
import {MapLogEntry} from '../log/map.mjs';
import {FocusEvent} from './events.mjs';
import {CollapsableElement, DOM} from './helper.mjs';
DOM.defineCustomElement(
'view/map-panel',
(templateText) => class MapPanel extends CollapsableElement {
_map;
_timeline;
_selectedLogEntries = [];
_displayedLogEntries = [];
constructor() {
super(templateText);
this.searchBarBtn.addEventListener('click', e => this._handleSearch(e));
this.showAllRadio.onclick = _ => this._showEntries(this._timeline);
this.showTimerangeRadio.onclick = _ =>
this._showEntries(this._timeline.selectionOrSelf);
this.showSelectionRadio.onclick = _ =>
this._showEntries(this._selectedLogEntries);
}
get showAllRadio() {
return this.$('#show-all');
}
get showTimerangeRadio() {
return this.$('#show-timerange');
}
get showSelectionRadio() {
return this.$('#show-selection');
}
get mapTransitionsPanel() {
return this.$('#map-transitions');
}
get mapDetailsTransitionsPanel() {
return this.$('#map-details-transitions');
}
get mapDetailsPanel() {
return this.$('#map-details');
}
get searchBarBtn() {
return this.$('#searchBarBtn');
}
get searchBar() {
return this.$('#searchBar');
}
set timeline(timeline) {
console.assert(timeline !== undefined, 'timeline undefined!');
this._timeline = timeline;
this.$('.panel').style.display =
timeline.isEmpty() ? 'none' : 'inherit';
this.mapTransitionsPanel.timeline = timeline;
this.mapDetailsTransitionsPanel.timeline = timeline;
}
set selectedLogEntries(entries) {
if (entries === this._timeline.selection) {
this.showTimerangeRadio.click();
} else if (entries == this._timeline) {
this.showAllRadio.click();
} else {
this._selectedLogEntries = entries;
this.showSelectionRadio.click();
}
}
set map(map) {
this._map = map;
this.requestUpdate();
}
_showEntries(entries) {
this._displayedLogEntries = entries;
this.requestUpdate();
}
_update() {
this.mapDetailsTransitionsPanel.selectedLogEntries = [this._map];
this.mapDetailsPanel.map = this._map;
this.mapTransitionsPanel.selectedLogEntries = this._displayedLogEntries;
}
_handleSearch(e) {
const searchBar = this.$('#searchBarInput');
const searchBarInput = searchBar.value;
// access the map from model cache
const selectedMap = MapLogEntry.get(searchBarInput);
if (selectedMap) {
searchBar.className = 'success';
this.dispatchEvent(new FocusEvent(selectedMap));
} else {
searchBar.className = 'failure';
}
}
});

View File

@ -0,0 +1,19 @@
<!-- Copyright 2020 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. -->
<head>
<link href="./index.css" rel="stylesheet">
</head>
<style>
#mapDetails,
#filePositionNode {
overflow: scroll;
}
#mapDetails {
font-family: monospace;
white-space: pre;
}
</style>
<section id="mapProperties"></section>
<section id="mapDetails"></section>

View File

@ -0,0 +1,59 @@
// Copyright 2020 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 {FocusEvent} from '../events.mjs';
import {DOM, ExpandableText, V8CustomElement} from '../helper.mjs';
DOM.defineCustomElement(
'./view/map-panel/map-details',
(templateText) => class MapDetails extends V8CustomElement {
_map;
constructor() {
super(templateText);
}
get _mapDetails() {
return this.$('#mapDetails');
}
get _mapProperties() {
return this.$('#mapProperties');
}
set map(map) {
if (this._map === map) return;
this._map = map;
this.requestUpdate();
}
_update() {
this._mapProperties.innerText = '';
if (this._map) {
let clickableDetailsTable = DOM.table('properties');
{
const row = clickableDetailsTable.insertRow();
row.insertCell().innerText = 'ID';
row.insertCell().innerText = `${this._map.id}`;
}
{
const row = clickableDetailsTable.insertRow();
row.insertCell().innerText = 'Source location';
const sourceLocation = row.insertCell();
new ExpandableText(sourceLocation, `${this._map.sourcePosition}`);
sourceLocation.className = 'clickable';
sourceLocation.onclick = e => this._handleSourcePositionClick(e);
}
this._mapProperties.appendChild(clickableDetailsTable);
this._mapDetails.innerText = this._map.description;
} else {
this._mapDetails.innerText = '';
}
}
_handleSourcePositionClick(event) {
this.dispatchEvent(new FocusEvent(this._map.sourcePosition));
}
});

View File

@ -0,0 +1,140 @@
<!-- Copyright 2020 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. -->
<head>
<link href="./index.css" rel="stylesheet">
</head>
<style>
#transitionView {
overflow-x: scroll;
white-space: nowrap;
min-height: 50px;
max-height: 200px;
padding: 50px 0 0 0;
margin-top: -25px;
width: 100%;
}
#transitionView::-webkit-scrollbar {
width: 0;
background-color: transparent;
}
.map {
width: 20px;
height: 20px;
display: inline-block;
border-radius: 50%;
background-color: var(--map-background-color);
border: 4px solid var(--surface-color);
font-size: 10px;
text-align: center;
line-height: 18px;
color: var(--on-surface-color);
vertical-align: top;
margin-top: -13px;
/* raise z-index */
position: relative;
z-index: 2;
cursor: pointer;
}
.map.selected {
border-color: var(--on-surface-color);
}
.transitions {
display: inline-block;
margin-left: -15px;
}
.transition {
min-height: 55px;
margin: 0 0 -2px 2px;
}
/* gray out deprecated transitions */
.deprecated>.transitionEdge,
.deprecated>.map {
opacity: 0.5;
}
.deprecated>.transition {
border-color: rgba(0, 0, 0, 0.5);
}
/* Show a border for all but the first transition */
.transition:nth-of-type(2),
.transition:nth-last-of-type(n+2) {
border-left: 2px solid;
margin-left: 0px;
}
/* special case for 2 transitions */
.transition:nth-last-of-type(1) {
border-left: none;
}
/* topmost transitions are not related */
#transitionView>.transition {
border-left: none;
}
/* topmost transition edge needs initial offset to be aligned */
#transitionView>.transition>.transitionEdge {
margin-left: 13px;
}
.transitionEdge {
height: 2px;
width: 80px;
display: inline-block;
margin: 0 0 2px 0;
background-color: var(--map-background-color);
vertical-align: top;
padding-left: 15px;
}
.transitionLabel {
color: var(--on-surface-color);
transform: rotate(-15deg);
transform-origin: top left;
margin-top: -10px;
font-size: 10px;
white-space: normal;
word-break: break-all;
background-color: var(--surface-color);
}
.showSubtransitions {
width: 0;
height: 0;
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-top: 10px solid var(--map-background-color);
cursor: zoom-in;
margin: 4px 0 0 4px;
}
.showSubtransitions.opened {
border-top: none;
border-bottom: 10px solid var(--map-background-color);
cursor: zoom-out;
}
#tooltip {
position: absolute;
width: 10px;
height: 10px;
background-color: var(--red);
pointer-events: none;
z-index: 100;
display: none;
}
#title {
padding-bottom: 10px;
}
</style>
<section id="transitionView"></section>

View File

@ -0,0 +1,181 @@
// Copyright 2020 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 {FocusEvent, SelectRelatedEvent, ToolTipEvent} from '../events.mjs';
import {CSSColor, DOM, V8CustomElement} from '../helper.mjs';
DOM.defineCustomElement(
'./view/map-panel/map-transitions',
(templateText) => class MapTransitions extends V8CustomElement {
_timeline;
_map;
_edgeToColor = new Map();
_selectedLogEntries;
_displayedMapsInTree;
_toggleSubtreeHandler = this._handleToggleSubtree.bind(this);
_mapClickHandler = this._handleMapClick.bind(this);
_mapDoubleClickHandler = this._handleMapDoubleClick.bind(this);
_mouseoverMapHandler = this._handleMouseoverMap.bind(this);
constructor() {
super(templateText);
this.currentNode = this.transitionView;
}
get transitionView() {
return this.$('#transitionView');
}
set timeline(timeline) {
this._timeline = timeline;
this._edgeToColor.clear();
timeline.getBreakdown().forEach(breakdown => {
this._edgeToColor.set(breakdown.key, CSSColor.at(breakdown.id));
});
}
set selectedLogEntries(list) {
this._selectedLogEntries = list;
this.requestUpdate();
}
_update() {
this.transitionView.style.display = 'none';
DOM.removeAllChildren(this.transitionView);
if (this._selectedLogEntries.length == 0) return;
this._displayedMapsInTree = new Set();
// Limit view to 200 maps for performance reasons.
this._selectedLogEntries.slice(0, 200).forEach(
(map) => this._addMapAndParentTransitions(map));
this._displayedMapsInTree = undefined;
this.transitionView.style.display = '';
}
_addMapAndParentTransitions(map) {
if (map === undefined) return;
if (this._displayedMapsInTree.has(map)) return;
this._displayedMapsInTree.add(map);
this.currentNode = this.transitionView;
let parents = map.getParents();
if (parents.length > 0) {
this._addTransitionTo(parents.pop());
parents.reverse().forEach((each) => this._addTransitionTo(each));
}
let mapNode = this._addSubtransitions(map);
// Mark and show the selected map.
mapNode.classList.add('selected');
if (this.selectedMap == map) {
setTimeout(
() => mapNode.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
inline: 'nearest',
}),
1);
}
}
_addSubtransitions(map) {
let mapNode = this._addTransitionTo(map);
// Draw outgoing linear transition line.
let current = map;
while (current.children.length == 1) {
current = current.children[0].to;
this._addTransitionTo(current);
}
return mapNode;
}
_addTransitionEdge(map) {
let classes = ['transitionEdge'];
let edge = DOM.div(classes);
edge.style.backgroundColor = this._edgeToColor.get(map.edge.type);
let labelNode = DOM.div('transitionLabel');
labelNode.innerText = map.edge.toString();
edge.appendChild(labelNode);
return edge;
}
_addTransitionTo(map) {
// transition[ transitions[ transition[...], transition[...], ...]];
this._displayedMapsInTree?.add(map);
let transition = DOM.div('transition');
if (map.isDeprecated()) transition.classList.add('deprecated');
if (map.edge) {
transition.appendChild(this._addTransitionEdge(map));
}
let mapNode = this._addMapNode(map);
transition.appendChild(mapNode);
let subtree = DOM.div('transitions');
transition.appendChild(subtree);
this.currentNode.appendChild(transition);
this.currentNode = subtree;
return mapNode;
}
_addMapNode(map) {
let node = DOM.div('map');
if (map.edge)
node.style.backgroundColor = this._edgeToColor.get(map.edge.type);
node.map = map;
node.onclick = this._mapClickHandler
node.ondblclick = this._mapDoubleClickHandler
node.onmouseover = this._mouseoverMapHandler
if (map.children.length > 1) {
node.innerText = map.children.length;
const showSubtree = DOM.div('showSubtransitions');
showSubtree.onclick = this._toggleSubtreeHandler
node.appendChild(showSubtree);
}
else if (map.children.length == 0) {
node.innerHTML = '&#x25CF;';
}
this.currentNode.appendChild(node);
return node;
}
_handleMapClick(event) {
const map = event.currentTarget.map;
this.dispatchEvent(new FocusEvent(map));
}
_handleMapDoubleClick(event) {
this.dispatchEvent(new SelectRelatedEvent(event.currentTarget.map));
}
_handleMouseoverMap(event) {
this.dispatchEvent(new ToolTipEvent(
event.currentTarget.map, event.currentTarget, event.ctrlKey));
}
_handleToggleSubtree(event) {
event.stopImmediatePropagation();
const node = event.currentTarget.parentElement;
const map = node.map;
event.target.classList.toggle('opened');
const transitionsNode =
node.parentElement.querySelector('.transitions');
const subtransitionNodes = transitionsNode.children;
if (subtransitionNodes.length <= 1) {
// Add subtransitions except the one that's already shown.
let visibleTransitionMap = subtransitionNodes.length == 1 ?
transitionsNode.querySelector('.map').map :
undefined;
map.children.forEach((edge) => {
if (edge.to != visibleTransitionMap) {
this.currentNode = transitionsNode;
this._addSubtransitions(edge.to);
}
});
} else {
// remove all but the first (currently selected) subtransition
for (let i = subtransitionNodes.length - 1; i > 0; i--) {
transitionsNode.removeChild(subtransitionNodes[i]);
}
}
return false;
}
});

View File

@ -0,0 +1,159 @@
<!-- 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. -->
<head>
<link href="./index.css" rel="stylesheet">
<style>
:host {
--flame-category-width: 40px;
}
.panelBody {
display: flex;
flex-direction: column;
}
#overview {
flex: 0 0 25px;
}
.tableContainer {
position: relative;
overflow-y: scroll;
flex-grow: 1;
}
#flameChart {
--height: 400px;
flex: 0 0 var(--height);
height: var(--height);
width: 100%;
position: relative;
overflow: scroll;
}
#table thead {
position: sticky;
top: 0px;
background-color: var(--surface-color);
}
#flameChart div {
position: absolute;
font-size: 8px;
line-height: 10px;
vertical-align: middle;
}
#flameChartFlames div {
height: 10px;
border: 1px var(--border-color) solid;
font-family: var(--code-font);
color: var(--on-primary-color);
overflow: hidden;
text-align: left;
}
#flameChartFlames div:hover {
border: 1px var(--background-color) solid;
}
#flameChart > div {
box-sizing: border-box;
overflow: visible;
color: var(--on-surface-color);
padding-right: 5px;
text-align: right;
}
#flameChartSelected, #flameChartIn, #flameChartOut {
width: var(--flame-category-width);
}
#flameChartIn {
/* bottom-right align the text */
display: flex;
justify-content: flex-end;
align-items: flex-end;
}
#flameChartFlames {
top: 0xp;
left: var(--flame-category-width);
}
#table .r {
text-align: right;
}
/* SVG */
.fsIn {
background-color: bisque;
}
.fsOut {
background-color: lightblue;
}
.fsMain {
background-color: var(--primary-color);
}
#table .nm {
font-family: var(--code-font);
font-size: var(--code-font-size);
}
#table .aC { /* arrow closed */
transition: transform 0.2s ease-out 0s;
user-select: none;
}
#table .aO { /* arrow opened */
transform: rotate(90deg);
transition: transform 0.2s ease-out 0s;
user-select: none;
}
</style>
</head>
<div class="panel">
<input type="checkbox" id="closer" class="panelCloserInput" checked>
<label class="panelCloserLabel" for="closer">▼</label>
<h2>Profiler</h2>
<div class="selection">
<input type="radio" id="show-all" name="selectionType" value="all" checked >
<label for="show-all">All</label>
<input type="radio" id="show-timerange" name="selectionType" value="timerange">
<label for="show-timerange">Time Range</label>
<input type="radio" id="show-selection" name="selectionType" value="selection">
<label for="show-selection">Last Selection</label>
</div>
<div id="body" class="panelBody">
<timeline-overview id="overview"></timeline-overview>
<div class="tableContainer">
<table id="table">
<thead>
<tr>
<td colspan="2">Self</td>
<td colspan="2">Total</td>
<td></td>
<td>Type</td>
<td>Name</td>
<td>SourcePosition</td>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
<div id="flameChart">
<div id="flameChartIn">IN↧</div>
<div id="flameChartSelected">Pivot</div>
<div id="flameChartOut">OUT↧</div>
<div id="flameChartFlames"></div>
</div>
</div>
</div>

View File

@ -0,0 +1,313 @@
// 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.sfi?.codeEntries.size > 1) {
for (let dynamicCode of mainCode.sfi.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, e.ctrlKey));
}
_handleFlameChartClick(e) {
const profileNode = e.target.data;
if (!profileNode) return;
this._updateOverview(profileNode);
this._updateFlameChart(profileNode)
}
});

View File

@ -0,0 +1,64 @@
<!-- Copyright 2020 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. -->
<head>
<link href="./index.css" rel="stylesheet">
</head>
<style>
h3 {
margin-block-start: 0em;
}
.properties {
overflow: auto;
max-height: 300px;
}
.properties table {
overflow: visible;
min-width: 350px;
border-collapse: collapse;
}
.properties table td {
vertical-align: top;
}
.properties table > tbody > tr > td:nth-child(2n+1):after {
content: ':';
}
.properties table > tbody > tr > td:nth-child(2n+1) {
padding-right: 3px;
}
.properties table > tbody > tr > td:nth-child(2n+2) {
width: 100%;
}
.properties table select {
width: 100%;
}
.code {
font-family: var(--code-font);
font-size: var(--code-font-size);
}
.footer {
margin-top: 10px;
text-align: right;
width: 100%;
display: flex;
align-content: space-between;
}
.footer button {
flex: 1 1 0;
}
</style>
<div id="body">
<div id="content"></div>
</div>

View File

@ -0,0 +1,148 @@
// Copyright 2021 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 {App} from '../index.mjs'
import {FocusEvent, SelectRelatedEvent} from './events.mjs';
import {DOM, entriesEquals, ExpandableText, V8CustomElement} from './helper.mjs';
DOM.defineCustomElement(
'view/property-link-table',
template => class PropertyLinkTable extends V8CustomElement {
_object;
_propertyDict;
_instanceLinkButtons = false;
_showHandler = this._handleShow.bind(this);
_showSourcePositionHandler = this._handleShowSourcePosition.bind(this);
_showRelatedHandler = this._handleShowRelated.bind(this);
_arrayValueSelectHandler = this._handleArrayValueSelect.bind(this);
constructor() {
super(template);
}
set instanceLinkButtons(newValue) {
this._instanceLinkButtons = newValue;
}
set propertyDict(propertyDict) {
if (entriesEquals(this._propertyDict, propertyDict)) return;
if (typeof propertyDict !== 'object') {
throw new Error(
`Invalid property dict, expected object: ${propertyDict}`);
}
this._propertyDict = propertyDict;
this.requestUpdate();
}
_update() {
this._fragment = new DocumentFragment();
this._table = DOM.table();
for (let key in this._propertyDict) {
const value = this._propertyDict[key];
this._addKeyValue(key, value);
}
const tableDiv = DOM.div('properties');
tableDiv.appendChild(this._table);
this._fragment.appendChild(tableDiv);
this._createFooter();
const newContent = DOM.div();
newContent.appendChild(this._fragment);
this.$('#content').replaceWith(newContent);
newContent.id = 'content';
this._fragment = undefined;
}
_addKeyValue(key, value) {
if (key == 'title') {
this._addTitle(value);
return;
}
if (key == '__this__') {
this._object = value;
return;
}
const row = this._table.insertRow();
row.insertCell().innerText = key;
const cell = row.insertCell();
if (value == undefined) return;
if (Array.isArray(value)) {
cell.appendChild(this._addArrayValue(value));
return;
} else if (App.isClickable(value)) {
cell.className = 'clickable';
cell.onclick = this._showHandler;
cell.data = value;
}
if (value.isCode) {
cell.classList.add('code');
}
new ExpandableText(cell, value.toString());
}
_addArrayValue(array) {
if (array.length == 0) {
return DOM.text('empty');
} else if (array.length > 200) {
return DOM.text(`${array.length} items`);
}
const select = DOM.element('select');
select.onchange = this._arrayValueSelectHandler;
for (let value of array) {
const option = DOM.element('option');
option.innerText = value === undefined ? '' : value.toString();
option.data = value;
select.add(option);
}
return select;
}
_addTitle(value) {
const title = DOM.element('h3');
title.innerText = value;
this._fragment.appendChild(title);
}
_createFooter() {
if (this._object === undefined) return;
if (!this._instanceLinkButtons) return;
const footer = DOM.div('footer');
let showButton =
footer.appendChild(DOM.button('🔍 Details', this._showHandler));
showButton.data = this._object;
showButton.title = `Show details for ${this._object}`
if (this._object.sourcePosition) {
let showSourcePositionButton = footer.appendChild(DOM.button(
'📍 Source Position', this._showSourcePositionHandler));
showSourcePositionButton.data = this._object;
showSourcePositionButton.title = 'Open the source position';
}
let showRelatedButton = footer.appendChild(
DOM.button('🕸 Related', this._showRelatedHandler));
showRelatedButton.data = this._object;
showRelatedButton.title = 'Show all related events in all panels';
this._fragment.appendChild(footer);
}
_handleArrayValueSelect(event) {
const logEntry = event.currentTarget.selectedOptions[0].data;
this.dispatchEvent(new FocusEvent(logEntry));
}
_handleShow(event) {
this.dispatchEvent(new FocusEvent(event.currentTarget.data));
}
_handleShowSourcePosition(event) {
this.dispatchEvent(
new FocusEvent(event.currentTarget.data.sourcePosition));
}
_handleShowRelated(event) {
this.dispatchEvent(new SelectRelatedEvent(event.currentTarget.data));
}
});

View File

@ -0,0 +1,75 @@
<!-- Copyright 2020 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. -->
<head>
<link href="./index.css" rel="stylesheet">
</head>
<style>
.scriptNode {
font-family: var(--code-font);
font-size: var(--code-font-size);
}
.scriptNode span {
counter-increment: sourceLineCounter 1;
text-indent: -3.5em;
padding-left: 3.5em;
display: block;
}
.scriptNode span::before {
content: counter(sourceLineCounter) ": ";
width: 3.5em;
display: inline-block;
white-space: pre;
text-align: right;
}
mark {
width: 1ch;
border-radius: 2px;
border: 0.5px var(--background-color) solid;
cursor: pointer;
background-color: var(--primary-color);
color: var(--on-primary-color);
}
.marked {
background-color: var(--secondary-color);
box-shadow: 0px 0px 2px 3px var(--secondary-color);
animation-name: pulse;
animation-duration: 3s;
animation-delay: 500ms;
}
@keyframes pulse {
0% {
box-shadow: 0px 0px 0px 3px var(--secondary-color);
}
5% {
box-shadow: 0px 0px 0px 10px var(--secondary-color);
}
10% {
box-shadow: 0px 0px 0px 0px var(--secondary-color);
}
15% {
box-shadow: 0px 0px 0px 10px var(--secondary-color);
}
20% {
box-shadow: 0px 0px 2px 3px var(--secondary-color);
}
}
</style>
<div class="panel">
<input type="checkbox" id="closer" class="panelCloserInput" checked>
<label class="panelCloserLabel" for="closer">▼</label>
<h2>Source Panel</h2>
<div class="selection">
<select id="script-dropdown"></select>
<button id="selectedRelatedButton">Select Related Events</button>
</div>
<div id="script" class="panelBody">
<div class="scriptNode"></div>
</div>
</div>

View File

@ -0,0 +1,383 @@
// Copyright 2020 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 {App} from '../index.mjs'
import {SelectionEvent, SelectRelatedEvent, ToolTipEvent} from './events.mjs';
import {arrayEquals, CollapsableElement, CSSColor, defer, delay, DOM, formatBytes, gradientStopsFromGroups, groupBy, LazyTable} from './helper.mjs';
// A source mapping proxy for source maps that don't have CORS headers.
// TODO(leszeks): Make this configurable.
const sourceMapFetchPrefix = 'http://localhost:8080/';
DOM.defineCustomElement(
'view/script-panel',
(templateText) => class SourcePanel extends CollapsableElement {
_selectedSourcePositions = [];
_sourcePositionsToMarkNodesPromise = defer();
_scripts = [];
_script;
showToolTipEntriesHandler = this.handleShowToolTipEntries.bind(this);
constructor() {
super(templateText);
this.scriptDropdown.addEventListener(
'change', e => this._handleSelectScript(e));
this.$('#selectedRelatedButton').onclick =
this._handleSelectRelated.bind(this);
}
get script() {
return this.$('#script');
}
get scriptNode() {
return this.$('.scriptNode');
}
set script(script) {
if (this._script === script) return;
this._script = script;
script.ensureSourceMapCalculated(sourceMapFetchPrefix);
this._sourcePositionsToMarkNodesPromise = defer();
this._selectedSourcePositions = this._selectedSourcePositions.filter(
each => each.script === script);
this.requestUpdate();
}
set focusedSourcePositions(sourcePositions) {
this.selectedSourcePositions = sourcePositions;
}
set selectedSourcePositions(sourcePositions) {
if (arrayEquals(this._selectedSourcePositions, sourcePositions)) {
this._focusSelectedMarkers(0);
} else {
this._selectedSourcePositions = sourcePositions;
// TODO: highlight multiple scripts
this.script = sourcePositions[0]?.script;
this._focusSelectedMarkers(100);
}
}
set scripts(scripts) {
this._scripts = scripts;
this._initializeScriptDropdown();
}
get scriptDropdown() {
return this.$('#script-dropdown');
}
_update() {
this._renderSourcePanel();
this._updateScriptDropdownSelection();
}
_initializeScriptDropdown() {
this._scripts.sort((a, b) => a.name?.localeCompare(b.name) ?? 0);
let select = this.scriptDropdown;
select.options.length = 0;
for (const script of this._scripts) {
const option = document.createElement('option');
const size = formatBytes(script.source.length);
option.text = `${script.name} (id=${script.id} size=${size})`;
option.script = script;
select.add(option);
}
}
_updateScriptDropdownSelection() {
this.scriptDropdown.selectedIndex =
this._script ? this._scripts.indexOf(this._script) : -1;
}
async _renderSourcePanel() {
let scriptNode;
const script = this._script;
if (script) {
await delay(1);
if (script != this._script) return;
const builder = new LineBuilder(this, this._script);
scriptNode = await builder.createScriptNode(this._script.startLine);
if (script != this._script) return;
this._sourcePositionsToMarkNodesPromise.resolve(
builder.sourcePositionToMarkers);
} else {
scriptNode = DOM.div();
this._selectedMarkNodes = undefined;
this._sourcePositionsToMarkNodesPromise.resolve(new Map());
}
const oldScriptNode = this.script.childNodes[1];
this.script.replaceChild(scriptNode, oldScriptNode);
}
async _focusSelectedMarkers(delay_ms) {
if (delay_ms) await delay(delay_ms);
const sourcePositionsToMarkNodes =
await this._sourcePositionsToMarkNodesPromise;
// Remove all marked nodes.
for (let markNode of sourcePositionsToMarkNodes.values()) {
markNode.className = '';
}
for (let sourcePosition of this._selectedSourcePositions) {
if (sourcePosition.script !== this._script) continue;
sourcePositionsToMarkNodes.get(sourcePosition).className = 'marked';
}
this._scrollToFirstSourcePosition(sourcePositionsToMarkNodes)
}
_scrollToFirstSourcePosition(sourcePositionsToMarkNodes) {
const sourcePosition = this._selectedSourcePositions.find(
each => each.script === this._script);
if (!sourcePosition) return;
const markNode = sourcePositionsToMarkNodes.get(sourcePosition);
markNode.scrollIntoView(
{behavior: 'smooth', block: 'center', inline: 'center'});
}
_handleSelectScript(e) {
const option =
this.scriptDropdown.options[this.scriptDropdown.selectedIndex];
this.script = option.script;
}
_handleSelectRelated(e) {
if (!this._script) return;
this.dispatchEvent(new SelectRelatedEvent(this._script));
}
setSelectedSourcePositionInternal(sourcePosition) {
this._selectedSourcePositions = [sourcePosition];
console.assert(sourcePosition.script === this._script);
}
handleSourcePositionClick(e) {
const sourcePosition = e.target.sourcePosition;
this.setSelectedSourcePositionInternal(sourcePosition);
this.dispatchEvent(new SelectRelatedEvent(sourcePosition));
}
handleSourcePositionMouseOver(e) {
const sourcePosition = e.target.sourcePosition;
const entries = sourcePosition.entries;
const toolTipContent = DOM.div();
toolTipContent.appendChild(
new ToolTipTableBuilder(this, entries).tableNode);
let sourceMapContent;
switch (this._script.sourceMapState) {
case 'loaded': {
const originalPosition = sourcePosition.originalPosition;
if (originalPosition.source === null) {
sourceMapContent = DOM.element(
'i', {textContent: 'no source mapping for location'});
} else {
sourceMapContent = DOM.element('a', {
href: `${originalPosition.source}`,
target: '_blank',
textContent: `${originalPosition.source}:${
originalPosition.line}:${originalPosition.column}`
});
}
break;
}
case 'loading':
sourceMapContent =
DOM.element('i', {textContent: 'source map still loading...'});
break;
case 'failed':
sourceMapContent =
DOM.element('i', {textContent: 'source map failed to load'});
break;
case 'none':
sourceMapContent = DOM.element('i', {textContent: 'no source map'});
break;
default:
break;
}
toolTipContent.appendChild(sourceMapContent);
this.dispatchEvent(
new ToolTipEvent(toolTipContent, e.target, e.ctrlKey));
}
handleShowToolTipEntries(event) {
let entries = event.currentTarget.data;
const sourcePosition = entries[0].sourcePosition;
// Add a source position entry so the current position stays focused.
this.setSelectedSourcePositionInternal(sourcePosition);
entries = entries.concat(this._selectedSourcePositions);
this.dispatchEvent(new SelectionEvent(entries));
}
});
class ToolTipTableBuilder {
constructor(scriptPanel, entries) {
this._scriptPanel = scriptPanel;
this.tableNode = DOM.table();
const tr = DOM.tr();
tr.appendChild(DOM.td('Type'));
tr.appendChild(DOM.td('Subtype'));
tr.appendChild(DOM.td('Count'));
this.tableNode.appendChild(document.createElement('thead')).appendChild(tr);
groupBy(entries, each => each.constructor, true).forEach(group => {
this.addRow(group.key.name, 'all', entries, false)
groupBy(group.entries, each => each.type, true).forEach(group => {
this.addRow('', group.key, group.entries, false)
})
})
}
addRow(name, subtypeName, entries) {
const tr = DOM.tr();
tr.appendChild(DOM.td(name));
tr.appendChild(DOM.td(subtypeName));
tr.appendChild(DOM.td(entries.length));
const button =
DOM.button('🔎', this._scriptPanel.showToolTipEntriesHandler);
button.title = `Show all ${entries.length} ${name || subtypeName} entries.`
button.data = entries;
tr.appendChild(DOM.td(button));
this.tableNode.appendChild(tr);
}
}
class SourcePositionIterator {
_entries;
_index = 0;
constructor(sourcePositions) {
this._entries = sourcePositions;
}
* forLine(lineIndex) {
this._findStart(lineIndex);
while (!this._done() && this._current().line === lineIndex) {
yield this._current();
this._next();
}
}
_findStart(lineIndex) {
while (!this._done() && this._current().line < lineIndex) {
this._next();
}
}
_current() {
return this._entries[this._index];
}
_done() {
return this._index >= this._entries.length;
}
_next() {
this._index++;
}
}
function* lineIterator(source, startLine) {
let current = 0;
let line = startLine;
while (current < source.length) {
const next = source.indexOf('\n', current);
if (next === -1) break;
yield [line, source.substring(current, next)];
line++;
current = next + 1;
}
if (current < source.length) yield [line, source.substring(current)];
}
class LineBuilder {
static _colorMap = (function() {
const map = new Map();
let i = 0;
for (let type of App.allEventTypes) {
map.set(type, CSSColor.at(i++));
}
return map;
})();
static get colorMap() {
return this._colorMap;
}
_script;
_clickHandler;
_mouseoverHandler;
_sourcePositionToMarkers = new Map();
constructor(panel, script) {
this._script = script;
this._clickHandler = panel.handleSourcePositionClick.bind(panel);
this._mouseoverHandler = panel.handleSourcePositionMouseOver.bind(panel);
}
get sourcePositionToMarkers() {
return this._sourcePositionToMarkers;
}
async createScriptNode(startLine) {
const scriptNode = DOM.div('scriptNode');
// TODO: sort on script finalization.
this._script.sourcePositions.sort((a, b) => {
if (a.line === b.line) return a.column - b.column;
return a.line - b.line;
});
const sourcePositionsIterator =
new SourcePositionIterator(this._script.sourcePositions);
scriptNode.style.counterReset = `sourceLineCounter ${startLine - 1}`;
for (let [lineIndex, line] of lineIterator(
this._script.source, startLine)) {
scriptNode.appendChild(
this._createLineNode(sourcePositionsIterator, lineIndex, line));
}
if (this._script.sourcePositions.length !=
this._sourcePositionToMarkers.size) {
console.error('Not all SourcePositions were processed.');
}
return scriptNode;
}
_createLineNode(sourcePositionsIterator, lineIndex, line) {
const lineNode = DOM.span();
let columnIndex = 0;
for (const sourcePosition of sourcePositionsIterator.forLine(lineIndex)) {
const nextColumnIndex = sourcePosition.column - 1;
lineNode.appendChild(document.createTextNode(
line.substring(columnIndex, nextColumnIndex)));
columnIndex = nextColumnIndex;
lineNode.appendChild(
this._createMarkerNode(line[columnIndex], sourcePosition));
columnIndex++;
}
lineNode.appendChild(
document.createTextNode(line.substring(columnIndex) + '\n'));
return lineNode;
}
_createMarkerNode(text, sourcePosition) {
const marker = document.createElement('mark');
this._sourcePositionToMarkers.set(sourcePosition, marker);
marker.textContent = text;
marker.sourcePosition = sourcePosition;
marker.onclick = this._clickHandler;
marker.onmouseover = this._mouseoverHandler;
const entries = sourcePosition.entries;
const groups = groupBy(entries, entry => entry.constructor);
if (groups.length > 1) {
const stops = gradientStopsFromGroups(
entries.length, '%', groups, type => LineBuilder.colorMap.get(type));
marker.style.backgroundImage = `linear-gradient(0deg,${stops.join(',')})`
} else {
marker.style.backgroundColor = LineBuilder.colorMap.get(groups[0].key)
}
return marker;
}
}

View File

@ -0,0 +1,45 @@
<!-- Copyright 2020 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. -->
<head>
<link href="./index.css" rel="stylesheet">
<style>
.panel {
padding-bottom: 0px;
position: relative;
}
.titleBackground {
position: absolute;
left: 0px;
top: 34px;
border-radius: 0 0 0 7px;
height: calc(100% - 34px);
width: 30px;
background-color: var(--border-color);
}
.titleButtons {
float: right;
}
.titleButtons button {
border-radius: 20px;
width: 20px;
height: 20px;
font-weight: bold;
line-height: 11px;
}
</style>
</head>
<div class="panel">
<h2>
Timeline Panel
<div class="titleButtons">
<button id="zoomIn" title="Increase resolution">+</button>
<button id="zoomOut" title="Decrease resolution">–</button>
</div>
</h2>
<div class="titleBackground"></div>
<div>
<slot></slot>
</div>
</div>

View File

@ -0,0 +1,80 @@
// Copyright 2020 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 './timeline/timeline-track.mjs';
import './timeline/timeline-track-map.mjs';
import './timeline/timeline-track-tick.mjs';
import './timeline/timeline-track-timer.mjs';
import {SynchronizeSelectionEvent} from './events.mjs';
import {DOM, V8CustomElement} from './helper.mjs';
DOM.defineCustomElement(
'view/timeline-panel',
(templateText) => class TimelinePanel extends V8CustomElement {
constructor() {
super(templateText);
this.addEventListener('scrolltrack', e => this.handleTrackScroll(e));
this.addEventListener(
SynchronizeSelectionEvent.name,
e => this.handleSelectionSyncronization(e));
this.$('#zoomIn').onclick = () => this.nofChunks *= 1.5;
this.$('#zoomOut').onclick = () => this.nofChunks /= 1.5;
}
set nofChunks(count) {
for (const track of this.timelineTracks) {
track.nofChunks = count;
}
}
get nofChunks() {
return this.timelineTracks[0].nofChunks;
}
set currentTime(time) {
for (const track of this.timelineTracks) {
track.currentTime = time;
}
}
get currentTime() {
return this.timelineTracks[0].currentTime;
}
get timelineTracks() {
return this.$('slot').assignedNodes().filter(
node => node.nodeType === Node.ELEMENT_NODE);
}
handleTrackScroll(event) {
for (const track of this.timelineTracks) {
track.scrollLeft = event.detail;
}
}
handleSelectionSyncronization(event) {
this.timeSelection = {start: event.start, end: event.end};
}
set timeSelection(selection) {
if (selection.start > selection.end) {
throw new Error('Invalid time range');
}
const tracks = Array.from(this.timelineTracks);
if (selection.zoom) {
// To avoid inconsistencies copy the zoom/nofChunks from the first
// track
const firstTrack = tracks.pop();
firstTrack.timeSelection = selection;
selection.zoom = false;
for (const track of tracks) track.timeSelection = selection;
this.nofChunks = firstTrack.nofChunks;
} else {
for (const track of this.timelineTracks) {
track.timeSelection = selection;
}
}
}
});

View File

@ -0,0 +1,73 @@
<!-- 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. -->
<head>
<link href="./index.css" rel="stylesheet">
</head>
<style>
:host {
--selection-height: 5px;
--total-height: 50px;
}
#svg {
width: 100%;
height: var(--total-height);
position: sticky;
top: 0px;
}
.marker {
width: 2px;
y: var(--selection-height);
height: calc(var(--total-height) - var(--selection-height));
}
#indicator {
stroke: var(--on-surface-color);
}
.continuousTrack {
transform: translate(0, var(--selection-height));
}
#filler {
height: var(--total-height);
}
#selection {
height: var(--total-height);
}
#selection rect {
height: var(--selection-height);
fill: var(--selection-color);
}
#selection .top {
y: 0px;
}
#selection .bottom {
y: calc(var(--total-height) - var(--selection-height));
}
#selection line {
stroke: var(--on-surface-color);
}
</style>
<svg id="svg" viewBox="0 1 800 50" preserveAspectRatio=none>
<defs>
<pattern id="pattern1" patternUnits="userSpaceOnUse" width="4" height="4">
<path d="M-1,1 l2,-2
M0,4 l4,-4
M3,5 l2,-2" stroke="white"/>
</pattern>
<mask id="mask1">
<rect width="800" height="40" fill="url(#pattern1)" />
</mask>
</defs>
<rect id="filler" width="800" fill-opacity="0"/>
<g id="content"></g>
<svg id="selection">
<line x1="0%" y1="0" x2="0%" y2="50" />
<rect class="top" x="0%" width="100%"></rect>
<rect class="bottom" x="0%" width="100%"></rect>
<line x1="100%" y1="0" x2="100%" y2="50" />
</svg>
<line id="indicator" x1="0" y1="0" x2="0" y2="50" />
</svg>

View File

@ -0,0 +1,279 @@
// 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 {Timeline} from '../../timeline.mjs';
import {ToolTipEvent} from '../events.mjs';
import {arrayEquals, CSSColor, DOM, formatDurationMicros, SVG, V8CustomElement} from '../helper.mjs';
/** @template T */
export class Track {
static continuous(array, color) {
return new this(array, color, true);
}
static discrete(array, color) {
return new this(array, color, false);
}
/** @param {T[]} logEntries */
constructor(logEntries, color, isContinuous) {
/** @type {Set<T>} */
this.logEntries = new Set(logEntries);
this.color = color;
/** @type {bool} */
this.isContinuous = isContinuous;
}
isEmpty() {
return this.logEntries.size == 0;
}
/** @param {T} logEntry */
hasEntry(logEntry) {
return this.logEntries.has(logEntry);
}
static compare(left, right) {
return left.equals(right);
}
equals(other) {
if (!arrayEquals(
Array.from(this.logEntries), Array.from(other.logEntries))) {
return false;
}
if (this.color != other.color) return false;
if (this.isContinuous != other.isContinuous) return false;
return true;
}
}
const kHorizontalPixels = 800;
const kMarginHeight = 5;
const kHeight = 40;
DOM.defineCustomElement(
'view/timeline/timeline-overview',
(templateText) =>
/** @template T */
class TimelineOverview extends V8CustomElement {
/** @type {Timeline<T>} */
_timeline;
/** @type {Track[]} */
_tracks = [];
_timeToPixel = 1;
/** @type {{entry:T, track:Track} => number} */
_countCallback = (entry, track) => track.hasEntry(entry);
constructor() {
super(templateText);
this._indicatorNode = this.$('#indicator');
this._selectionNode = this.$('#selection');
this._contentNode = this.$('#content');
this._svgNode = this.$('#svg');
this._svgNode.onmousemove = this._handleMouseMove.bind(this);
}
/**
* @param {Timeline<T>} timeline
*/
set timeline(timeline) {
this._timeline = timeline;
this._timeToPixel = kHorizontalPixels / this._timeline.duration();
}
/**
* @param {Track[]} tracks
*/
set tracks(tracks) {
// TODO(cbruni): Allow updating the selection time-range independently
// from the data. if (arrayEquals(this._tracks, tracks, Track.compare))
// return;
this._tracks = tracks;
this.requestUpdate();
}
/** @param {{entry:T, track:Track} => number} callback*/
set countCallback(callback) {
this._countCallback = callback;
}
_handleMouseMove(e) {
const externalPixelToTime = this._timeline.duration() /
this._svgNode.getBoundingClientRect().width;
const timeToInternalPixel =
kHorizontalPixels / this._timeline.duration();
const xPos = e.offsetX;
const timeMicros = xPos * externalPixelToTime;
const maxTimeDistance = 2 * externalPixelToTime;
this._setIndicatorPosition(timeMicros * timeToInternalPixel);
let toolTipContent =
this._findLogEntryAtTime(timeMicros, maxTimeDistance);
if (!toolTipContent) {
toolTipContent = `Time ${formatDurationMicros(timeMicros)}`;
}
this.dispatchEvent(
new ToolTipEvent(toolTipContent, this._indicatorNode, e.ctrlKey));
}
_findLogEntryAtTime(time, maxTimeDistance) {
const minTime = time - maxTimeDistance;
const maxTime = time + maxTimeDistance;
for (let track of this._tracks) {
for (let entry of track.logEntries) {
if (minTime <= entry.time && entry.time <= maxTime) return entry;
}
}
}
_setIndicatorPosition(x) {
this._indicatorNode.setAttribute('x1', x);
this._indicatorNode.setAttribute('x2', x);
}
_update() {
const fragment = new DocumentFragment();
this._tracks.forEach((track, index) => {
if (!track.isEmpty()) {
fragment.appendChild(this._renderTrack(track, index));
}
});
DOM.removeAllChildren(this._contentNode);
this._contentNode.appendChild(fragment);
this._setIndicatorPosition(-10);
this._updateSelection();
}
_renderTrack(track, index) {
if (track.isContinuous)
return this._renderContinuousTrack(track, index);
return this._renderDiscreteTrack(track, index);
}
_renderContinuousTrack(track, index) {
const freq = new Frequency(this._timeline);
freq.collect(track, this._countCallback);
const path = SVG.path('continuousTrack');
path.setAttribute('d', freq.toSVG(kHeight));
path.setAttribute('fill', track.color);
if (index != 0) path.setAttribute('mask', `url(#mask${index})`)
return path;
}
_renderDiscreteTrack(track) {
const group = SVG.g();
for (let entry of track.logEntries) {
const x = entry.time * this._timeToPixel;
const kWidth = 2;
const rect = SVG.rect('marker');
rect.setAttribute('x', x - (kWidth / 2));
rect.setAttribute('fill', track.color);
rect.data = entry;
group.appendChild(rect);
}
return group;
}
_updateSelection() {
let startTime = -10;
let duration = 0;
if (this._timeline) {
startTime = this._timeline.selectionOrSelf.startTime;
duration = this._timeline.selectionOrSelf.duration();
}
this._selectionNode.setAttribute('x', startTime * this._timeToPixel);
this._selectionNode.setAttribute('width', duration * this._timeToPixel);
}
});
const kernel = smoothingKernel(50);
function smoothingKernel(size) {
const kernel = new Float32Array(size);
const mid = (size - 1) / 2;
const stddev = size / 10;
for (let i = mid; i < size; i++) {
const x = i - (mid | 0);
const value =
Math.exp(-(x ** 2 / 2 / stddev)) / (stddev * Math.sqrt(2 * Math.PI));
kernel[Math.ceil(x + mid)] = value;
kernel[Math.floor(mid - x)] = value;
}
return kernel;
}
class Frequency {
constructor(timeline) {
this._size = kHorizontalPixels;
this._timeline = timeline;
this._data = new Int16Array(this._size + kernel.length);
this._max = undefined;
this._smoothenedData = undefined;
}
collect(track, sumFn) {
const kernelRadius = (kernel.length / 2) | 0;
let count = 0;
let dataIndex = kernelRadius;
const timeDelta = this._timeline.duration() / this._size;
let nextTime = this._timeline.startTime + timeDelta;
const values = this._timeline.values;
const length = values.length;
let i = 0;
while (i < length && dataIndex < this._data.length) {
const tick = values[i];
if (tick.time < nextTime) {
count += sumFn(tick, track);
i++;
} else {
this._data[dataIndex] = count;
nextTime += timeDelta;
dataIndex++;
count = 0;
}
}
this._data[dataIndex] = count;
}
max() {
if (this._max !== undefined) return this._max;
let max = 0;
this._smoothenedData = new Float32Array(this._size);
for (let start = 0; start < this._size; start++) {
let value = 0;
for (let i = 0; i < kernel.length; i++) {
value += this._data[start + i] * kernel[i];
}
this._smoothenedData[start] = value;
max = Math.max(max, value);
}
this._max = max;
return this._max;
}
toSVG(height) {
const vScale = height / this.max();
const initialY = height;
const buffer = [
'M 0',
initialY,
];
let prevY = initialY;
let usedPrevY = false;
for (let i = 0; i < this._size; i++) {
const y = height - (this._smoothenedData[i] * vScale) | 0;
if (y == prevY) {
usedPrevY = false;
continue;
}
if (!usedPrevY) buffer.push('L', i - 1, prevY);
buffer.push('L', i, y);
prevY = y;
usedPrevY = true;
}
if (!usedPrevY) buffer.push('L', this._size - 1, prevY);
buffer.push('L', this._size - 1, initialY);
buffer.push('Z');
return buffer.join(' ');
}
}

View File

@ -0,0 +1,735 @@
// Copyright 2020 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 {kChunkHeight, kChunkVisualWidth, kChunkWidth} from '../../log/map.mjs';
import {SelectionEvent, SelectTimeEvent, SynchronizeSelectionEvent, ToolTipEvent,} from '../events.mjs';
import {CSSColor, delay, DOM, formatDurationMicros, V8CustomElement} from '../helper.mjs';
export const kTimelineHeight = 200;
export class TimelineTrackBase extends V8CustomElement {
_timeline;
_nofChunks = 500;
_chunks = [];
_selectedEntry;
_focusedEntry;
_timeToPixel;
_timeStartPixelOffset;
_legend;
_lastContentWidth = 0;
_cachedTimelineBoundingClientRect;
_cachedTimelineScrollLeft;
constructor(templateText) {
super(templateText);
this._selectionHandler = new SelectionHandler(this);
this._legend = new Legend(this.$('#legendTable'));
this.timelineChunks = this.$('#timelineChunks');
this.timelineSamples = this.$('#timelineSamples');
this.timelineNode = this.$('#timeline');
this._toolTipTargetNode = undefined;
this.hitPanelNode = this.$('#hitPanel');
this.timelineAnnotationsNode = this.$('#timelineAnnotations');
this.timelineMarkersNode = this.$('#timelineMarkers');
this._scalableContentNode = this.$('#scalableContent');
this.isLocked = false;
this.setAttribute('tabindex', 0);
}
_initEventListeners() {
this._legend.onFilter = this._handleFilterTimeline.bind(this);
this.timelineNode.addEventListener(
'scroll', this._handleTimelineScroll.bind(this));
this.hitPanelNode.onclick = this._handleClick.bind(this);
this.hitPanelNode.ondblclick = this._handleDoubleClick.bind(this);
this.hitPanelNode.onmousemove = this._handleMouseMove.bind(this);
this.$('#selectionForeground')
.addEventListener('mousemove', this._handleMouseMove.bind(this));
window.addEventListener('resize', () => this._resetCachedDimensions());
}
static get observedAttributes() {
return ['title'];
}
attributeChangedCallback(name, oldValue, newValue) {
if (name == 'title') {
this.$('#title').innerHTML = newValue;
}
}
_handleFilterTimeline(type) {
this._updateChunks();
this._legend.update(true);
}
set data(timeline) {
console.assert(timeline);
if (!this._timeline) this._initEventListeners();
this._timeline = timeline;
this._legend.timeline = timeline;
this.$('.content').style.display = timeline.isEmpty() ? 'none' : 'relative';
this._updateChunks();
}
set timeSelection({start, end, focus = false, zoom = false}) {
this._selectionHandler.timeSelection = {start, end};
this.updateSelection();
if (focus || zoom) {
if (!Number.isFinite(start) || !Number.isFinite(end)) {
throw new Error('Invalid number ranges');
}
if (focus) {
this.currentTime = (start + end) / 2;
}
if (zoom) {
const margin = 0.2;
const newVisibleTime = (end - start) * (1 + 2 * margin);
const currentVisibleTime =
this._cachedTimelineBoundingClientRect.width / this._timeToPixel;
this.nofChunks = this.nofChunks * (currentVisibleTime / newVisibleTime);
}
}
}
updateSelection() {
this._selectionHandler.update();
this._legend.update();
}
get _timelineBoundingClientRect() {
if (this._cachedTimelineBoundingClientRect === undefined) {
this._cachedTimelineBoundingClientRect =
this.timelineNode.getBoundingClientRect();
}
return this._cachedTimelineBoundingClientRect;
}
get _timelineScrollLeft() {
if (this._cachedTimelineScrollLeft === undefined) {
this._cachedTimelineScrollLeft = this.timelineNode.scrollLeft;
}
return this._cachedTimelineScrollLeft;
}
_resetCachedDimensions() {
this._cachedTimelineBoundingClientRect = undefined;
this._cachedTimelineScrollLeft = undefined;
}
// Maps the clicked x position to the x position on timeline
positionOnTimeline(pagePosX) {
let rect = this._timelineBoundingClientRect;
let posClickedX = pagePosX - rect.left + this._timelineScrollLeft;
return posClickedX;
}
positionToTime(pagePosX) {
return this.relativePositionToTime(this.positionOnTimeline(pagePosX));
}
relativePositionToTime(timelineRelativeX) {
const timelineAbsoluteX = timelineRelativeX + this._timeStartPixelOffset;
return (timelineAbsoluteX / this._timeToPixel) | 0;
}
timeToPosition(time) {
let relativePosX = time * this._timeToPixel;
relativePosX -= this._timeStartPixelOffset;
return relativePosX;
}
set nofChunks(count) {
const centerTime = this.currentTime;
const kMinNofChunks = 100;
if (count < kMinNofChunks) count = kMinNofChunks;
const kMaxNofChunks = 10 * 1000;
if (count > kMaxNofChunks) count = kMaxNofChunks;
this._nofChunks = count | 0;
this._updateChunks();
this.currentTime = centerTime;
}
get nofChunks() {
return this._nofChunks;
}
_updateChunks() {
this._chunks = undefined;
this._updateDimensions();
this.requestUpdate();
}
get chunks() {
if (this._chunks?.length != this.nofChunks) {
this._chunks =
this._timeline.chunks(this.nofChunks, this._legend.filterPredicate);
console.assert(this._chunks.length == this._nofChunks);
}
return this._chunks;
}
set selectedEntry(value) {
this._selectedEntry = value;
}
get selectedEntry() {
return this._selectedEntry;
}
get focusedEntry() {
return this._focusedEntry;
}
set focusedEntry(entry) {
this._focusedEntry = entry;
if (entry) this._drawAnnotations(entry);
}
set scrollLeft(offset) {
this.timelineNode.scrollLeft = offset;
this._cachedTimelineScrollLeft = offset;
}
get scrollLeft() {
return this._cachedTimelineScrollLeft;
}
set currentTime(time) {
const position = this.timeToPosition(time);
const centerOffset = this._timelineBoundingClientRect.width / 2;
this.scrollLeft = Math.max(0, position - centerOffset);
}
get currentTime() {
const centerOffset =
this._timelineBoundingClientRect.width / 2 + this.scrollLeft;
return this.relativePositionToTime(centerOffset);
}
handleEntryTypeDoubleClick(e) {
this.dispatchEvent(new SelectionEvent(e.target.parentNode.entries));
}
timelineIndicatorMove(offset) {
this.timelineNode.scrollLeft += offset;
this._cachedTimelineScrollLeft = undefined;
}
_handleTimelineScroll(e) {
let scrollLeft = e.currentTarget.scrollLeft;
this._cachedTimelineScrollLeft = scrollLeft;
this.dispatchEvent(new CustomEvent(
'scrolltrack', {bubbles: true, composed: true, detail: scrollLeft}));
}
_updateDimensions() {
// No data in this timeline, no need to resize
if (!this._timeline) return;
const centerOffset = this._timelineBoundingClientRect.width / 2;
const time =
this.relativePositionToTime(this._timelineScrollLeft + centerOffset);
const start = this._timeline.startTime;
const width = this._nofChunks * kChunkWidth;
this._lastContentWidth = parseInt(this.timelineMarkersNode.style.width);
this._timeToPixel = width / this._timeline.duration();
this._timeStartPixelOffset = start * this._timeToPixel;
this.$('#cropper').style.width = `${width}px`;
this.timelineMarkersNode.style.width = `${width}px`;
this.timelineAnnotationsNode.style.width = `${width}px`;
this.hitPanelNode.style.width = `${width}px`;
const ratio = this._scaleContent(width) || 1;
this.timelineChunks.style.width = `${width / Math.min(1, ratio)}px`;
this._drawMarkers();
this._selectionHandler.update();
this._cachedTimelineScrollLeft = this.timelineNode.scrollLeft =
this.timeToPosition(time) - centerOffset;
}
_scaleContent(currentWidth) {
if (!this._lastContentWidth) return 1;
const ratio = currentWidth / this._lastContentWidth;
this._scalableContentNode.style.transform = `scale(${ratio}, 1)`;
return ratio;
}
_adjustHeight(height) {
const dataHeight = Math.max(height, 200);
const viewHeight = Math.min(dataHeight, 400);
this.style.setProperty('--data-height', dataHeight + 'px');
this.style.setProperty('--view-height', viewHeight + 'px');
this.timelineNode.style.overflowY =
(height > kTimelineHeight) ? 'scroll' : 'hidden';
}
_update() {
this._legend.update();
this._drawContent().then(() => this._drawAnnotations(this.selectedEntry));
this._resetCachedDimensions();
}
async _drawContent() {
if (this._timeline.isEmpty()) return;
await delay(5);
const chunks = this.chunks;
const max = chunks.max(each => each.size());
let buffer = '';
for (let i = 0; i < chunks.length; i++) {
const chunk = chunks[i];
const height = (chunk.size() / max * kChunkHeight);
chunk.height = height;
if (chunk.isEmpty()) continue;
buffer += '<g>';
buffer += this._drawChunk(i, chunk);
buffer += '</g>'
}
this._scalableContentNode.innerHTML = buffer;
this._scalableContentNode.style.transform = 'scale(1, 1)';
}
_drawChunk(chunkIndex, chunk) {
const groups = chunk.getBreakdown(event => event.type);
let buffer = '';
const kHeight = chunk.height;
let lastHeight = kTimelineHeight;
for (let i = 0; i < groups.length; i++) {
const group = groups[i];
if (group.length == 0) break;
const height = (group.length / chunk.size() * kHeight) | 0;
lastHeight -= height;
const color = this._legend.colorForType(group.key);
buffer += `<rect x=${chunkIndex * kChunkWidth} y=${lastHeight} height=${
height} width=${kChunkVisualWidth} fill=${color} />`
}
return buffer;
}
_drawMarkers() {
// Put a time marker roughly every 20 chunks.
const expected = this._timeline.duration() / this._nofChunks * 20;
let interval = (10 ** Math.floor(Math.log10(expected)));
let correction = Math.log10(expected / interval);
correction = (correction < 0.33) ? 1 : (correction < 0.75) ? 2.5 : 5;
interval *= correction;
const start = this._timeline.startTime;
let time = start;
let buffer = '';
while (time < this._timeline.endTime) {
const delta = time - start;
const text = `${(delta / 1000) | 0} ms`;
const x = (delta * this._timeToPixel) | 0;
buffer += `<text x=${x - 2} y=0 class=markerText >${text}</text>`
buffer +=
`<line x1=${x} x2=${x} y1=12 y2=2000 dy=100% class=markerLine />`
time += interval;
}
this.timelineMarkersNode.innerHTML = buffer;
}
_drawAnnotations(logEntry, time) {
if (!this._focusedEntry) return;
this._drawEntryMark(this._focusedEntry);
}
_drawEntryMark(entry) {
const [x, y] = this._positionForEntry(entry);
const color = this._legend.colorForType(entry.type);
const mark =
`<circle cx=${x} cy=${y} r=3 stroke=${color} class=annotationPoint />`;
this.timelineAnnotationsNode.innerHTML = mark;
}
_handleUnlockedMouseEvent(event) {
this._focusedEntry = this._getEntryForEvent(event);
if (!this._focusedEntry) return false;
this._updateToolTip(event);
const time = this.positionToTime(event.pageX);
this._drawAnnotations(this._focusedEntry, time);
}
_updateToolTip(event) {
if (!this._focusedEntry) return false;
this.dispatchEvent(new ToolTipEvent(
this._focusedEntry, this._toolTipTargetNode, event.ctrlKey));
event.stopImmediatePropagation();
}
_handleClick(event) {
if (event.button !== 0) return;
if (event.target === this.timelineChunks) return;
this.isLocked = !this.isLocked;
// Do this unconditionally since we want the tooltip to be update to the
// latest locked state.
this._handleUnlockedMouseEvent(event);
return false;
}
_handleDoubleClick(event) {
if (event.button !== 0) return;
this._selectionHandler.clearSelection();
const time = this.positionToTime(event.pageX);
const chunk = this._getChunkForEvent(event)
if (!chunk) return;
event.stopImmediatePropagation();
this.dispatchEvent(new SelectTimeEvent(chunk.start, chunk.end));
return false;
}
_handleMouseMove(event) {
if (event.button !== 0) return;
if (this._selectionHandler.isSelecting) return false;
if (this.isLocked && this._focusedEntry) {
this._updateToolTip(event);
return false;
}
this._handleUnlockedMouseEvent(event);
}
_getChunkForEvent(event) {
const time = this.positionToTime(event.pageX);
return this._chunkForTime(time);
}
_chunkForTime(time) {
const chunkIndex = ((time - this._timeline.startTime) /
this._timeline.duration() * this._nofChunks) |
0;
return this.chunks[chunkIndex];
}
_positionForEntry(entry) {
const chunk = this._chunkForTime(entry.time);
if (chunk === undefined) return [-1, -1];
const xFrom = (chunk.index * kChunkWidth + kChunkVisualWidth / 2) | 0;
const yFrom = kTimelineHeight - chunk.yOffset(entry) | 0;
return [xFrom, yFrom];
}
_getEntryForEvent(event) {
const chunk = this._getChunkForEvent(event);
if (chunk?.isEmpty() ?? true) return false;
const relativeIndex = Math.round(
(kTimelineHeight - event.layerY) / chunk.height * (chunk.size() - 1));
if (relativeIndex > chunk.size()) return false;
const logEntry = chunk.at(relativeIndex);
const node = this.getToolTipTargetNode(logEntry);
if (!node) return logEntry;
const style = node.style;
style.left = `${chunk.index * kChunkWidth}px`;
style.top = `${kTimelineHeight - chunk.height}px`;
style.height = `${chunk.height}px`;
style.width = `${kChunkVisualWidth}px`;
return logEntry;
}
getToolTipTargetNode(logEntry) {
let node = this._toolTipTargetNode;
if (node) {
if (node.logEntry === logEntry) return undefined;
node.parentNode.removeChild(node);
}
node = this._toolTipTargetNode = DOM.div('toolTipTarget');
node.logEntry = logEntry;
this.$('#cropper').appendChild(node);
return node;
}
};
class SelectionHandler {
// TODO turn into static field once Safari supports it.
static get SELECTION_OFFSET() {
return 10
};
_timeSelection = {start: -1, end: Infinity};
_selectionOriginTime = -1;
constructor(timeline) {
this._timeline = timeline;
this._timelineNode = this._timeline.$('#timeline');
this._timelineNode.addEventListener(
'mousedown', this._handleMouseDown.bind(this));
this._timelineNode.addEventListener(
'mouseup', this._handleMouseUp.bind(this));
this._timelineNode.addEventListener(
'mousemove', this._handleMouseMove.bind(this));
this._selectionNode = this._timeline.$('#selection');
this._selectionForegroundNode = this._timeline.$('#selectionForeground');
this._selectionForegroundNode.addEventListener(
'dblclick', this._handleDoubleClick.bind(this));
this._selectionBackgroundNode = this._timeline.$('#selectionBackground');
this._leftHandleNode = this._timeline.$('#leftHandle');
this._rightHandleNode = this._timeline.$('#rightHandle');
}
update() {
if (!this.hasSelection) {
this._selectionNode.style.display = 'none';
return;
}
this._selectionNode.style.display = 'inherit';
const startPosition = this.timeToPosition(this._timeSelection.start);
const endPosition = this.timeToPosition(this._timeSelection.end);
this._leftHandleNode.style.left = startPosition + 'px';
this._rightHandleNode.style.left = endPosition + 'px';
const delta = endPosition - startPosition;
this._selectionForegroundNode.style.left = startPosition + 'px';
this._selectionForegroundNode.style.width = delta + 'px';
this._selectionBackgroundNode.style.left = startPosition + 'px';
this._selectionBackgroundNode.style.width = delta + 'px';
}
set timeSelection(selection) {
this._timeSelection.start = selection.start;
this._timeSelection.end = selection.end;
}
clearSelection() {
this._timeline.dispatchEvent(new SelectTimeEvent());
}
timeToPosition(posX) {
return this._timeline.timeToPosition(posX);
}
positionToTime(posX) {
return this._timeline.positionToTime(posX);
}
get isSelecting() {
return this._selectionOriginTime >= 0;
}
get hasSelection() {
return this._timeSelection.start >= 0 &&
this._timeSelection.end != Infinity;
}
get _leftHandlePosX() {
return this._leftHandleNode.getBoundingClientRect().x;
}
get _rightHandlePosX() {
return this._rightHandleNode.getBoundingClientRect().x;
}
_isOnLeftHandle(posX) {
return Math.abs(this._leftHandlePosX - posX) <=
SelectionHandler.SELECTION_OFFSET;
}
_isOnRightHandle(posX) {
return Math.abs(this._rightHandlePosX - posX) <=
SelectionHandler.SELECTION_OFFSET;
}
_handleMouseDown(event) {
if (event.button !== 0) return;
let xPosition = event.clientX
// Update origin time in case we click on a handle.
if (this._isOnLeftHandle(xPosition)) {
xPosition = this._rightHandlePosX;
}
else if (this._isOnRightHandle(xPosition)) {
xPosition = this._leftHandlePosX;
}
this._selectionOriginTime = this.positionToTime(xPosition);
}
_handleMouseMove(event) {
if (event.button !== 0) return;
if (!this.isSelecting) return;
const currentTime = this.positionToTime(event.clientX);
this._timeline.dispatchEvent(new SynchronizeSelectionEvent(
Math.min(this._selectionOriginTime, currentTime),
Math.max(this._selectionOriginTime, currentTime)));
}
_handleMouseUp(event) {
if (event.button !== 0) return;
this._selectionOriginTime = -1;
if (this._timeSelection.start === -1) return;
const delta = this._timeSelection.end - this._timeSelection.start;
if (delta <= 1 || isNaN(delta)) return;
this._timeline.dispatchEvent(new SelectTimeEvent(
this._timeSelection.start, this._timeSelection.end));
}
_handleDoubleClick(event) {
if (!this.hasSelection) return;
// Focus and zoom to the current selection.
this._timeline.dispatchEvent(new SelectTimeEvent(
this._timeSelection.start, this._timeSelection.end, true, true));
}
}
class Legend {
_timeline;
_lastSelection;
_typesFilters = new Map();
_typeClickHandler = this._handleTypeClick.bind(this);
_filterPredicate = this.filter.bind(this);
onFilter = () => {};
constructor(table) {
this._table = table;
this._enableDuration = false;
}
set timeline(timeline) {
this._timeline = timeline;
const groups = timeline.getBreakdown();
this._typesFilters = new Map(groups.map(each => [each.key, true]));
this._colors =
new Map(groups.map(each => [each.key, CSSColor.at(each.id)]));
}
get selection() {
return this._timeline.selectionOrSelf;
}
get filterPredicate() {
for (let visible of this._typesFilters.values()) {
if (!visible) return this._filterPredicate;
}
return undefined;
}
colorForType(type) {
let color = this._colors.get(type);
if (color === undefined) {
color = CSSColor.at(this._colors.size);
this._colors.set(type, color);
}
return color;
}
filter(logEntry) {
return this._typesFilters.get(logEntry.type);
}
update(force = false) {
if (!force && this._lastSelection === this.selection) return;
this._lastSelection = this.selection;
const tbody = DOM.tbody();
const missingTypes = new Set(this._typesFilters.keys());
this._checkDurationField();
let selectionDuration = 0;
const breakdown =
this.selection.getBreakdown(undefined, this._enableDuration);
if (this._enableDuration) {
if (this.selection.cachedDuration === undefined) {
this.selection.cachedDuration = this._breakdownTotalDuration(breakdown);
}
selectionDuration = this.selection.cachedDuration;
}
breakdown.forEach(group => {
tbody.appendChild(this._addTypeRow(group, selectionDuration));
missingTypes.delete(group.key);
});
missingTypes.forEach(key => {
const emptyGroup = {key, length: 0, duration: 0};
tbody.appendChild(this._addTypeRow(emptyGroup, selectionDuration));
});
if (this._timeline.selection) {
tbody.appendChild(this._addRow(
'', 'Selection', this.selection.length, '100%', selectionDuration,
'100%'));
}
// Showing 100% for 'All' and for 'Selection' would be confusing.
const allPercent = this._timeline.selection ? '' : '100%';
tbody.appendChild(this._addRow(
'', 'All', this._timeline.length, allPercent,
this._timeline.cachedDuration, allPercent));
this._table.tBodies[0].replaceWith(tbody);
}
_checkDurationField() {
if (this._enableDuration) return;
const example = this.selection.at(0);
if (!example || !('duration' in example)) return;
this._enableDuration = true;
this._table.tHead.rows[0].appendChild(DOM.td('Duration'));
}
_addRow(colorNode, type, count, countPercent, duration, durationPercent) {
const row = DOM.tr();
const colorCell = row.appendChild(DOM.td(colorNode, 'color'));
colorCell.setAttribute('title', `Toggle '${type}' entries.`);
const typeCell = row.appendChild(DOM.td(type, 'text'));
typeCell.setAttribute('title', type);
row.appendChild(DOM.td(count.toString()));
row.appendChild(DOM.td(countPercent));
if (this._enableDuration) {
row.appendChild(DOM.td(formatDurationMicros(duration ?? 0)));
row.appendChild(DOM.td(durationPercent ?? '0%'));
}
return row
}
_addTypeRow(group, selectionDuration) {
const color = this.colorForType(group.key);
const classes = ['colorbox'];
if (group.length == 0) classes.push('empty');
const colorDiv = DOM.div(classes);
colorDiv.style.borderColor = color;
if (this._typesFilters.get(group.key)) {
colorDiv.style.backgroundColor = color;
} else {
colorDiv.style.backgroundColor = CSSColor.backgroundImage;
}
let duration = 0;
let durationPercent = '';
if (this._enableDuration) {
// group.duration was added in _breakdownTotalDuration.
duration = group.duration;
durationPercent = selectionDuration == 0 ?
'0%' :
this._formatPercent(duration / selectionDuration);
}
const countPercent =
this._formatPercent(group.length / this.selection.length);
const row = this._addRow(
colorDiv, group.key, group.length, countPercent, duration,
durationPercent);
row.className = 'clickable';
row.onclick = this._typeClickHandler;
row.data = group.key;
return row;
}
_handleTypeClick(e) {
const type = e.currentTarget.data;
this._typesFilters.set(type, !this._typesFilters.get(type));
this.onFilter(type);
}
_breakdownTotalDuration(breakdown) {
let duration = 0;
breakdown.forEach(group => {
group.duration = this._groupDuration(group);
duration += group.duration;
})
return duration;
}
_groupDuration(group) {
let duration = 0;
const entries = group.entries;
for (let i = 0; i < entries.length; i++) {
duration += entries[i].duration;
}
return duration;
}
_formatPercent(ratio) {
return `${(ratio * 100).toFixed(1)}%`;
}
}

View File

@ -0,0 +1,259 @@
// Copyright 2021 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 {kChunkVisualWidth, MapLogEntry} from '../../log/map.mjs';
import {FocusEvent} from '../events.mjs';
import {CSSColor, DOM} from '../helper.mjs';
import {TimelineTrackBase} from './timeline-track-base.mjs'
DOM.defineCustomElement(
'view/timeline/timeline-track', 'timeline-track-map',
(templateText) => class TimelineTrackMap extends TimelineTrackBase {
constructor() {
super(templateText);
this.navigation = new Navigation(this)
}
_handleKeyDown(event) {}
getMapStyle(map) {
return map.edge && map.edge.from ? CSSColor.onBackgroundColor :
CSSColor.onPrimaryColor;
}
markMap(map) {
const [x, y] = map.position(this.chunks);
const strokeColor = this.getMapStyle(map);
return `<circle cx=${x} cy=${y} r=${2} stroke=${
strokeColor} class=annotationPoint />`
}
markSelectedMap(map) {
const [x, y] = map.position(this.chunks);
const strokeColor = this.getMapStyle(map);
return `<circle cx=${x} cy=${y} r=${3} stroke=${
strokeColor} class=annotationPoint />`
}
_drawAnnotations(logEntry, time) {
if (!(logEntry instanceof MapLogEntry)) return;
if (!logEntry.edge) {
this.timelineAnnotationsNode.innerHTML = '';
return;
}
// Draw the trace of maps in reverse order to make sure the outgoing
// transitions of previous maps aren't drawn over.
const kOpaque = 1.0;
let stack = [];
let current = logEntry;
while (current !== undefined) {
stack.push(current);
current = current.parent;
}
// Draw outgoing refs as fuzzy background. Skip the last map entry.
let buffer = '';
let nofEdges = 0;
const kMaxOutgoingEdges = 100;
for (let i = stack.length - 2; i >= 0; i--) {
const map = stack[i].parent;
nofEdges += map.children.length;
if (nofEdges > kMaxOutgoingEdges) break;
buffer += this.drawOutgoingEdges(map, 0.4, 1);
}
// Draw main connection.
let labelOffset = 15;
let xPrev = 0;
for (let i = stack.length - 1; i >= 0; i--) {
let map = stack[i];
if (map.edge) {
const [xTo, data] = this.drawEdge(map.edge, kOpaque, labelOffset);
buffer += data;
if (xTo == xPrev) {
labelOffset += 10;
} else {
labelOffset = 15
}
xPrev = xTo;
}
buffer += this.markMap(map);
}
buffer += this.drawOutgoingEdges(logEntry, 0.9, 3);
// Mark selected map
buffer += this.markSelectedMap(logEntry);
this.timelineAnnotationsNode.innerHTML = buffer;
}
drawEdge(edge, opacity, labelOffset = 20) {
let buffer = '';
if (!edge.from || !edge.to) return [-1, buffer];
const [xFrom, yFrom] = edge.from.position(this.chunks);
const [xTo, yTo] = edge.to.position(this.chunks);
const sameChunk = xTo == xFrom;
if (sameChunk) labelOffset += 10;
const color = this._legend.colorForType(edge.type);
const offsetX = 20;
const midX = xFrom + (xTo - xFrom) / 2;
const midY = (yFrom + yTo) / 2 - 100;
if (!sameChunk) {
if (opacity == 1.0) {
buffer += `<path d="M ${xFrom} ${yFrom} Q ${midX} ${midY}, ${xTo} ${
yTo}" class=strokeBG />`
}
buffer += `<path d="M ${xFrom} ${yFrom} Q ${midX} ${midY}, ${xTo} ${
yTo}" stroke=${color} fill=none opacity=${opacity} />`
} else {
if (opacity == 1.0) {
buffer += `<line x1=${xFrom} x2=${xTo} y1=${yFrom} y2=${
yTo} class=strokeBG />`;
}
buffer += `<line x1=${xFrom} x2=${xTo} y1=${yFrom} y2=${yTo} stroke=${
color} fill=none opacity=${opacity} />`;
}
if (opacity == 1.0) {
const centerX =
sameChunk ? xTo : ((xFrom / 2 + midX + xTo / 2) / 2) | 0;
const centerY =
sameChunk ? yTo : ((yFrom / 2 + midY + yTo / 2) / 2) | 0;
const centerYTo = centerY - labelOffset;
buffer +=
`<line x1=${centerX} x2=${centerX + offsetX} y1=${centerY} y2=${
centerYTo} stroke=${color} fill=none opacity=${opacity} />`;
buffer += `<text x=${centerX + offsetX + 2} y=${
centerYTo} class=annotationLabel opacity=${opacity} >${
edge.toString()}</text>`;
}
return [xTo, buffer];
}
drawOutgoingEdges(map, opacity = 1.0, max = 10, depth = 0) {
let buffer = '';
if (!map || depth >= max) return buffer;
const limit = Math.min(map.children.length, 100)
for (let i = 0; i < limit; i++) {
const edge = map.children[i];
const [xTo, data] = this.drawEdge(edge, opacity);
buffer += data;
buffer +=
this.drawOutgoingEdges(edge.to, opacity * 0.5, max, depth + 1);
}
return buffer;
}
})
class Navigation {
constructor(track) {
this._track = track;
this._track.addEventListener('keydown', this._handleKeyDown.bind(this));
this._map = undefined;
}
_handleKeyDown(event) {
if (!this._track.isFocused) return;
let handled = false;
switch (event.key) {
case 'ArrowDown':
handled = true;
if (event.shiftKey) {
this.selectPrevEdge();
} else {
this.moveInChunk(-1);
}
break;
case 'ArrowUp':
handled = true;
if (event.shiftKey) {
this.selectNextEdge();
} else {
this.moveInChunk(1);
}
break;
case 'ArrowLeft':
handled = true;
this.moveInChunks(false);
break;
case 'ArrowRight':
handled = true;
this.moveInChunks(true);
break;
case 'Enter':
handled = true;
this.selectMap();
break
}
if (handled) {
event.stopPropagation();
event.preventDefault();
return false;
}
}
get map() {
return this._track.focusedEntry;
}
set map(map) {
this._track.focusedEntry = map;
}
get chunks() {
return this._track.chunks;
}
selectMap() {
if (!this.map) return;
this._track.dispatchEvent(new FocusEvent(this.map))
}
selectNextEdge() {
if (!this.map) return;
if (this.map.children.length != 1) return;
this.show(this.map.children[0].to);
}
selectPrevEdge() {
if (!this.map) return;
if (!this.map.parent) return;
this.map = this.map.parent;
}
selectDefaultMap() {
this.map = this.chunks[0].at(0);
}
moveInChunks(next) {
if (!this.map) return this.selectDefaultMap();
let chunkIndex = this.map.chunkIndex(this.chunks);
let currentChunk = this.chunks[chunkIndex];
let currentIndex = currentChunk.indexOf(this.map);
let newChunk;
if (next) {
newChunk = chunk.next(this.chunks);
} else {
newChunk = chunk.prev(this.chunks);
}
if (!newChunk) return;
let newIndex = Math.min(currentIndex, newChunk.size() - 1);
this.map = newChunk.at(newIndex);
}
moveInChunk(delta) {
if (!this.map) return this.selectDefaultMap();
let chunkIndex = this.map.chunkIndex(this.chunks)
let chunk = this.chunks[chunkIndex];
let index = chunk.indexOf(this.map) + delta;
let map;
if (index < 0) {
map = chunk.prev(this.chunks).last();
} else if (index >= chunk.size()) {
map = chunk.next(this.chunks).first()
} else {
map = chunk.at(index);
}
this.map = map;
}
}

View File

@ -0,0 +1,150 @@
// Copyright 2021 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 {Timeline} from '../../timeline.mjs';
import {SelectTimeEvent} from '../events.mjs';
import {CSSColor, delay, SVG} from '../helper.mjs';
import {TimelineTrackBase} from './timeline-track-base.mjs'
const kItemHeight = 8;
export class TimelineTrackStackedBase extends TimelineTrackBase {
_originalContentWidth = 0;
_drawableItems = new Timeline();
_updateChunks() {
// We don't need to update the chunks here.
this._updateDimensions();
this.requestUpdate();
}
set data(timeline) {
super.data = timeline;
this._contentWidth = 0;
if (timeline.values.length > 0) this._prepareDrawableItems();
}
_handleDoubleClick(event) {
if (event.button !== 0) return;
this._selectionHandler.clearSelection();
const item = this._getDrawableItemForEvent(event);
if (item === undefined) return;
event.stopImmediatePropagation();
this.dispatchEvent(new SelectTimeEvent(item.startTime, item.endTime));
return false;
}
_getStackDepthForEvent(event) {
return Math.floor(event.layerY / kItemHeight) - 1;
}
_getDrawableItemForEvent(event) {
const depth = this._getStackDepthForEvent(event);
const time = this.positionToTime(event.pageX);
const index = this._drawableItems.find(time);
for (let i = index - 1; i > 0; i--) {
const item = this._drawableItems.at(i);
if (item.depth != depth) continue;
if (item.endTime < time) continue;
return item;
}
return undefined;
}
_drawableItemToLogEntry(item) {
return item;
}
_getEntryForEvent(event) {
const item = this._getDrawableItemForEvent(event);
const logEntry = this._drawableItemToLogEntry(item);
if (item === undefined) return undefined;
const node = this.getToolTipTargetNode(logEntry);
if (!node) return logEntry;
const style = node.style;
style.left = `${event.layerX}px`;
style.top = `${(item.depth + 1) * kItemHeight}px`;
style.height = `${kItemHeight}px`
return logEntry;
}
_prepareDrawableItems() {
// Subclass responsibility.
}
_adjustStackDepth(maxDepth) {
// Account for empty top line
maxDepth++;
this._adjustHeight(maxDepth * kItemHeight);
}
_scaleContent(currentWidth) {
if (this._originalContentWidth == 0) return;
// Instead of repainting just scale the content.
const ratio = currentWidth / this._originalContentWidth;
this._scalableContentNode.style.transform = `scale(${ratio}, 1)`;
this.style.setProperty('--txt-scale', `scale(${1 / ratio}, 1)`);
return ratio;
}
async _drawContent() {
if (this._originalContentWidth > 0) return;
this._originalContentWidth = parseInt(this.timelineMarkersNode.style.width);
this._scalableContentNode.innerHTML = '';
let buffer = '';
const add = async () => {
const svg = SVG.svg();
svg.innerHTML = buffer;
this._scalableContentNode.appendChild(svg);
buffer = '';
await delay(50);
};
const items = this._drawableItems.values;
for (let i = 0; i < items.length; i++) {
if ((i % 3000) == 0) await add();
buffer += this._drawItem(items[i], i);
}
add();
}
_drawItem(item, i, outline = false) {
const x = roundTo3Digits(this.timeToPosition(item.time));
const y = (item.depth + 1) * kItemHeight;
let width = roundTo3Digits(item.duration * this._timeToPixel);
if (outline) {
return `<rect x=${x} y=${y} width=${width} height=${
kItemHeight - 1} class=fs />`;
}
let color = this._legend.colorForType(item.type);
if (i % 2 == 1) {
color = CSSColor.darken(color, 20);
}
return `<rect x=${x} y=${y} width=${width} height=${kItemHeight - 1} fill=${
color} class=f />`;
}
_drawItemText(item) {
const type = item.type;
const kHeight = 9;
const x = this.timeToPosition(item.time);
const y = item.depth * (kHeight + 1);
let width = item.duration * this._timeToPixel;
width -= width * 0.1;
let buffer = '';
if (width < 15 || type == 'Other') return buffer;
const rawName = item.entry.getName();
if (rawName.length == 0) return buffer;
const kChartWidth = 5;
const maxChars = Math.floor(width / kChartWidth)
const text = rawName.substr(0, maxChars);
buffer += `<text x=${x + 1} y=${y - 3} class=txt>${text}</text>`
return buffer;
}
}
function roundTo3Digits(value) {
return ((value * 1000) | 0) / 1000;
}

View File

@ -0,0 +1,258 @@
<!-- Copyright 2020 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. -->
<head>
<link href="./index.css" rel="stylesheet">
</head>
<style>
:host {
--view-height: 200px;
--data-height: 200px;
--text-scale: scale(1, 1);
}
#timeline {
position: relative;
height: calc(var(--view-height) + 12px);
overflow-y: hidden;
overflow-x: scroll;
user-select: none;
}
#cropper {
overflow: hidden;
position: relative;
}
.dataSized {
min-height: var(--data-height);
}
#timelineSamples, #timelineChunks,
#timelineMarkers, #timelineAnnotations, #hitPanel {
top: 0px;
position: absolute;
margin-right: 100px;
}
#timelineMarkers, #timelineAnnotations,
.noPointerEvents, .noPointerEvents * {
pointer-events: none;
}
.toolTipTarget {
position: absolute;
}
.title {
position: relative;
float: left;
width: 20px;
writing-mode: vertical-rl;
text-orientation: mixed;
margin: 20px 0 0 -10px;
padding: 5px 5px 0px 5px;
overflow: hidden;
border-radius: 7px;
font-weight: 400;
}
.panelCloserInput:checked ~ h3 {
display: inherit;
flex: 1;
writing-mode: unset;
text-orientation: unset;
background-color: var(--border-color);
border-radius: 0px;
padding: 5px;
margin: 0 -10px 0 20px;
}
.timelineLegend {
position: relative;
float: right;
height: calc(var(--view-height) + 12px);
overflow-y: scroll;
margin-right: -10px;
padding: 0 2px 0 2px;
width: 400px;
border-left: 1px solid var(--border-color);
}
#legendTable {
width: 100%;
border-collapse: collapse;
}
thead {
border-top: 1px solid var(--border-color);
border-bottom: 1px solid var(--border-color);
}
th,
td {
padding: 1px 3px 2px 3px;
}
#legendTable td {
padding-top: 3px;
text-align: right;
}
/* Center colors */
#legendTable .color {
text-align: center;
}
/* Left align text*/
#legendTable .text {
text-align: left;
width: 100%;
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
}
.timeline {
background-color: var(--timeline-background-color);
}
#selection {
display: none;
top: 0px;
left: 0px;
position: absolute;
}
#rightHandle,
#leftHandle {
background-color: rgba(200, 200, 200, 0.5);
height: 100%;
width: 5px;
position: absolute;
z-index: 3;
cursor: col-resize;
}
#leftHandle {
border-left: 1px solid var(--on-surface-color);
}
#rightHandle {
border-right: 1px solid var(--on-surface-color);
margin-left: -5px;
}
#selectionForeground{
z-index: 2;
cursor: grab;
height: 100%;
position: absolute;
}
#selectionForeground:active {
cursor: grabbing;
}
#selectionBackground {
background-color: var(--selection-color);
height: 100%;
position: absolute;
}
.content {
display: flex;
position: relative;
}
.panelCloserLabel {
position: absolute;
top: 5px;
left: 0px;
}
.title {
flex: initial;
}
#timeline {
flex: 1;
}
.legend {
flex: initial;
}
.colorbox.empty {
opacity: 0.5;
}
</style>
<style>
/* SVG styles */
.txt {
font: 8px var(--code-font);
transform: var(--txt-scale);
}
.annotationLabel {
fill: var(--on-surface-color);
font-size: 9px;
}
.annotationPoint {
fill: var(--on-background-color);
stroke-width: 1;
}
.strokeBG {
stroke: var(--on-background-color);
stroke-width: 2;
fill: none;
}
.markerLine {
stroke: var(--on-background-color);
stroke-dasharray: 2 2;
}
.markerText {
fill: var(--on-surface-color);
dominant-baseline: hanging;
font-size: 9px;
}
.flame, .f {
stroke-width: 0;
}
.flameSelected, .fs {
fill: var(--on-background-color);
fill-opacity: 0.1;
stroke: var(--on-background-color);
stroke-opacity: 0.8;
stroke-width: 1;
vector-effect: non-scaling-stroke;
}
svg {
shape-rendering: optimizeSpeed;
}
</style>
<div class="content">
<input type="checkbox" id="closer" class="panelCloserInput">
<label class="panelCloserLabel" for="closer">▼</label>
<h3 class="title" id="title"></h3>
<div id="timeline">
<div id="selection" class="dataSized">
<div id="leftHandle"></div>
<div id="selectionForeground"></div>
<div id="selectionBackground"></div>
<div id="rightHandle"></div>
</div>
<div id="cropper" class="dataSized">
<svg id="timelineChunks" xmlns="http://www.w3.org/2000/svg" class="dataSized">
<g id="scalableContent"></g>
</svg>
<svg id="timelineAnnotations" xmlns="http://www.w3.org/2000/svg" class="dataSized noPointerEvents"></svg>
<svg id="timelineMarkers" xmlns="http://www.w3.org/2000/svg" class="dataSized noPointerEvents"></svg>
</div>
<!-- Use a div element covering all complex items to prevent slow hit test-->
<div id="hitPanel" class="dataSized"></div>
</div>
<div class="timelineLegend">
<table id="legendTable">
<thead>
<tr>
<td></td>
<td class="text">Type</td>
<td>Count</td>
<td></td>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>

View File

@ -0,0 +1,112 @@
// Copyright 2021 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 {Flame, FlameBuilder} from '../../profiling.mjs';
import {Timeline} from '../../timeline.mjs';
import {delay, DOM, SVG} from '../helper.mjs';
import {TimelineTrackStackedBase} from './timeline-track-stacked-base.mjs'
DOM.defineCustomElement(
'view/timeline/timeline-track', 'timeline-track-tick',
(templateText) => class TimelineTrackTick extends TimelineTrackStackedBase {
constructor() {
super(templateText);
this._annotations = new Annotations(this);
}
_prepareDrawableItems() {
const flameBuilder = FlameBuilder.forTime(this._timeline.values, true);
this._drawableItems = new Timeline(Flame, flameBuilder.flames);
this._annotations.flames = this._drawableItems;
this._adjustStackDepth(flameBuilder.maxDepth);
}
_drawAnnotations(logEntry, time) {
if (time === undefined) {
time = this.relativePositionToTime(this._timelineScrollLeft);
}
this._annotations.update(logEntry, time);
}
_drawableItemToLogEntry(flame) {
const logEntry = flame?.logEntry;
if (logEntry === undefined || typeof logEntry == 'number')
return undefined;
return logEntry;
}
})
class Annotations {
_flames;
_logEntry;
_buffer;
constructor(track) {
this._track = track;
}
set flames(flames) {
this._flames = flames;
}
get _node() {
return this._track.timelineAnnotationsNode;
}
async update(logEntry, time) {
if (this._logEntry == logEntry) return;
this._logEntry = logEntry;
this._node.innerHTML = '';
if (logEntry === undefined) return;
this._buffer = '';
const start = this._flames.find(time);
let offset = 0;
// Draw annotations gradually outwards starting form the given time.
let deadline = performance.now() + 100;
for (let range = 0; range < this._flames.length; range += 10000) {
this._markFlames(start - range, start - offset);
this._markFlames(start + offset, start + range);
offset = range;
if ((navigator?.scheduling?.isInputPending({includeContinuous: true}) ??
false) ||
performance.now() >= deadline) {
// Yield if we have to handle an input event, or we're out of time.
await delay(50);
// Abort if we started another update asynchronously.
if (this._logEntry != logEntry) return;
deadline = performance.now() + 100;
}
this._drawBuffer();
}
this._drawBuffer();
}
_markFlames(start, end) {
const rawFlames = this._flames.values;
if (start < 0) start = 0;
if (end > rawFlames.length) end = rawFlames.length;
const logEntry = this._logEntry;
// Also compare against the SFI , if any.
const sfi = logEntry.entry?.sfi ?? -1;
for (let i = start; i < end; i++) {
const flame = rawFlames[i];
const flameLogEntry = flame.logEntry;
if (!flameLogEntry) continue;
if (flameLogEntry !== logEntry) {
if (flameLogEntry.entry?.sfi !== sfi) continue;
}
this._buffer += this._track._drawItem(flame, i, true);
}
}
_drawBuffer() {
if (this._buffer.length == 0) return;
const svg = SVG.svg();
svg.innerHTML = this._buffer;
this._node.appendChild(svg);
this._buffer = '';
}
}

View File

@ -0,0 +1,41 @@
// Copyright 2021 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 {CSSColor, DOM, SVG, V8CustomElement} from '../helper.mjs';
import {TimelineTrackBase} from './timeline-track-base.mjs'
import {TimelineTrackStackedBase} from './timeline-track-stacked-base.mjs'
DOM.defineCustomElement(
'view/timeline/timeline-track', 'timeline-track-timer',
(templateText) =>
class TimelineTrackTimer extends TimelineTrackStackedBase {
constructor() {
super(templateText);
}
_prepareDrawableItems() {
const stack = [];
let maxDepth = 0;
for (let i = 0; i < this._timeline.length; i++) {
const timer = this._timeline.at(i);
let insertDepth = -1;
for (let depth = 0; depth < stack.length; depth++) {
const pendingTimer = stack[depth];
if (pendingTimer === undefined) {
if (insertDepth === -1) insertDepth = depth;
} else if (pendingTimer.endTime <= timer.startTime) {
stack[depth] == undefined;
if (insertDepth === -1) insertDepth = depth;
}
}
if (insertDepth === -1) insertDepth = stack.length;
stack[insertDepth] = timer;
timer.depth = insertDepth;
maxDepth = Math.max(maxDepth, insertDepth);
}
this._drawableItems = this._timeline;
this._adjustStackDepth(maxDepth++);
}
});

View File

@ -0,0 +1,15 @@
// Copyright 2021 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 {CSSColor, DOM, SVG, V8CustomElement} from '../helper.mjs';
import {TimelineTrackBase} from './timeline-track-base.mjs'
DOM.defineCustomElement(
'view/timeline/timeline-track',
(templateText) => class TimelineTrack extends TimelineTrackBase {
constructor() {
super(templateText);
}
})

View File

@ -0,0 +1,112 @@
<!-- Copyright 2020 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. -->
<head>
<link href="./index.css" rel="stylesheet">
</head>
<style>
:host {
position: absolute;
z-index: 100;
will-change: transform;
}
#body {
display: none;
position: absolute;
--tip-offset: 10px;
--tip-width: 10px;
--tip-height: 40px;
}
#content {
background-color: rgba(var(--surface-color-rgb), 0.8);
border: 3px var(--primary-color) solid;
border-radius: 10px;
padding: 10px;
width: auto;
min-width: 100px;
max-width: 400px;
min-height: 100px;
max-height: 400px;
box-shadow: 0px 0px 10px rgba(0,0,0,0.5);
}
#content > h3 {
margin-top: 0;
}
.textContent {
font-family: var(--code-font);
white-space: pre;
overflow-wrap: anywhere;
overflow-x: hidden;
max-width: 500px;
}
#body.top {
bottom: var(--tip-height);
}
#body.bottom {
top: var(--tip-height);
}
#body.left {
right: calc(var(--tip-offset) * -1 - var(--tip-width));
}
#body.right {
left: calc(var(--tip-offset) * -1 - var(--tip-width));
}
.tip, .tipThin {
width: 0;
height: 0;
border-style: solid;
position: absolute;
border-color: var(--primary-color) transparent transparent transparent;
pointer-events: none;
}
.tip {
border-width: var(--tip-width) var(--tip-width) 0 var(--tip-width);
}
.tipThin {
border-width: var(--tip-height) 4px 2px 4px;
bottom: -30px;
left: -4px;
}
/* Tip positioning modifiers */
.top > .tip {
bottom: calc(var(--tip-width) * -1);
}
.top > .tipThin {
bottom: calc(var(--tip-height) * -1);
}
.bottom > .tip {
top: calc(var(--tip-width) * -1);
transform: scaleY(-1);
}
.bottom > .tipThin {
top: calc(var(--tip-height) * -1);
transform: scaleY(-1);
}
.left > .tip {
right: var(--tip-offset);
}
.left > .tipThin {
right: var(--tip-offset);
}
.right > .tip {
left: var(--tip-offset);
}
.right > .tipThin {
left: var(--tip-offset);
}
</style>
<div id="body">
<div id="content">
<property-link-table id="properties"></property-link-table>
</div>
<div class="tip">
<div class="tipThin"></div>
</div>
</div>

View File

@ -0,0 +1,148 @@
// Copyright 2020 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 {Debouncer, DOM, V8CustomElement} from './helper.mjs';
DOM.defineCustomElement(
'view/tool-tip', (templateText) => class Tooltip extends V8CustomElement {
_targetNode;
_content;
_isHidden = true;
_debouncedSetData =
new Debouncer((...args) => this._setData(...args), 500)
constructor() {
super(templateText);
this._intersectionObserver = new IntersectionObserver((entries) => {
if (entries[0].intersectionRatio <= 0) {
this.hide();
} else {
this.show();
this.requestUpdate(true);
}
});
document.addEventListener('click', (event) => {
// Only hide the tooltip if we click anywhere outside of it.
let target = event.target;
while (target) {
if (target == this) return;
target = target.parentNode;
}
this.hide()
});
}
_update() {
if (!this._targetNode || this._isHidden) return;
if (!this._targetNode.parentNode) return;
const rect = this._targetNode.getBoundingClientRect();
rect.x += rect.width / 2;
let atRight = this._useRight(rect.x);
let atBottom = this._useBottom(rect.y);
if (atBottom) rect.y += rect.height;
this._setPosition(rect, atRight, atBottom);
this.requestUpdate(true);
}
set data({content, positionOrTargetNode, immediate}) {
if (immediate) {
this._debouncedSetData.callNow(content, positionOrTargetNode)
} else {
this._debouncedSetData.call(content, positionOrTargetNode)
}
}
_setData(content, positionOrTargetNode) {
if (positionOrTargetNode.nodeType === undefined) {
this._targetNode = undefined;
const position = positionOrTargetNode;
this._setPosition(
position, this._useRight(position.x),
this._useBottom(position.y));
} else {
this._setTargetNode(positionOrTargetNode);
}
this._setContent(content);
}
_setTargetNode(targetNode) {
this._intersectionObserver.disconnect();
this._targetNode = targetNode;
if (targetNode === undefined) return;
if (!(targetNode instanceof SVGElement)) {
this._intersectionObserver.observe(targetNode);
}
this.requestUpdate(true);
}
_setPosition(viewportPosition, atRight, atBottom) {
const horizontalMode = atRight ? 'right' : 'left';
const verticalMode = atBottom ? 'bottom' : 'top';
this.bodyNode.className = horizontalMode + ' ' + verticalMode;
const pageX = viewportPosition.x + window.scrollX;
this.style.left = `${pageX}px`;
const pageY = viewportPosition.y + window.scrollY;
this.style.top = `${pageY}px`;
}
_useBottom(viewportY) {
return viewportY <= 400;
}
_useRight(viewportX) {
return viewportX < document.documentElement.clientWidth / 2;
}
_setContent(content) {
if (!content) return this.hide();
this.show();
if (this._content === content) return;
this._content = content;
if (typeof content === 'string') {
this.contentNode.innerHTML = content;
this.contentNode.className = 'textContent';
} else if (content?.nodeType && content?.nodeName) {
this._setContentNode(content);
} else {
if (this.contentNode.firstChild?.localName == 'property-link-table') {
this.contentNode.firstChild.propertyDict = content;
} else {
const node = DOM.element('property-link-table');
node.instanceLinkButtons = true;
node.propertyDict = content;
this._setContentNode(node);
}
}
}
_setContentNode(content) {
const newContent = DOM.div();
newContent.appendChild(content);
this.contentNode.replaceWith(newContent);
newContent.id = 'content';
}
hide() {
this._content = undefined;
if (this._isHidden) return;
this._isHidden = true;
this.bodyNode.style.display = 'none';
this.targetNode = undefined;
}
show() {
if (!this._isHidden) return;
this.bodyNode.style.display = 'block';
this._isHidden = false;
}
get bodyNode() {
return this.$('#body');
}
get contentNode() {
return this.$('#content');
}
});