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,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');
}
});