v8/tools/js/web-api-helper.mjs
Camillo Bruni 6078cb5283 [tools][system-analyzer] Various improvements
- Parse the condensed source position info support for jitted code
- Add progress bar/circle to loader
- Use temporary Array instead of concatenated strings in escapeField to
  reduce gc pressure
- Use bound functions as event handlers in more places
- Various timeline legend fixes:
  - Fix columns alignment when duration is present
  - Use fixed width to avoid breaking the UI
  - Correctly show total/percents for 'All' and 'Selection' entries
  - Improve usability of filtering buttons: added tooltips and fixed
    redrawing on filtering

Bug: v8:10644
Change-Id: I1275b31b7b13a05d9d6283d3067c1032d2d4819c
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/3574544
Reviewed-by: Patrick Thier <pthier@chromium.org>
Commit-Queue: Camillo Bruni <cbruni@chromium.org>
Cr-Commit-Position: refs/heads/main@{#79897}
2022-04-08 19:46:22 +00:00

296 lines
7.9 KiB
JavaScript

// 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 {delay, formatBytes} from './helper.mjs';
export class V8CustomElement extends HTMLElement {
_updateTimeoutId;
_updateCallback = this.forceUpdate.bind(this);
constructor(templateText) {
super();
const shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.innerHTML = templateText;
}
$(id) {
return this.shadowRoot.querySelector(id);
}
querySelectorAll(query) {
return this.shadowRoot.querySelectorAll(query);
}
requestUpdate(useAnimation = false) {
if (useAnimation) {
window.cancelAnimationFrame(this._updateTimeoutId);
this._updateTimeoutId =
window.requestAnimationFrame(this._updateCallback);
} else {
// Use timeout tasks to asynchronously update the UI without blocking.
clearTimeout(this._updateTimeoutId);
const kDelayMs = 5;
this._updateTimeoutId = setTimeout(this._updateCallback, kDelayMs);
}
}
forceUpdate() {
this._updateTimeoutId = undefined;
this._update();
}
_update() {
throw Error('Subclass responsibility');
}
get isFocused() {
return document.activeElement === this;
}
}
export class FileReader extends V8CustomElement {
constructor(templateText) {
super(templateText);
this.addEventListener('click', this.handleClick.bind(this));
this.addEventListener('dragover', this.handleDragOver.bind(this));
this.addEventListener('drop', this.handleChange.bind(this));
this.$('#file').addEventListener('change', this.handleChange.bind(this));
this.fileReader = this.$('#fileReader');
this.fileReader.addEventListener('keydown', this.handleKeyEvent.bind(this));
this.progressNode = this.$('#progress');
this.progressTextNode = this.$('#progressText');
}
set error(message) {
this._updateLabel(message);
this.root.className = 'fail';
}
_updateLabel(text) {
this.$('#label').innerText = text;
}
handleKeyEvent(event) {
if (event.key == 'Enter') this.handleClick(event);
}
handleClick(event) {
this.$('#file').click();
}
handleChange(event) {
// Used for drop and file change.
event.preventDefault();
const host = event.dataTransfer ? event.dataTransfer : event.target;
this.readFile(host.files[0]);
}
handleDragOver(event) {
event.preventDefault();
}
connectedCallback() {
this.fileReader.focus();
}
get root() {
return this.$('#root');
}
setProgress(progress, processedBytes = 0) {
this.progress = Math.max(0, Math.min(progress, 1));
this.processedBytes = processedBytes;
}
updateProgressBar() {
// Create a circular progress bar, starting at 12 o'clock.
this.progressNode.style.backgroundImage = `conic-gradient(
var(--primary-color) 0%,
var(--primary-color) ${this.progress * 100}%,
var(--surface-color) ${this.progress * 100}%)`;
this.progressTextNode.innerText =
this.processedBytes ? formatBytes(this.processedBytes, 1) : '';
if (this.root.className == 'loading') {
window.requestAnimationFrame(() => this.updateProgressBar());
}
}
readFile(file) {
this.dispatchEvent(new CustomEvent('fileuploadstart', {
bubbles: true,
composed: true,
detail: {
progressCallback: this.setProgress.bind(this),
totalSize: file.size,
}
}));
if (!file) {
this.error = 'Failed to load file.';
return;
}
this.fileReader.blur();
this.setProgress(0);
this.root.className = 'loading';
// Delay the loading a bit to allow for CSS animations to happen.
window.requestAnimationFrame(() => this.asyncReadFile(file));
}
async asyncReadFile(file) {
this.updateProgressBar();
const decoder = globalThis.TextDecoderStream;
if (decoder) {
await this._streamFile(file, decoder);
} else {
await this._readFullFile(file);
}
this._updateLabel(`Finished loading '${file.name}'.`);
this.dispatchEvent(
new CustomEvent('fileuploadend', {bubbles: true, composed: true}));
this.root.className = 'done';
}
async _readFullFile(file) {
const text = await file.text();
this._handleFileChunk(text);
}
async _streamFile(file, decoder) {
const stream = file.stream().pipeThrough(new decoder());
const reader = stream.getReader();
let chunk, readerDone;
do {
const readResult = await reader.read();
chunk = readResult.value;
readerDone = readResult.done;
if (!chunk) break;
this._handleFileChunk(chunk);
// Artificial delay to allow for layout updates.
await delay(5);
} while (!readerDone);
}
_handleFileChunk(chunk) {
this.dispatchEvent(new CustomEvent('fileuploadchunk', {
bubbles: true,
composed: true,
detail: chunk,
}));
}
}
export class DOM {
static element(type, options) {
const node = document.createElement(type);
if (options === undefined) return node;
if (typeof options === 'string') {
// Old behaviour: options = class string
node.className = options;
} else if (Array.isArray(options)) {
// Old behaviour: options = class array
DOM.addClasses(node, options);
} else {
// New behaviour: options = attribute dict
for (const [key, value] of Object.entries(options)) {
if (key == 'className') {
node.className = value;
} else if (key == 'classList') {
DOM.addClasses(node, value);
} else if (key == 'textContent') {
node.textContent = value;
} else if (key == 'children') {
for (const child of value) {
node.appendChild(child);
}
} else {
node.setAttribute(key, value);
}
}
}
return node;
}
static addClasses(node, classes) {
const classList = node.classList;
if (typeof classes === 'string') {
classList.add(classes);
} else {
for (let i = 0; i < classes.length; i++) {
classList.add(classes[i]);
}
}
return node;
}
static text(string) {
return document.createTextNode(string);
}
static button(label, clickHandler) {
const button = DOM.element('button');
button.innerText = label;
if (typeof clickHandler != 'function') {
throw new Error(
`DOM.button: Expected function but got clickHandler=${clickHandler}`);
}
button.onclick = clickHandler;
return button;
}
static div(options) {
return this.element('div', options);
}
static span(options) {
return this.element('span', options);
}
static table(options) {
return this.element('table', options);
}
static tbody(options) {
return this.element('tbody', options);
}
static td(textOrNode, className) {
const node = this.element('td');
if (typeof textOrNode === 'object') {
node.appendChild(textOrNode);
} else if (textOrNode) {
node.innerText = textOrNode;
}
if (className) node.className = className;
return node;
}
static tr(classes) {
return this.element('tr', classes);
}
static removeAllChildren(node) {
let range = document.createRange();
range.selectNodeContents(node);
range.deleteContents();
}
static defineCustomElement(
path, nameOrGenerator, maybeGenerator = undefined) {
let generator = nameOrGenerator;
let name = nameOrGenerator;
if (typeof nameOrGenerator == 'function') {
console.assert(maybeGenerator === undefined);
name = path.substring(path.lastIndexOf('/') + 1, path.length);
} else {
console.assert(typeof nameOrGenerator == 'string');
generator = maybeGenerator;
}
path = path + '-template.html';
fetch(path)
.then(stream => stream.text())
.then(
templateText =>
customElements.define(name, generator(templateText)));
}
}