89e0d45c66
- Create SourcePosition objects for Map and IC log entries - Display source code with markers for SourcePositions - Avoid some try-catches for a better debugging experience Bug: v8:10644 Change-Id: I559b0eaeaa1442986a00d2ef720d19ba85178509 Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/2424258 Reviewed-by: Sathya Gunasekaran <gsathya@chromium.org> Commit-Queue: Camillo Bruni <cbruni@chromium.org> Auto-Submit: Camillo Bruni <cbruni@chromium.org> Cr-Commit-Position: refs/heads/master@{#70091}
225 lines
6.9 KiB
JavaScript
225 lines
6.9 KiB
JavaScript
// 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 { Group } from './ic-model.mjs';
|
|
import Processor from "./processor.mjs";
|
|
import { MapLogEvent } from "./log/map.mjs";
|
|
import { FocusEvent, SelectTimeEvent, SelectionEvent } from './events.mjs';
|
|
import { defineCustomElement, V8CustomElement } from './helper.mjs';
|
|
|
|
defineCustomElement('ic-panel', (templateText) =>
|
|
class ICPanel extends V8CustomElement {
|
|
#selectedLogEvents;
|
|
#timeline;
|
|
constructor() {
|
|
super(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!");
|
|
this.#timeline = value;
|
|
this.selectedLogEvents = this.#timeline.all;
|
|
this.updateCount();
|
|
}
|
|
get groupKey() {
|
|
return this.$('#group-key');
|
|
}
|
|
|
|
get table() {
|
|
return this.$('#table');
|
|
}
|
|
|
|
get tableBody() {
|
|
return this.$('#table-body');
|
|
}
|
|
|
|
get count() {
|
|
return this.$('#count');
|
|
}
|
|
|
|
get spanSelectAll() {
|
|
return this.querySelectorAll("span");
|
|
}
|
|
|
|
set selectedLogEvents(value) {
|
|
this.#selectedLogEvents = value;
|
|
this.updateCount();
|
|
this.updateTable();
|
|
}
|
|
|
|
updateCount() {
|
|
this.count.innerHTML = this.selectedLogEvents.length;
|
|
}
|
|
|
|
get selectedLogEvents() {
|
|
return this.#selectedLogEvents;
|
|
}
|
|
|
|
updateTable(event) {
|
|
let select = this.groupKey;
|
|
let key = select.options[select.selectedIndex].text;
|
|
let tableBody = this.tableBody;
|
|
this.removeAllChildren(tableBody);
|
|
let groups = Group.groupBy(this.selectedLogEvents, key, true);
|
|
this.render(groups, tableBody);
|
|
}
|
|
|
|
escapeHtml(unsafe) {
|
|
if (!unsafe) return "";
|
|
return unsafe.toString()
|
|
.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, """)
|
|
.replace(/'/g, "'");
|
|
}
|
|
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 selectedMapLogEvents =
|
|
this.searchIcLogEventToMapLogEvent(id, entry.entries);
|
|
this.dispatchEvent(new SelectionEvent(selectedMapLogEvents));
|
|
}
|
|
|
|
searchIcLogEventToMapLogEvent(id, icLogEvents) {
|
|
// searches for mapLogEvents using the id, time
|
|
const selectedMapLogEventsSet = new Set();
|
|
for (const icLogEvent of icLogEvents) {
|
|
const time = icLogEvent.time;
|
|
const selectedMap = MapLogEvent.get(id, time);
|
|
selectedMapLogEventsSet.add(selectedMap);
|
|
}
|
|
return Array.from(selectedMapLogEventsSet);
|
|
}
|
|
|
|
//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));
|
|
}
|
|
|
|
render(entries, parent) {
|
|
let fragment = document.createDocumentFragment();
|
|
let max = Math.min(1000, entries.length)
|
|
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));
|
|
}
|
|
let details = this.td(tr, '<span>ℹ</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;
|
|
if (omitted > 0) {
|
|
let tr = document.createElement("tr");
|
|
let tdNode = this.td(tr, 'Omitted ' + omitted + " entries.");
|
|
tdNode.colSpan = 4;
|
|
fragment.appendChild(tr);
|
|
}
|
|
parent.appendChild(fragment);
|
|
}
|
|
|
|
|
|
renderDrilldown(entry, previousSibling) {
|
|
let tr = document.createElement('tr');
|
|
tr.className = "entry-details";
|
|
tr.style.display = "none";
|
|
// indent by one td.
|
|
tr.appendChild(document.createElement("td"));
|
|
let td = document.createElement("td");
|
|
td.colSpan = 3;
|
|
for (let key in entry.groups) {
|
|
td.appendChild(this.renderDrilldownGroup(entry, 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;
|
|
}
|
|
|
|
initGroupKeySelect() {
|
|
let select = this.groupKey;
|
|
select.options.length = 0;
|
|
for (let i in Processor.kProperties) {
|
|
let option = document.createElement("option");
|
|
option.text = Processor.kProperties[i];
|
|
select.add(option);
|
|
}
|
|
}
|
|
|
|
handleICTimeFilter(e) {
|
|
this.dispatchEvent(new SelectTimeEvent(
|
|
parseInt(this.$('#filter-time-start').value),
|
|
parseInt(this.$('#filter-time-end').value)));
|
|
}
|
|
|
|
});
|