Commit 6078cb52 authored by Camillo Bruni's avatar Camillo Bruni Committed by V8 LUCI CQ

[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/+/3574544Reviewed-by: 's avatarPatrick Thier <pthier@chromium.org>
Commit-Queue: Camillo Bruni <cbruni@chromium.org>
Cr-Commit-Position: refs/heads/main@{#79897}
parent 8072d31a
......@@ -38,17 +38,17 @@ export class CsvParser {
escapeField(string) {
let nextPos = string.indexOf("\\");
if (nextPos === -1) return string;
let result = string.substring(0, nextPos);
let result = [string.substring(0, nextPos)];
// Escape sequences of the form \x00 and \u0000;
let pos = 0;
while (nextPos !== -1) {
const escapeIdentifier = string[nextPos + 1];
pos = nextPos + 2;
if (escapeIdentifier === 'n') {
result += '\n';
result.push('\n');
nextPos = pos;
} else if (escapeIdentifier === '\\') {
result += '\\';
result.push('\\');
nextPos = pos;
} else {
if (escapeIdentifier === 'x') {
......@@ -61,9 +61,9 @@ export class CsvParser {
// Convert the selected escape sequence to a single character.
const escapeChars = string.substring(pos, nextPos);
if (escapeChars === '2C') {
result += ',';
result.push(',');
} else {
result += String.fromCharCode(parseInt(escapeChars, 16));
result.push(String.fromCharCode(parseInt(escapeChars, 16)));
}
}
......@@ -72,13 +72,13 @@ export class CsvParser {
nextPos = string.indexOf("\\", pos);
// If there are no more escape sequences consume the rest of the string.
if (nextPos === -1) {
result += string.substr(pos);
result.push(string.substr(pos));
break;
} else if (pos !== nextPos) {
result += string.substring(pos, nextPos);
result.push(string.substring(pos, nextPos));
}
}
return result;
return result.join('');
}
/**
......
......@@ -8,7 +8,7 @@ export const GB = MB * KB;
export const kMillis2Seconds = 1 / 1000;
export const kMicro2Milli = 1 / 1000;
export function formatBytes(bytes) {
export function formatBytes(bytes, digits = 2) {
const units = ['B', 'KiB', 'MiB', 'GiB'];
const divisor = 1024;
let index = 0;
......@@ -16,7 +16,7 @@ export function formatBytes(bytes) {
index++;
bytes /= divisor;
}
return bytes.toFixed(2) + units[index];
return bytes.toFixed(digits) + units[index];
}
export function formatMicroSeconds(micro) {
......@@ -51,3 +51,18 @@ export function calcOffsetInVMCage(address) {
let ret = Number(address & mask);
return ret;
}
export function delay(time) {
return new Promise(resolver => setTimeout(resolver, time));
}
export function defer() {
let resolve_func, reject_func;
const p = new Promise((resolve, reject) => {
resolve_func = resolve;
reject_func = resolve;
});
p.resolve = resolve_func;
p.reject = reject_func;
return p;
}
......@@ -40,6 +40,7 @@ found in the LICENSE file. -->
#loader {
display: none;
will-change: rotate;
}
.loading #loader {
......@@ -53,17 +54,38 @@ found in the LICENSE file. -->
background-color: var(--file-reader-background-color);
}
#spinner {
#spinner, #progress, #progressText {
position: absolute;
width: 100px;
height: 100px;
width: 120px;
height: 120px;
top: 40%;
left: 50%;
margin-left: -50px;
border: 30px solid var(--surface-color);
border-top: 30px solid var(--primary-color);
margin-left: calc(-60px - 10px);
border-radius: 50%;
animation: spin 1s ease-in-out infinite;
}
#spinner {
border: 20px solid var(--surface-color);
border-top: 20px solid var(--primary-color);
animation: spin 1s linear infinite;
will-change: transform;
transform: scale(1.1);
}
#progress, #progressText {
padding: 20px;
}
#progress {
transition: all 0.5s ease-in-out;
}
#progressText {
line-height: 120px;
font-size: 28px;
transform: scale(0.55);
text-align: center;
vertical-align: middle;
background-color: var(--surface-color);
}
#label {
......@@ -88,6 +110,8 @@ found in the LICENSE file. -->
<input id="file" type="file" name="file" />
</div>
<div id="loader">
<div id="progress"></div>
<div id="spinner"></div>
<div id="progressText"></div>
</div>
</div>
......@@ -2,6 +2,8 @@
// 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);
......@@ -50,12 +52,14 @@ export class V8CustomElement extends HTMLElement {
export class FileReader extends V8CustomElement {
constructor(templateText) {
super(templateText);
this.addEventListener('click', (e) => this.handleClick(e));
this.addEventListener('dragover', (e) => this.handleDragOver(e));
this.addEventListener('drop', (e) => this.handleChange(e));
this.$('#file').addEventListener('change', (e) => this.handleChange(e));
this.$('#fileReader')
.addEventListener('keydown', (e) => this.handleKeyEvent(e));
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) {
......@@ -78,8 +82,6 @@ export class FileReader extends V8CustomElement {
handleChange(event) {
// Used for drop and file change.
event.preventDefault();
this.dispatchEvent(
new CustomEvent('fileuploadstart', {bubbles: true, composed: true}));
const host = event.dataTransfer ? event.dataTransfer : event.target;
this.readFile(host.files[0]);
}
......@@ -92,26 +94,50 @@ export class FileReader extends V8CustomElement {
this.fileReader.focus();
}
get fileReader() {
return this.$('#fileReader');
}
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);
......@@ -137,7 +163,10 @@ export class FileReader extends V8CustomElement {
const readResult = await reader.read();
chunk = readResult.value;
readerDone = readResult.done;
if (chunk) this._handleFileChunk(chunk);
if (!chunk) break;
this._handleFileChunk(chunk);
// Artificial delay to allow for layout updates.
await delay(5);
} while (!readerDone);
}
......
......@@ -44,7 +44,7 @@ export const parseVarArgs = 'parse-var-args';
* @constructor
*/
export class LogReader {
constructor (timedRange, pairwiseTimedRange) {
constructor(timedRange=false, pairwiseTimedRange=false) {
this.dispatchTable_ = new Map();
this.timedRange_ = timedRange;
this.pairwiseTimedRange_ = pairwiseTimedRange;
......
......@@ -234,6 +234,35 @@ export class Script {
}
const kOffsetPairRegex = /C([0-9]+)O([0-9]+)/g;
class SourcePositionTable {
constructor(encodedTable) {
this._offsets = [];
while (true) {
const regexResult = kOffsetPairRegex.exec(encodedTable);
if (!regexResult) break;
const codeOffset = parseInt(regexResult[1]);
const scriptOffset = parseInt(regexResult[2]);
if (isNaN(codeOffset) || isNaN(scriptOffset)) continue;
this._offsets.push({code: codeOffset, script: scriptOffset});
}
}
getScriptOffset(codeOffset) {
if (codeOffset < 0) {
throw new Exception(`Invalid codeOffset=${codeOffset}, should be >= 0`);
}
for (let i = this.offsetTable.length - 1; i >= 0; i--) {
const offset = this._offsets[i];
if (offset.code <= codeOffset) {
return offset.script;
}
}
return this._offsets[0].script;
}
}
class SourceInfo {
script;
start;
......@@ -243,13 +272,16 @@ class SourceInfo {
fns;
disassemble;
setSourcePositionInfo(script, startPos, endPos, sourcePositionTable, inliningPositions, inlinedFunctions) {
setSourcePositionInfo(
script, startPos, endPos, sourcePositionTableData, inliningPositions,
inlinedFunctions) {
this.script = script;
this.start = startPos;
this.end = endPos;
this.positions = sourcePositionTable;
this.positions = sourcePositionTableData;
this.inlined = inliningPositions;
this.fns = inlinedFunctions;
this.sourcePositionTable = new SourcePositionTable(sourcePositionTableData);
}
setDisassemble(code) {
......
......@@ -2,21 +2,6 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
export function delay(time) {
return new Promise(resolver => setTimeout(resolver, time));
}
export function defer() {
let resolve_func, reject_func;
const p = new Promise((resolve, reject) => {
resolve_func = resolve;
reject_func = resolve;
});
p.resolve = resolve_func;
p.reject = reject_func;
return p;
}
export class Group {
constructor(key, id, parentTotal, entries) {
this.key = key;
......
......@@ -179,9 +179,9 @@ button:hover {
.colorbox {
display: inline-block;
width: 10px;
height: 10px;
border: 1px var(--background-color) solid;
width: 8px;
height: 8px;
border: 2px var(--background-color) solid;
border-radius: 50%;
}
......
......@@ -15,9 +15,8 @@ import {MapLogEntry} from './log/map.mjs';
import {TickLogEntry} from './log/tick.mjs';
import {TimerLogEntry} from './log/timer.mjs';
import {Processor} from './processor.mjs';
import {Timeline} from './timeline.mjs'
import {FocusEvent, SelectionEvent, SelectRelatedEvent, SelectTimeEvent, ToolTipEvent,} from './view/events.mjs';
import {$, CSSColor, groupBy} from './view/helper.mjs';
import {$, groupBy} from './view/helper.mjs';
class App {
_state;
......@@ -51,11 +50,11 @@ class App {
toolTip: $('#tool-tip'),
};
this._view.logFileReader.addEventListener(
'fileuploadstart', (e) => this.handleFileUploadStart(e));
'fileuploadstart', this.handleFileUploadStart.bind(this));
this._view.logFileReader.addEventListener(
'fileuploadchunk', (e) => this.handleFileUploadChunk(e));
'fileuploadchunk', this.handleFileUploadChunk.bind(this));
this._view.logFileReader.addEventListener(
'fileuploadend', (e) => this.handleFileUploadEnd(e));
'fileuploadend', this.handleFileUploadEnd.bind(this));
this._startupPromise = this._loadCustomElements();
this._view.codeTrack.svg = true;
}
......@@ -91,14 +90,14 @@ class App {
document.addEventListener(
'keydown', e => this._navigation?.handleKeyDown(e));
document.addEventListener(
SelectRelatedEvent.name, e => this.handleSelectRelatedEntries(e));
SelectRelatedEvent.name, this.handleSelectRelatedEntries.bind(this));
document.addEventListener(
SelectionEvent.name, e => this.handleSelectEntries(e))
SelectionEvent.name, this.handleSelectEntries.bind(this))
document.addEventListener(
FocusEvent.name, e => this.handleFocusLogEntry(e));
FocusEvent.name, this.handleFocusLogEntry.bind(this));
document.addEventListener(
SelectTimeEvent.name, e => this.handleTimeRangeSelect(e));
document.addEventListener(ToolTipEvent.name, e => this.handleToolTip(e));
SelectTimeEvent.name, this.handleTimeRangeSelect.bind(this));
document.addEventListener(ToolTipEvent.name, this.handleToolTip.bind(this));
}
handleSelectRelatedEntries(e) {
......@@ -362,6 +361,8 @@ class App {
this.restartApp();
$('#container').className = 'initial';
this._processor = new Processor();
this._processor.setProgressCallback(
e.detail.totalSize, e.detail.progressCallback);
}
async handleFileUploadChunk(e) {
......
......@@ -30,7 +30,7 @@ export class TimerLogEntry extends LogEntry {
}
get duration() {
return this._endTime - this._time;
return Math.max(0, this._endTime - this._time);
}
covers(time) {
......@@ -53,4 +53,4 @@ export class TimerLogEntry extends LogEntry {
'duration',
];
}
}
\ No newline at end of file
}
......@@ -61,6 +61,11 @@ export class Processor extends LogReader {
_lastCodeLogEntry;
_lastTickLogEntry;
_chunkRemainder = '';
_totalInputBytes = 0;
_processedInputChars = 0;
_progressCallback;
MAJOR_VERSION = 7;
MINOR_VERSION = 6;
constructor() {
......@@ -205,7 +210,22 @@ export class Processor extends LogReader {
this._chunkConsumer.push(chunk)
}
setProgressCallback(totalSize, callback) {
this._totalInputBytes = totalSize;
this._progressCallback = callback;
}
async _updateProgress() {
if (!this._progressCallback) return;
// We use chars and bytes interchangeably for simplicity. This causes us to
// slightly underestimate progress.
this._progressCallback(
this._processedInputChars / this._totalInputBytes,
this._processedInputChars);
}
async _processChunk(chunk) {
const prevProcessedInputChars = this._processedInputChars;
let end = chunk.length;
let current = 0;
let next = 0;
......@@ -226,7 +246,9 @@ export class Processor extends LogReader {
current = next + 1;
lineNumber++;
await this.processLogLine(line);
this._processedInputChars = prevProcessedInputChars + current;
}
this._updateProgress();
} catch (e) {
console.error(
`Could not parse log line ${lineNumber}, trying to continue: ${e}`);
......
// 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 {arrayEquals, defer, groupBy} from '../helper.mjs';
import {App} from '../index.mjs'
import {SelectionEvent, SelectRelatedEvent, ToolTipEvent} from './events.mjs';
import {CollapsableElement, CSSColor, delay, DOM, formatBytes, gradientStopsFromGroups, LazyTable} from './helper.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.
......
......@@ -2,10 +2,9 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import {delay} from '../../helper.mjs';
import {kChunkHeight, kChunkVisualWidth, kChunkWidth} from '../../log/map.mjs';
import {SelectionEvent, SelectTimeEvent, SynchronizeSelectionEvent, ToolTipEvent,} from '../events.mjs';
import {CSSColor, DOM, formatDurationMicros, SVG, V8CustomElement} from '../helper.mjs';
import {CSSColor, delay, DOM, formatDurationMicros, V8CustomElement} from '../helper.mjs';
export const kTimelineHeight = 200;
......@@ -41,9 +40,9 @@ export class TimelineTrackBase extends V8CustomElement {
}
_initEventListeners() {
this._legend.onFilter = (type) => this._handleFilterTimeline();
this._legend.onFilter = this._handleFilterTimeline.bind(this);
this.timelineNode.addEventListener(
'scroll', e => this._handleTimelineScroll(e));
'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);
......@@ -64,6 +63,7 @@ export class TimelineTrackBase extends V8CustomElement {
_handleFilterTimeline(type) {
this._updateChunks();
this._legend.update(true);
}
set data(timeline) {
......@@ -597,24 +597,39 @@ class Legend {
return this._typesFilters.get(logEntry.type);
}
update() {
if (this._lastSelection === this.selection) return;
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();
this.selection.getBreakdown(undefined, this._enableDuration)
.forEach(group => {
tbody.appendChild(this._addTypeRow(group));
missingTypes.delete(group.key);
});
missingTypes.forEach(
key => tbody.appendChild(this._addRow('', key, 0, '0%')));
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%'));
tbody.appendChild(this._addRow(
'', 'Selection', this.selection.length, '100%', selectionDuration,
'100%'));
}
tbody.appendChild(this._addRow('', 'All', this._timeline.length, ''));
// 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);
}
......@@ -628,8 +643,9 @@ class Legend {
_addRow(colorNode, type, count, countPercent, duration, durationPercent) {
const row = DOM.tr();
row.appendChild(DOM.td(colorNode));
const typeCell = row.appendChild(DOM.td(type));
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));
......@@ -640,26 +656,31 @@ class Legend {
return row
}
_addTypeRow(group) {
_addTypeRow(group, selectionDuration) {
const color = this.colorForType(group.key);
const colorDiv = DOM.div('colorbox');
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.borderColor = color;
colorDiv.style.backgroundColor = CSSColor.backgroundImage;
}
let duration = 0;
let durationPercent = '';
if (this._enableDuration) {
const entries = group.entries;
for (let i = 0; i < entries.length; i++) {
duration += entries[i].duration;
}
// group.duration was added in _breakdownTotalDuration.
duration = group.duration;
durationPercent = selectionDuration == 0 ?
'0%' :
this._formatPercent(duration / selectionDuration);
}
let countPercent =
`${(group.length / this.selection.length * 100).toFixed(1)}%`;
const countPercent =
this._formatPercent(group.length / this.selection.length);
const row = this._addRow(
colorDiv, group.key, group.length, countPercent, duration, '');
colorDiv, group.key, group.length, countPercent, duration,
durationPercent);
row.className = 'clickable';
row.onclick = this._typeClickHandler;
row.data = group.key;
......@@ -671,4 +692,26 @@ class Legend {
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)}%`;
}
}
......@@ -2,10 +2,9 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import {delay} from '../../helper.mjs';
import {Timeline} from '../../timeline.mjs';
import {SelectTimeEvent} from '../events.mjs';
import {CSSColor, DOM, SVG} from '../helper.mjs';
import {CSSColor, delay, SVG} from '../helper.mjs';
import {TimelineTrackBase} from './timeline-track-base.mjs'
......
......@@ -69,14 +69,21 @@ found in the LICENSE file. -->
height: calc(var(--view-height) + 12px);
overflow-y: scroll;
margin-right: -10px;
padding-right: 2px;
padding: 0 2px 0 2px;
width: 400px;
border-left: 1px solid var(--border-color);
}
#legendTable {
width: 280px;
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;
......@@ -84,24 +91,20 @@ found in the LICENSE file. -->
#legendTable td {
padding-top: 3px;
text-align: right;
}
/* Center colors */
#legendTable td:nth-of-type(4n+1) {
#legendTable .color {
text-align: center;
}
/* Left align text*/
#legendTable td:nth-of-type(4n+2) {
#legendTable .text {
text-align: left;
width: 100%;
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
}
/* right align numbers */
#legendTable td:nth-of-type(4n+3),
#legendTable td:nth-of-type(4n+4) {
text-align: right;
}
.timeline {
background-color: var(--timeline-background-color);
......@@ -163,6 +166,9 @@ found in the LICENSE file. -->
.legend {
flex: initial;
}
.colorbox.empty {
opacity: 0.5;
}
</style>
<style>
/* SVG styles */
......@@ -235,7 +241,7 @@ found in the LICENSE file. -->
<thead>
<tr>
<td></td>
<td>Type</td>
<td class="text">Type</td>
<td>Count</td>
<td></td>
</tr>
......
......@@ -2,10 +2,10 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import {delay} from '../../helper.mjs';
import {TickLogEntry} from '../../log/tick.mjs';
import {Timeline} from '../../timeline.mjs';
import {DOM, SVG} from '../helper.mjs';
import {delay, DOM, SVG} from '../helper.mjs';
import {TimelineTrackStackedBase} from './timeline-track-stacked-base.mjs'
class Flame {
......
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