Commit 58c65035 authored by Camillo Bruni's avatar Camillo Bruni Committed by Commit Bot

[tools][system-analyzer] Clean up ICPanel and add DOM helper

- Move all createElement helpers onto a separate DOM class
- Make ICPanel.update async
- Show number of selected IC events in the ICPanel header
- Use shared bound functions for event listeners in the ICPanel groups
- Use triangle to mark opened and closed ICPanel groups
- Use global --border-color CSS variable

Bug: v8:10644
Change-Id: Ib35d94db1019d5cdcee057f0f047472f478ab3be
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/2507718Reviewed-by: 's avatarSathya Gunasekaran  <gsathya@chromium.org>
Commit-Queue: Camillo Bruni <cbruni@chromium.org>
Cr-Commit-Position: refs/heads/master@{#70919}
parent 4242b192
......@@ -22,26 +22,6 @@ function formatSeconds(millis) {
return (millis * kMillis2Seconds).toFixed(2) + 's';
}
function defineCustomElement(path, generator) {
let name = path.substring(path.lastIndexOf("/") + 1, path.length);
path = path + '-template.html';
fetch(path)
.then(stream => stream.text())
.then(
templateText => customElements.define(name, generator(templateText)));
}
// DOM Helpers
function removeAllChildren(node) {
let range = document.createRange();
range.selectNodeContents(node);
range.deleteContents();
}
function $(id) {
return document.querySelector(id)
}
class CSSColor {
static getColor(name) {
const style = getComputedStyle(document.body);
......@@ -135,16 +115,64 @@ function typeToColor(type) {
return CSSColor.secondaryColor;
}
function div(classes) {
let node = document.createElement('div');
if (classes !== void 0) {
if (typeof classes === 'string') {
node.classList.add(classes);
} else {
classes.forEach(cls => node.classList.add(cls));
class DOM {
static div(classes) {
const node = document.createElement('div');
if (classes !== void 0) {
if (typeof classes === 'string') {
node.classList.add(classes);
} else {
classes.forEach(cls => node.classList.add(cls));
}
}
return node;
}
static table(className) {
const node = document.createElement('table');
if (className) node.classList.add(className);
return node;
}
static td(textOrNode, className) {
const node = document.createElement('td');
if (typeof textOrNode === 'object') {
node.appendChild(textOrNode);
} else if (textOrNode) {
node.innerText = textOrNode;
}
if (className) node.classList.add(className);
return node;
}
static tr(className) {
const node = document.createElement('tr');
if (className) node.classList.add(className);
return node;
}
static text(string) {
return document.createTextNode(string);
}
static removeAllChildren(node) {
let range = document.createRange();
range.selectNodeContents(node);
range.deleteContents();
}
static defineCustomElement(path, generator) {
let name = path.substring(path.lastIndexOf("/") + 1, path.length);
path = path + '-template.html';
fetch(path)
.then(stream => stream.text())
.then(
templateText => customElements.define(name, generator(templateText)));
}
return node;
}
function $(id) {
return document.querySelector(id)
}
class V8CustomElement extends HTMLElement {
......@@ -160,30 +188,32 @@ class V8CustomElement extends HTMLElement {
querySelectorAll(query) {
return this.shadowRoot.querySelectorAll(query);
}
}
div(classes) { return div(classes) }
table(className) {
let node = document.createElement('table')
if (className) node.classList.add(className)
return node;
class LazyTable {
constructor(table, rowData, rowElementCreator) {
this._table = table;
this._rowData = rowData;
this._rowElementCreator = rowElementCreator;
const tbody = table.querySelector('tbody');
table.replaceChild(document.createElement('tbody'), tbody);
table.querySelector("tfoot td").onclick = (e) => this._addMoreRows();
this._addMoreRows();
}
td(textOrNode) {
let node = document.createElement('td');
if (typeof textOrNode === 'object') {
node.appendChild(textOrNode);
} else {
node.innerText = textOrNode;
}
return node;
_nextRowDataSlice() {
return this._rowData.splice(0, 100);
}
tr() {
return document.createElement('tr');
_addMoreRows() {
const fragment = new DocumentFragment();
for (let row of this._nextRowDataSlice()) {
const tr = this._rowElementCreator(row);
fragment.appendChild(tr);
}
this._table.querySelector('tbody').appendChild(fragment);
}
removeAllChildren(node) { return removeAllChildren(node); }
}
......@@ -217,6 +247,6 @@ function delay(time) {
}
export {
defineCustomElement, V8CustomElement, removeAllChildren,
$, div, typeToColor, CSSColor, delay, LazyTable,
DOM, $, V8CustomElement, formatBytes,
typeToColor, CSSColor, delay, LazyTable,
};
......@@ -25,33 +25,49 @@ found in the LICENSE file. -->
padding: 0.5em 0 0.2em 0;
}
.details {
width: 0.1em;
}
.details span {
padding: 0 0.4em 0 0.4em;
background-color: var(--on-surface-color);
color: var(--surface-color);
border-radius: 25px;
.toggle {
width: 1em;
text-align: center;
cursor: -webkit-zoom-in;
color: rgba(var(--border-color), 1);
}
.toggle::before {
content: "▶";
}
.open .toggle::before {
content: "▼";
}
.panel {
position: relative;
min-height: 200px;
}
#legend {
padding-right: 20px;
position: absolute;
right: 10px;
top: 10px;
background-color: var(--surface-color);
border-radius: 5px;
border: 3px solid rgba(var(--border-color), 0.2);
padding: 0 10px 0 10px;
}
dl {
float: right;
border-style: solid;
border-width: 1px;
padding: 20px;
#legend dt {
font-family: monospace;
}
#legend h3 {
margin-top: 10px;
}
.scroller {
max-height: 800px;
overflow-y: scroll;
}
</style>
<div class="panel">
<h2>IC Panel</h2>
<h2>IC Panel <span id="count"></span></h2>
<div id="legend">
<h3>Legend</h3>
<dl>
<dt>0</dt>
<dd>uninitialized</dd>
......@@ -69,22 +85,14 @@ found in the LICENSE file. -->
<dd>generic</dd>
</dl>
</div>
<h3>Data</h3>
<p>Trace Count: <span id="count">0</span></p>
<h3>Result</h3>
<p>
Group-Key:
Group by IC-property:
<select id="group-key"></select>
</p>
<p>
Filter by Time
<input type="search" id="filter-time-start" placeholder="start"></input> :
<input type="search" id="filter-time-end" placeholder="end"></input>
<button id="filterICTimeBtn">Filter</button>
<p>
<table id="table" width="100%">
<tbody id="table-body">
</tbody>
</table>
</p>
<div class="panelBody">
<table id="table" width="100%">
<tbody id="table-body">
</tbody>
</table>
</div>
</div>
......@@ -5,10 +5,10 @@
import { Group } from './ic-model.mjs';
import { MapLogEntry } from "./log/map.mjs";
import { FocusEvent, SelectTimeEvent, SelectionEvent } from './events.mjs';
import { defineCustomElement, V8CustomElement } from './helper.mjs';
import { DOM, V8CustomElement, delay } from './helper.mjs';
import { IcLogEntry } from './log/ic.mjs';
defineCustomElement('ic-panel', (templateText) =>
DOM.defineCustomElement('ic-panel', (templateText) =>
class ICPanel extends V8CustomElement {
_selectedLogEntries;
_timeline;
......@@ -17,8 +17,6 @@ defineCustomElement('ic-panel', (templateText) =>
this.initGroupKeySelect();
this.groupKey.addEventListener(
'change', e => this.updateTable(e));
this.$('#filterICTimeBtn').addEventListener(
'click', e => this.handleICTimeFilter(e));
}
set timeline(value) {
console.assert(value !== undefined, "timeline undefined!");
......@@ -48,19 +46,24 @@ defineCustomElement('ic-panel', (templateText) =>
set selectedLogEntries(value) {
this._selectedLogEntries = value;
this.update();
}
async update() {
await delay(1);
this.updateCount();
this.updateTable();
}
updateCount() {
this.count.innerHTML = this._selectedLogEntries.length;
this.count.innerHTML = "length=" + this._selectedLogEntries.length;
}
updateTable(event) {
let select = this.groupKey;
let key = select.options[select.selectedIndex].text;
let tableBody = this.tableBody;
this.removeAllChildren(tableBody);
DOM.removeAllChildren(tableBody);
let groups = Group.groupBy(this._selectedLogEntries, key, true);
this.render(groups, tableBody);
}
......@@ -74,32 +77,12 @@ defineCustomElement('ic-panel', (templateText) =>
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
processValue(unsafe) {
if (!unsafe) return "";
if (!unsafe.startsWith("http")) return this.escapeHtml(unsafe);
let a = document.createElement("a");
a.href = unsafe;
a.textContent = unsafe;
return a;
}
td(tr, content, className) {
let node = document.createElement("td");
if (typeof content == "object") {
node.appendChild(content);
} else {
node.innerHTML = content;
}
node.className = className;
tr.appendChild(node);
return node
}
handleMapClick(e) {
const entry = e.target.parentNode.entry;
const id = entry.key;
const group = e.target.parentNode.entry;
const id = group.key;
const selectedMapLogEntries =
this.searchIcLogEntryToMapLogEntry(id, entry.entries);
this.searchIcLogEntryToMapLogEntry(id, group.entries);
this.dispatchEvent(new SelectionEvent(selectedMapLogEntries));
}
......@@ -116,105 +99,97 @@ defineCustomElement('ic-panel', (templateText) =>
//TODO(zcankara) Handle in the processor for events with source positions.
handleFilePositionClick(e) {
const entry = e.target.parentNode.entry;
this.dispatchEvent(new FocusEvent(entry.filePosition));
const tr = e.target.parentNode;
const sourcePosition = tr.group.entries[0].sourcePosition;
this.dispatchEvent(new FocusEvent(sourcePosition));
}
render(entries, parent) {
let fragment = document.createDocumentFragment();
let max = Math.min(1000, entries.length)
render(groups, parent) {
const fragment = document.createDocumentFragment();
const max = Math.min(1000, groups.length)
const detailsClickHandler = this.handleDetailsClick.bind(this);
const mapClickHandler = this.handleMapClick.bind(this);
const fileClickHandler = this.handleFilePositionClick.bind(this);
for (let i = 0; i < max; i++) {
let entry = entries[i];
let tr = document.createElement("tr");
tr.entry = entry;
//TODO(zcankara) Create one bound method and use it everywhere
if (entry.property === "map") {
tr.addEventListener('click', e => this.handleMapClick(e));
tr.classList.add('clickable');
} else if (entry.property == "filePosition") {
tr.classList.add('clickable');
tr.addEventListener('click',
e => this.handleFilePositionClick(e));
const group = groups[i];
const tr = DOM.tr();
tr.group = group;
const details = tr.appendChild(DOM.td('', 'toggle'));
details.onclick = detailsClickHandler;
tr.appendChild(DOM.td(group.percentage + "%", 'percentage'));
tr.appendChild(DOM.td(group.count, 'count'));
const valueTd = tr.appendChild(DOM.td(group.key, 'key'));
if (group.property === "map") {
valueTd.onclick = mapClickHandler;
valueTd.classList.add('clickable');
} else if (group.property == "filePosition") {
valueTd.classList.add('clickable');
valueTd.onclick = fileClickHandler;
}
let details = this.td(tr, '<span>&#8505;</a>', 'details');
//TODO(zcankara) don't keep the whole function context alive
details.onclick = _ => this.toggleDetails(details);
this.td(tr, entry.percentage + "%", 'percentage');
this.td(tr, entry.count, 'count');
this.td(tr, this.processValue(entry.key), 'key');
fragment.appendChild(tr);
}
let omitted = entries.length - max;
const omitted = groups.length - max;
if (omitted > 0) {
let tr = document.createElement("tr");
let tdNode = this.td(tr, 'Omitted ' + omitted + " entries.");
const tr = DOM.tr();
const tdNode =
tr.appendChild(DOM.td('Omitted ' + omitted + " entries."));
tdNode.colSpan = 4;
fragment.appendChild(tr);
}
parent.appendChild(fragment);
}
handleDetailsClick(event) {
const tr = event.target.parentNode;
const group = tr.group;
// Create subgroup in-place if the don't exist yet.
if (group.groups === undefined) {
group.createSubGroups();
this.renderDrilldown(group, tr);
}
let 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(entry, previousSibling) {
let tr = document.createElement('tr');
tr.className = "entry-details";
renderDrilldown(group, previousSibling) {
let tr = DOM.tr("entry-details");
tr.style.display = "none";
// indent by one td.
tr.appendChild(document.createElement("td"));
let td = document.createElement("td");
tr.appendChild(DOM.td());
let td = DOM.td();
td.colSpan = 3;
for (let key in entry.groups) {
td.appendChild(this.renderDrilldownGroup(entry, key));
for (let key in group.groups) {
this.renderDrilldownGroup(td, group.groups[key], key);
}
tr.appendChild(td);
// Append the new TR after previousSibling.
previousSibling.parentNode.insertBefore(tr, previousSibling.nextSibling)
}
renderDrilldownGroup(entry, key) {
let max = 20;
let group = entry.groups[key];
let div = document.createElement("div")
div.className = 'drilldown-group-title'
div.textContent = key + ' [top ' + max + ' out of ' + group.length + ']';
let table = document.createElement("table");
this.render(group.slice(0, max), table, false)
div.appendChild(table);
return div;
}
toggleDetails(node) {
let tr = node.parentNode;
let entry = tr.entry;
// Create subgroup in-place if the don't exist yet.
if (entry.groups === undefined) {
entry.createSubGroups();
this.renderDrilldown(entry, tr);
}
let details = tr.nextSibling;
let display = details.style.display;
if (display != "none") {
display = "none";
} else {
display = "table-row"
};
details.style.display = display;
renderDrilldownGroup(td, children, key) {
const max = 20;
const div = DOM.div('drilldown-group-title');
div.textContent =
`Grouped by ${key} [top ${max} out of ${children.length}]`;
td.appendChild(div);
const table = DOM.table();
this.render(children.slice(0, max), table, false)
td.appendChild(table);
}
initGroupKeySelect() {
let select = this.groupKey;
const select = this.groupKey;
select.options.length = 0;
for (const propertyName of IcLogEntry.propertyNames) {
let option = document.createElement("option");
const option = document.createElement("option");
option.text = propertyName;
select.add(option);
}
}
handleICTimeFilter(e) {
this.dispatchEvent(new SelectTimeEvent(
parseInt(this.$('#filter-time-start').value),
parseInt(this.$('#filter-time-end').value)));
}
});
......@@ -18,6 +18,7 @@
--blue: #6e77dc;
--orange: #dc9b6e;
--violet: #d26edc;
--border-color: 128, 128, 128;
}
[data-theme="light"] {
......@@ -96,7 +97,7 @@ a:link {
dl {
display: grid;
grid-template-columns: min-content auto;
grid-gap: 10px;
grid-gap: 5px;
}
dt {
text-align: right;
......@@ -110,9 +111,14 @@ dd {
background-color: var(--surface-color);
color: var(--on-surface-color);
padding: 10px 10px 10px 10px;
overflow-x: auto;
border-radius: 10px;
border: 3px solid rgba(128, 128, 128, 0.2);
border: 3px solid rgba(var(--border-color), 0.2);
}
.panelBody {
max-height: 800px;
overflow-y: scroll;
margin: 0 -10px -10px 0;
}
.panel > h2 {
......@@ -127,7 +133,16 @@ select,
button {
background-color: var(--surface-color);
color: var(--on-surface-color);
border: 2px solid rgba(var(--border-color), 0.4);
border-radius: 5px;
padding: 2px;
}
input:hover,
select:hover,
button:hover {
border: 2px solid rgba(var(--border-color), 0.6);
}
.colorbox {
width: 10px;
height: 10px;
......
......@@ -158,9 +158,11 @@ class App {
this._view.sourcePanel.data = processor.scripts
} catch(e) {
this._view.logFileReader.error = "Log file contains errors!"
throw(e);
} finally {
$("#container").className = "loaded";
this.fileLoaded = true;
}
$("#container").className = "loaded";
this.fileLoaded = true;
}
refreshTimelineTrackView() {
......
// 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 { defineCustomElement, V8CustomElement } from './helper.mjs';
import { DOM, V8CustomElement } from './helper.mjs';
defineCustomElement('log-file-reader', (templateText) =>
DOM.defineCustomElement('log-file-reader', (templateText) =>
class LogFileReader extends V8CustomElement {
constructor() {
super(templateText);
......
......@@ -6,9 +6,9 @@ import "./map-panel/map-details.mjs";
import "./map-panel/map-transitions.mjs";
import { FocusEvent } from './events.mjs';
import { MapLogEntry } from "./log/map.mjs";
import { defineCustomElement, V8CustomElement } from './helper.mjs';
import { DOM, V8CustomElement } from './helper.mjs';
defineCustomElement('map-panel', (templateText) =>
DOM.defineCustomElement('map-panel', (templateText) =>
class MapPanel extends V8CustomElement {
_map;
constructor() {
......
// 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 { V8CustomElement, defineCustomElement } from "../helper.mjs";
import { V8CustomElement, DOM} from "../helper.mjs";
import { FocusEvent } from "../events.mjs";
defineCustomElement(
DOM.defineCustomElement(
"./map-panel/map-details",
(templateText) =>
class MapDetails extends V8CustomElement {
......
// 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 { V8CustomElement, defineCustomElement, typeToColor } from "../helper.mjs";
import { V8CustomElement, DOM, typeToColor } from "../helper.mjs";
import { FocusEvent, SelectionEvent } from "../events.mjs";
defineCustomElement(
DOM.defineCustomElement(
"./map-panel/map-transitions",
(templateText) =>
class MapTransitions extends V8CustomElement {
......@@ -64,7 +64,7 @@ defineCustomElement(
}
_showMaps() {
this.transitionView.style.display = "none";
this.removeAllChildren(this.transitionView);
DOM.removeAllChildren(this.transitionView);
this._displayedMapsInTree = new Set();
// Limit view to 200 maps for performance reasons.
this.selectedMapLogEntries.slice(0, 200).forEach((map) =>
......@@ -121,9 +121,9 @@ defineCustomElement(
addTransitionEdge(map) {
let classes = ["transitionEdge"];
let edge = this.div(classes);
let edge = DOM.div(classes);
edge.style.backgroundColor = typeToColor(map.edge);
let labelNode = this.div("transitionLabel");
let labelNode = DOM.div("transitionLabel");
labelNode.innerText = map.edge.toString();
edge.appendChild(labelNode);
return edge;
......@@ -132,7 +132,7 @@ defineCustomElement(
addTransitionTo(map) {
// transition[ transitions[ transition[...], transition[...], ...]];
this._displayedMapsInTree?.add(map);
let transition = this.div("transition");
let transition = DOM.div("transition");
if (map.isDeprecated()) transition.classList.add("deprecated");
if (map.edge) {
transition.appendChild(this.addTransitionEdge(map));
......@@ -140,7 +140,7 @@ defineCustomElement(
let mapNode = this.addMapNode(map);
transition.appendChild(mapNode);
let subtree = this.div("transitions");
let subtree = DOM.div("transitions");
transition.appendChild(subtree);
this.currentNode.appendChild(transition);
......@@ -150,13 +150,13 @@ defineCustomElement(
}
addMapNode(map) {
let node = this.div("map");
let node = DOM.div("map");
if (map.edge) node.style.backgroundColor = typeToColor(map.edge);
node.map = map;
node.addEventListener("click", () => this.selectMap(map));
if (map.children.length > 1) {
node.innerText = map.children.length;
let showSubtree = this.div("showSubtransitions");
let showSubtree = DOM.div("showSubtransitions");
showSubtree.addEventListener("click", (e) =>
this.toggleSubtree(e, node)
);
......
......@@ -235,7 +235,7 @@ export class Processor extends LogReader {
slow_reason) {
let fnName = this.functionName(pc);
let parts = fnName.split(' ');
let fileName = parts[1];
let fileName = parts[parts.length-1];
let script = this.getScript(fileName);
// TODO: Use SourcePosition here directly
let entry = new IcLogEntry(
......
......@@ -19,7 +19,7 @@ found in the LICENSE file. -->
}
pre.scriptNode span::before {
content: counter(sourceLineCounter) " ";
content: counter(sourceLineCounter) ": ";
display: inline-block;
width: 4em;
padding-left: auto;
......@@ -27,26 +27,28 @@ found in the LICENSE file. -->
text-align: right;
}
mark {
width: 1ch;
height: 1lh;
border-radius: 0.1lh;
border: 0.5px var(--background-color) solid;
cursor: pointer;
}
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);
}
.marked {
background-color: var(--primary-color);
color: var(--on-primary-color);
}
#script-dropdown {
width: 100%;
margin-bottom: 10px;
}
</style>
<div class="panel">
<h2>Source Panel</h2>
<div class="script-dropdown">
<label for="scripts-label">Scripts:</label>
<select id="script-dropdown"></select>
</div>
<div id="script">
<select id="script-dropdown"></select>
<div id="script" class="panelBody">
<pre class="scripNode"></pre>
</div>
</div>
// 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 { V8CustomElement, defineCustomElement } from "./helper.mjs";
import { V8CustomElement, DOM, delay, formatBytes} from "./helper.mjs";
import { SelectionEvent, FocusEvent } from "./events.mjs";
import { MapLogEntry } from "./log/map.mjs";
import { IcLogEntry } from "./log/ic.mjs";
defineCustomElement(
DOM.defineCustomElement(
"source-panel",
(templateText) =>
class SourcePanel extends V8CustomElement {
_selectedSourcePositions;
_selectedSourcePositions = [];
_sourcePositionsToMarkNodes;
_scripts = [];
_script;
constructor() {
super(templateText);
this.scriptDropdown.addEventListener(
'change', e => this.handleSelectScript(e));
'change', e => this._handleSelectScript(e));
}
get script() {
return this.$('#script');
}
get scriptNode() {
return this.$('.scriptNode');
}
set script(script) {
if (this._script === script) return;
this._script = script;
// TODO: fix undefined scripts
if (script !== undefined) this.renderSourcePanel();
this._renderSourcePanel();
this._updateScriptDropdownSelection();
}
set selectedSourcePositions(sourcePositions) {
this._selectedSourcePositions = sourcePositions;
// TODO: highlight multiple scripts
this.script = sourcePositions[0]?.script;
this._focusSelectedMarkers();
}
get selectedSourcePositions() {
return this._selectedSourcePositions;
}
set data(value) {
this._scripts = value;
this.initializeScriptDropdown();
this.script = this._scripts[0];
set data(scripts) {
this._scripts = scripts;
this._initializeScriptDropdown();
}
get scriptDropdown() {
return this.$("#script-dropdown");
}
initializeScriptDropdown() {
_initializeScriptDropdown() {
this._scripts.sort((a, b) => a.name.localeCompare(b.name));
let select = this.scriptDropdown;
select.options.length = 0;
for (const script of this._scripts) {
const option = document.createElement("option");
option.text = `${script.name} (id=${script.id})`;
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;
}
renderSourcePanel() {
const builder = new LineBuilder(this, this._script);
const scriptNode = builder.createScriptNode();
async _renderSourcePanel() {
let scriptNode;
if (this._script) {
await delay(1);
const builder = new LineBuilder(
this, this._script, this._selectedSourcePositions);
scriptNode = builder.createScriptNode();
this._sourcePositionsToMarkNodes = builder.sourcePositionToMarkers;
} else {
scriptNode = document.createElement("pre");
this._selectedMarkNodes = undefined;
}
const oldScriptNode = this.script.childNodes[1];
this.script.replaceChild(scriptNode, oldScriptNode);
}
handleSelectScript(e) {
const option = this.scriptDropdown.options[this.scriptDropdown.selectedIndex];
async _focusSelectedMarkers() {
await delay(100);
// Remove all marked nodes.
for (let markNode of this._sourcePositionsToMarkNodes.values()) {
markNode.className = "";
}
for (let sourcePosition of this._selectedSourcePositions) {
this._sourcePositionsToMarkNodes
.get(sourcePosition).className = "marked";
}
const sourcePosition = this._selectedSourcePositions[0];
if (!sourcePosition) return;
const markNode = this._sourcePositionsToMarkNodes.get(sourcePosition);
markNode.scrollIntoView({
behavior: "smooth", block: "nearest", inline: "center"
});
}
_handleSelectScript(e) {
const option =
this.scriptDropdown.options[this.scriptDropdown.selectedIndex];
this.script = option.script;
this.selectLogEntries(this._script.entries());
}
handleSourcePositionClick(e) {
this.selectLogEntries(e.target.sourcePosition.entries)
}
selectLogEntries(logEntries) {
let icLogEntries = [];
let mapLogEntries = [];
for (const entry of e.target.sourcePosition.entries) {
for (const entry of logEntries) {
if (entry instanceof MapLogEntry) {
mapLogEntries.push(entry);
} else if (entry instanceof IcLogEntry) {
......@@ -79,14 +125,11 @@ defineCustomElement(
}
if (icLogEntries.length > 0 ) {
this.dispatchEvent(new SelectionEvent(icLogEntries));
this.dispatchEvent(new FocusEvent(icLogEntries[0]));
}
if (mapLogEntries.length > 0) {
this.dispatchEvent(new SelectionEvent(mapLogEntries));
this.dispatchEvent(new FocusEvent(mapLogEntries[0]));
}
}
}
);
......@@ -99,12 +142,19 @@ class SourcePositionIterator {
}
*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];
}
......@@ -131,13 +181,17 @@ function * lineIterator(source) {
if (current < source.length) yield [line, source.substring(current)];
}
class LineBuilder {
_script
_clickHandler
_sourcePositions
_script;
_clickHandler;
_sourcePositions;
_selection;
_sourcePositionToMarkers = new Map();
constructor(panel, script) {
constructor(panel, script, highlightPositions) {
this._script = script;
this._selection = new Set(highlightPositions);
this._clickHandler = panel.handleSourcePositionClick.bind(panel);
// TODO: sort on script finalization.
script.sourcePositions.sort((a, b) => {
......@@ -146,7 +200,10 @@ class LineBuilder {
})
this._sourcePositions
= new SourcePositionIterator(script.sourcePositions);
}
get sourcePositionToMarkers() {
return this._sourcePositionToMarkers;
}
createScriptNode() {
......@@ -179,7 +236,7 @@ class LineBuilder {
_createMarkerNode(text, sourcePosition) {
const marker = document.createElement("mark");
marker.classList.add('marked');
this._sourcePositionToMarkers.set(sourcePosition, marker);
marker.textContent = text;
marker.sourcePosition = sourcePosition;
marker.onclick = this._clickHandler;
......
// 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 { V8CustomElement, defineCustomElement } from "./helper.mjs";
import { V8CustomElement, DOM} from "./helper.mjs";
import { SelectionEvent } from "./events.mjs";
import { delay, LazyTable } from "./helper.mjs";
defineCustomElement(
DOM.defineCustomElement(
"stats-panel",
(templateText) =>
class StatsPanel extends V8CustomElement {
......@@ -80,11 +80,11 @@ defineCustomElement(
let tbody = document.createElement("tbody");
let total = this._selectedLogEntries.length;
pairs.forEach(([name, color, filter]) => {
let row = this.tr();
let row = DOM.tr();
if (color !== null) {
row.appendChild(this.td(this.div(["colorbox", color])));
row.appendChild(DOM.td(DOM.div(["colorbox", color])));
} else {
row.appendChild(this.td(""));
row.appendChild(DOM.td(""));
}
row.classList.add('clickable');
row.onclick = (e) => {
......@@ -95,11 +95,11 @@ defineCustomElement(
}
this.dispatchEvent(new SelectionEvent(node.maps));
};
row.appendChild(this.td(name));
row.appendChild(DOM.td(name));
let count = this.count(filter);
row.appendChild(this.td(count));
row.appendChild(DOM.td(count));
let percent = Math.round((count / total) * 1000) / 10;
row.appendChild(this.td(percent.toFixed(1) + "%"));
row.appendChild(DOM.td(percent.toFixed(1) + "%"));
tbody.appendChild(row);
});
this.$("#typeTable").replaceChild(tbody, this.$("#typeTable tbody"));
......@@ -117,7 +117,7 @@ defineCustomElement(
let rowData = Array.from(this._transitions.entries());
rowData.sort((a, b) => b[1].length - a[1].length);
new LazyTable(this.$("#nameTable"), rowData, ([name, maps]) => {
let row = this.tr();
let row = DOM.tr();
row.maps = maps;
row.classList.add('clickable');
row.addEventListener("click", (e) =>
......@@ -127,8 +127,8 @@ defineCustomElement(
)
)
);
row.appendChild(this.td(maps.length));
row.appendChild(this.td(name));
row.appendChild(DOM.td(maps.length));
row.appendChild(DOM.td(name));
return row;
});
}
......
......@@ -2,11 +2,11 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import { defineCustomElement, V8CustomElement } from './helper.mjs';
import { DOM, V8CustomElement } from './helper.mjs';
import { SynchronizeSelectionEvent } from './events.mjs';
import './timeline/timeline-track.mjs';
defineCustomElement('timeline-panel', (templateText) =>
DOM.defineCustomElement('timeline-panel', (templateText) =>
class TimelinePanel extends V8CustomElement {
constructor() {
super(templateText);
......
......@@ -2,16 +2,13 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import {
defineCustomElement, V8CustomElement, CSSColor, delay
} from '../helper.mjs';
import { DOM, V8CustomElement, CSSColor, delay } from '../helper.mjs';
import { kChunkWidth, kChunkHeight } from "../log/map.mjs";
import {
SelectionEvent, FocusEvent, SelectTimeEvent,
SynchronizeSelectionEvent
} from '../events.mjs';
const kColors = [
CSSColor.green,
CSSColor.violet,
......@@ -24,7 +21,7 @@ const kColors = [
CSSColor.secondaryColor,
];
defineCustomElement('./timeline/timeline-track', (templateText) =>
DOM.defineCustomElement('./timeline/timeline-track', (templateText) =>
class TimelineTrack extends V8CustomElement {
// TODO turn into static field once Safari supports it.
static get SELECTION_OFFSET() { return 10 };
......@@ -55,9 +52,9 @@ defineCustomElement('./timeline/timeline-track', (templateText) =>
let xPosition = e.clientX
// Update origin time in case we click on a handle.
if (this.isOnLeftHandle(xPosition)) {
xPosition = this.rightHandlePosX;
xPosition = this.rightHandlePosX;
} else if (this.isOnRightHandle(xPosition)) {
xPosition = this.leftHandlePosX;
xPosition = this.leftHandlePosX;
}
this._selectionOriginTime = this.positionToTime(xPosition);
}
......@@ -102,9 +99,9 @@ defineCustomElement('./timeline/timeline-track', (templateText) =>
const startPosition = this.timeToPosition(this._timeSelection.start);
const endPosition = this.timeToPosition(this._timeSelection.end);
const delta = endPosition - startPosition;
this.leftHandle.style.left = startPosition + "px";
this.selection.style.left = startPosition + "px";
this.rightHandle.style.left = endPosition + "px";
this.leftHandle.style.left = startPosition + "px";
this.selection.style.left = startPosition + "px";
this.rightHandle.style.left = endPosition + "px";
this.selection.style.width = delta + "px";
}
......@@ -165,6 +162,7 @@ defineCustomElement('./timeline/timeline-track', (templateText) =>
get timelineLegendContent() {
return this.$('#legendContent');
}
set data(value) {
this._timeline = value;
this._resetTypeToColorCache();
......@@ -223,33 +221,32 @@ defineCustomElement('./timeline/timeline-track', (templateText) =>
renderLegend() {
let timelineLegend = this.timelineLegend;
let timelineLegendContent = this.timelineLegendContent;
this.removeAllChildren(timelineLegendContent);
DOM.removeAllChildren(timelineLegendContent);
this._timeline.uniqueTypes.forEach((entries, type) => {
let row = this.tr();
let row = DOM.tr("clickable");
row.entries = entries;
row.classList.add('clickable');
row.addEventListener('dblclick', e => this.handleEntryTypeDblClick(e));
let color = this.typeToColor(type);
if (color !== null) {
let div = this.div(["colorbox"]);
let div = DOM.div("colorbox");
div.style.backgroundColor = color;
row.appendChild(this.td(div));
row.appendChild(DOM.td(div));
} else {
row.appendChild(this.td(""));
row.appendChild(DOM.td());
}
let td = this.td(type);
let td = DOM.td(type);
row.appendChild(td);
row.appendChild(this.td(entries.length));
row.appendChild(DOM.td(entries.length));
let percent = (entries.length / this.data.all.length) * 100;
row.appendChild(this.td(percent.toFixed(1) + "%"));
row.appendChild(DOM.td(percent.toFixed(1) + "%"));
timelineLegendContent.appendChild(row);
});
// Add Total row.
let row = this.tr();
row.appendChild(this.td(""));
row.appendChild(this.td("All"));
row.appendChild(this.td(this.data.all.length));
row.appendChild(this.td("100%"));
let row = DOM.tr();
row.appendChild(DOM.td(""));
row.appendChild(DOM.td("All"));
row.appendChild(DOM.td(this.data.all.length));
row.appendChild(DOM.td("100%"));
timelineLegendContent.appendChild(row);
timelineLegend.appendChild(timelineLegendContent);
}
......@@ -316,7 +313,7 @@ defineCustomElement('./timeline/timeline-track', (templateText) =>
updateTimeline() {
let chunksNode = this.timelineChunks;
this.removeAllChildren(chunksNode);
DOM.removeAllChildren(chunksNode);
let chunks = this.chunks;
let max = chunks.max(each => each.size());
let start = this.data.startTime;
......@@ -325,7 +322,7 @@ defineCustomElement('./timeline/timeline-track', (templateText) =>
this._timeToPixel = chunks.length * kChunkWidth / duration;
this._timeStartOffset = start * this._timeToPixel;
let addTimestamp = (time, name) => {
let timeNode = this.div('timestamp');
let timeNode = DOM.div('timestamp');
timeNode.innerText = name;
timeNode.style.left = ((time - start) * this._timeToPixel) + 'px';
chunksNode.appendChild(timeNode);
......@@ -336,7 +333,7 @@ defineCustomElement('./timeline/timeline-track', (templateText) =>
let height = (chunk.size() / max * kChunkHeight);
chunk.height = height;
if (chunk.isEmpty()) continue;
let node = this.div();
let node = DOM.div();
node.className = 'chunk';
node.style.left =
((chunks[i].start - start) * this._timeToPixel) + 'px';
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment