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 { ...@@ -38,17 +38,17 @@ export class CsvParser {
escapeField(string) { escapeField(string) {
let nextPos = string.indexOf("\\"); let nextPos = string.indexOf("\\");
if (nextPos === -1) return string; 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; // Escape sequences of the form \x00 and \u0000;
let pos = 0; let pos = 0;
while (nextPos !== -1) { while (nextPos !== -1) {
const escapeIdentifier = string[nextPos + 1]; const escapeIdentifier = string[nextPos + 1];
pos = nextPos + 2; pos = nextPos + 2;
if (escapeIdentifier === 'n') { if (escapeIdentifier === 'n') {
result += '\n'; result.push('\n');
nextPos = pos; nextPos = pos;
} else if (escapeIdentifier === '\\') { } else if (escapeIdentifier === '\\') {
result += '\\'; result.push('\\');
nextPos = pos; nextPos = pos;
} else { } else {
if (escapeIdentifier === 'x') { if (escapeIdentifier === 'x') {
...@@ -61,9 +61,9 @@ export class CsvParser { ...@@ -61,9 +61,9 @@ export class CsvParser {
// Convert the selected escape sequence to a single character. // Convert the selected escape sequence to a single character.
const escapeChars = string.substring(pos, nextPos); const escapeChars = string.substring(pos, nextPos);
if (escapeChars === '2C') { if (escapeChars === '2C') {
result += ','; result.push(',');
} else { } else {
result += String.fromCharCode(parseInt(escapeChars, 16)); result.push(String.fromCharCode(parseInt(escapeChars, 16)));
} }
} }
...@@ -72,13 +72,13 @@ export class CsvParser { ...@@ -72,13 +72,13 @@ export class CsvParser {
nextPos = string.indexOf("\\", pos); nextPos = string.indexOf("\\", pos);
// If there are no more escape sequences consume the rest of the string. // If there are no more escape sequences consume the rest of the string.
if (nextPos === -1) { if (nextPos === -1) {
result += string.substr(pos); result.push(string.substr(pos));
break; break;
} else if (pos !== nextPos) { } 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; ...@@ -8,7 +8,7 @@ export const GB = MB * KB;
export const kMillis2Seconds = 1 / 1000; export const kMillis2Seconds = 1 / 1000;
export const kMicro2Milli = 1 / 1000; export const kMicro2Milli = 1 / 1000;
export function formatBytes(bytes) { export function formatBytes(bytes, digits = 2) {
const units = ['B', 'KiB', 'MiB', 'GiB']; const units = ['B', 'KiB', 'MiB', 'GiB'];
const divisor = 1024; const divisor = 1024;
let index = 0; let index = 0;
...@@ -16,7 +16,7 @@ export function formatBytes(bytes) { ...@@ -16,7 +16,7 @@ export function formatBytes(bytes) {
index++; index++;
bytes /= divisor; bytes /= divisor;
} }
return bytes.toFixed(2) + units[index]; return bytes.toFixed(digits) + units[index];
} }
export function formatMicroSeconds(micro) { export function formatMicroSeconds(micro) {
...@@ -51,3 +51,18 @@ export function calcOffsetInVMCage(address) { ...@@ -51,3 +51,18 @@ export function calcOffsetInVMCage(address) {
let ret = Number(address & mask); let ret = Number(address & mask);
return ret; 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. --> ...@@ -40,6 +40,7 @@ found in the LICENSE file. -->
#loader { #loader {
display: none; display: none;
will-change: rotate;
} }
.loading #loader { .loading #loader {
...@@ -53,17 +54,38 @@ found in the LICENSE file. --> ...@@ -53,17 +54,38 @@ found in the LICENSE file. -->
background-color: var(--file-reader-background-color); background-color: var(--file-reader-background-color);
} }
#spinner { #spinner, #progress, #progressText {
position: absolute; position: absolute;
width: 100px; width: 120px;
height: 100px; height: 120px;
top: 40%; top: 40%;
left: 50%; left: 50%;
margin-left: -50px; margin-left: calc(-60px - 10px);
border: 30px solid var(--surface-color);
border-top: 30px solid var(--primary-color);
border-radius: 50%; 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 { #label {
...@@ -88,6 +110,8 @@ found in the LICENSE file. --> ...@@ -88,6 +110,8 @@ found in the LICENSE file. -->
<input id="file" type="file" name="file" /> <input id="file" type="file" name="file" />
</div> </div>
<div id="loader"> <div id="loader">
<div id="progress"></div>
<div id="spinner"></div> <div id="spinner"></div>
<div id="progressText"></div>
</div> </div>
</div> </div>
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import {delay, formatBytes} from './helper.mjs';
export class V8CustomElement extends HTMLElement { export class V8CustomElement extends HTMLElement {
_updateTimeoutId; _updateTimeoutId;
_updateCallback = this.forceUpdate.bind(this); _updateCallback = this.forceUpdate.bind(this);
...@@ -50,12 +52,14 @@ export class V8CustomElement extends HTMLElement { ...@@ -50,12 +52,14 @@ export class V8CustomElement extends HTMLElement {
export class FileReader extends V8CustomElement { export class FileReader extends V8CustomElement {
constructor(templateText) { constructor(templateText) {
super(templateText); super(templateText);
this.addEventListener('click', (e) => this.handleClick(e)); this.addEventListener('click', this.handleClick.bind(this));
this.addEventListener('dragover', (e) => this.handleDragOver(e)); this.addEventListener('dragover', this.handleDragOver.bind(this));
this.addEventListener('drop', (e) => this.handleChange(e)); this.addEventListener('drop', this.handleChange.bind(this));
this.$('#file').addEventListener('change', (e) => this.handleChange(e)); this.$('#file').addEventListener('change', this.handleChange.bind(this));
this.$('#fileReader') this.fileReader = this.$('#fileReader');
.addEventListener('keydown', (e) => this.handleKeyEvent(e)); this.fileReader.addEventListener('keydown', this.handleKeyEvent.bind(this));
this.progressNode = this.$('#progress');
this.progressTextNode = this.$('#progressText');
} }
set error(message) { set error(message) {
...@@ -78,8 +82,6 @@ export class FileReader extends V8CustomElement { ...@@ -78,8 +82,6 @@ export class FileReader extends V8CustomElement {
handleChange(event) { handleChange(event) {
// Used for drop and file change. // Used for drop and file change.
event.preventDefault(); event.preventDefault();
this.dispatchEvent(
new CustomEvent('fileuploadstart', {bubbles: true, composed: true}));
const host = event.dataTransfer ? event.dataTransfer : event.target; const host = event.dataTransfer ? event.dataTransfer : event.target;
this.readFile(host.files[0]); this.readFile(host.files[0]);
} }
...@@ -92,26 +94,50 @@ export class FileReader extends V8CustomElement { ...@@ -92,26 +94,50 @@ export class FileReader extends V8CustomElement {
this.fileReader.focus(); this.fileReader.focus();
} }
get fileReader() {
return this.$('#fileReader');
}
get root() { get root() {
return this.$('#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) { readFile(file) {
this.dispatchEvent(new CustomEvent('fileuploadstart', {
bubbles: true,
composed: true,
detail: {
progressCallback: this.setProgress.bind(this),
totalSize: file.size,
}
}));
if (!file) { if (!file) {
this.error = 'Failed to load file.'; this.error = 'Failed to load file.';
return; return;
} }
this.fileReader.blur(); this.fileReader.blur();
this.setProgress(0);
this.root.className = 'loading'; this.root.className = 'loading';
// Delay the loading a bit to allow for CSS animations to happen. // Delay the loading a bit to allow for CSS animations to happen.
window.requestAnimationFrame(() => this.asyncReadFile(file)); window.requestAnimationFrame(() => this.asyncReadFile(file));
} }
async asyncReadFile(file) { async asyncReadFile(file) {
this.updateProgressBar();
const decoder = globalThis.TextDecoderStream; const decoder = globalThis.TextDecoderStream;
if (decoder) { if (decoder) {
await this._streamFile(file, decoder); await this._streamFile(file, decoder);
...@@ -137,7 +163,10 @@ export class FileReader extends V8CustomElement { ...@@ -137,7 +163,10 @@ export class FileReader extends V8CustomElement {
const readResult = await reader.read(); const readResult = await reader.read();
chunk = readResult.value; chunk = readResult.value;
readerDone = readResult.done; 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); } while (!readerDone);
} }
......
...@@ -44,7 +44,7 @@ export const parseVarArgs = 'parse-var-args'; ...@@ -44,7 +44,7 @@ export const parseVarArgs = 'parse-var-args';
* @constructor * @constructor
*/ */
export class LogReader { export class LogReader {
constructor (timedRange, pairwiseTimedRange) { constructor(timedRange=false, pairwiseTimedRange=false) {
this.dispatchTable_ = new Map(); this.dispatchTable_ = new Map();
this.timedRange_ = timedRange; this.timedRange_ = timedRange;
this.pairwiseTimedRange_ = pairwiseTimedRange; this.pairwiseTimedRange_ = pairwiseTimedRange;
......
...@@ -234,6 +234,35 @@ export class Script { ...@@ -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 { class SourceInfo {
script; script;
start; start;
...@@ -243,13 +272,16 @@ class SourceInfo { ...@@ -243,13 +272,16 @@ class SourceInfo {
fns; fns;
disassemble; disassemble;
setSourcePositionInfo(script, startPos, endPos, sourcePositionTable, inliningPositions, inlinedFunctions) { setSourcePositionInfo(
script, startPos, endPos, sourcePositionTableData, inliningPositions,
inlinedFunctions) {
this.script = script; this.script = script;
this.start = startPos; this.start = startPos;
this.end = endPos; this.end = endPos;
this.positions = sourcePositionTable; this.positions = sourcePositionTableData;
this.inlined = inliningPositions; this.inlined = inliningPositions;
this.fns = inlinedFunctions; this.fns = inlinedFunctions;
this.sourcePositionTable = new SourcePositionTable(sourcePositionTableData);
} }
setDisassemble(code) { setDisassemble(code) {
......
...@@ -2,21 +2,6 @@ ...@@ -2,21 +2,6 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // 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 { export class Group {
constructor(key, id, parentTotal, entries) { constructor(key, id, parentTotal, entries) {
this.key = key; this.key = key;
......
...@@ -179,9 +179,9 @@ button:hover { ...@@ -179,9 +179,9 @@ button:hover {
.colorbox { .colorbox {
display: inline-block; display: inline-block;
width: 10px; width: 8px;
height: 10px; height: 8px;
border: 1px var(--background-color) solid; border: 2px var(--background-color) solid;
border-radius: 50%; border-radius: 50%;
} }
......
...@@ -15,9 +15,8 @@ import {MapLogEntry} from './log/map.mjs'; ...@@ -15,9 +15,8 @@ import {MapLogEntry} from './log/map.mjs';
import {TickLogEntry} from './log/tick.mjs'; import {TickLogEntry} from './log/tick.mjs';
import {TimerLogEntry} from './log/timer.mjs'; import {TimerLogEntry} from './log/timer.mjs';
import {Processor} from './processor.mjs'; import {Processor} from './processor.mjs';
import {Timeline} from './timeline.mjs'
import {FocusEvent, SelectionEvent, SelectRelatedEvent, SelectTimeEvent, ToolTipEvent,} from './view/events.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 { class App {
_state; _state;
...@@ -51,11 +50,11 @@ class App { ...@@ -51,11 +50,11 @@ class App {
toolTip: $('#tool-tip'), toolTip: $('#tool-tip'),
}; };
this._view.logFileReader.addEventListener( this._view.logFileReader.addEventListener(
'fileuploadstart', (e) => this.handleFileUploadStart(e)); 'fileuploadstart', this.handleFileUploadStart.bind(this));
this._view.logFileReader.addEventListener( this._view.logFileReader.addEventListener(
'fileuploadchunk', (e) => this.handleFileUploadChunk(e)); 'fileuploadchunk', this.handleFileUploadChunk.bind(this));
this._view.logFileReader.addEventListener( this._view.logFileReader.addEventListener(
'fileuploadend', (e) => this.handleFileUploadEnd(e)); 'fileuploadend', this.handleFileUploadEnd.bind(this));
this._startupPromise = this._loadCustomElements(); this._startupPromise = this._loadCustomElements();
this._view.codeTrack.svg = true; this._view.codeTrack.svg = true;
} }
...@@ -91,14 +90,14 @@ class App { ...@@ -91,14 +90,14 @@ class App {
document.addEventListener( document.addEventListener(
'keydown', e => this._navigation?.handleKeyDown(e)); 'keydown', e => this._navigation?.handleKeyDown(e));
document.addEventListener( document.addEventListener(
SelectRelatedEvent.name, e => this.handleSelectRelatedEntries(e)); SelectRelatedEvent.name, this.handleSelectRelatedEntries.bind(this));
document.addEventListener( document.addEventListener(
SelectionEvent.name, e => this.handleSelectEntries(e)) SelectionEvent.name, this.handleSelectEntries.bind(this))
document.addEventListener( document.addEventListener(
FocusEvent.name, e => this.handleFocusLogEntry(e)); FocusEvent.name, this.handleFocusLogEntry.bind(this));
document.addEventListener( document.addEventListener(
SelectTimeEvent.name, e => this.handleTimeRangeSelect(e)); SelectTimeEvent.name, this.handleTimeRangeSelect.bind(this));
document.addEventListener(ToolTipEvent.name, e => this.handleToolTip(e)); document.addEventListener(ToolTipEvent.name, this.handleToolTip.bind(this));
} }
handleSelectRelatedEntries(e) { handleSelectRelatedEntries(e) {
...@@ -362,6 +361,8 @@ class App { ...@@ -362,6 +361,8 @@ class App {
this.restartApp(); this.restartApp();
$('#container').className = 'initial'; $('#container').className = 'initial';
this._processor = new Processor(); this._processor = new Processor();
this._processor.setProgressCallback(
e.detail.totalSize, e.detail.progressCallback);
} }
async handleFileUploadChunk(e) { async handleFileUploadChunk(e) {
......
...@@ -30,7 +30,7 @@ export class TimerLogEntry extends LogEntry { ...@@ -30,7 +30,7 @@ export class TimerLogEntry extends LogEntry {
} }
get duration() { get duration() {
return this._endTime - this._time; return Math.max(0, this._endTime - this._time);
} }
covers(time) { covers(time) {
...@@ -53,4 +53,4 @@ export class TimerLogEntry extends LogEntry { ...@@ -53,4 +53,4 @@ export class TimerLogEntry extends LogEntry {
'duration', 'duration',
]; ];
} }
} }
\ No newline at end of file
...@@ -61,6 +61,11 @@ export class Processor extends LogReader { ...@@ -61,6 +61,11 @@ export class Processor extends LogReader {
_lastCodeLogEntry; _lastCodeLogEntry;
_lastTickLogEntry; _lastTickLogEntry;
_chunkRemainder = ''; _chunkRemainder = '';
_totalInputBytes = 0;
_processedInputChars = 0;
_progressCallback;
MAJOR_VERSION = 7; MAJOR_VERSION = 7;
MINOR_VERSION = 6; MINOR_VERSION = 6;
constructor() { constructor() {
...@@ -205,7 +210,22 @@ export class Processor extends LogReader { ...@@ -205,7 +210,22 @@ export class Processor extends LogReader {
this._chunkConsumer.push(chunk) 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) { async _processChunk(chunk) {
const prevProcessedInputChars = this._processedInputChars;
let end = chunk.length; let end = chunk.length;
let current = 0; let current = 0;
let next = 0; let next = 0;
...@@ -226,7 +246,9 @@ export class Processor extends LogReader { ...@@ -226,7 +246,9 @@ export class Processor extends LogReader {
current = next + 1; current = next + 1;
lineNumber++; lineNumber++;
await this.processLogLine(line); await this.processLogLine(line);
this._processedInputChars = prevProcessedInputChars + current;
} }
this._updateProgress();
} catch (e) { } catch (e) {
console.error( console.error(
`Could not parse log line ${lineNumber}, trying to continue: ${e}`); `Could not parse log line ${lineNumber}, trying to continue: ${e}`);
......
// Copyright 2020 the V8 project authors. All rights reserved. // Copyright 2020 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import {arrayEquals, defer, groupBy} from '../helper.mjs';
import {App} from '../index.mjs' import {App} from '../index.mjs'
import {SelectionEvent, SelectRelatedEvent, ToolTipEvent} from './events.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. // A source mapping proxy for source maps that don't have CORS headers.
// TODO(leszeks): Make this configurable. // TODO(leszeks): Make this configurable.
......
...@@ -2,10 +2,9 @@ ...@@ -2,10 +2,9 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import {delay} from '../../helper.mjs';
import {kChunkHeight, kChunkVisualWidth, kChunkWidth} from '../../log/map.mjs'; import {kChunkHeight, kChunkVisualWidth, kChunkWidth} from '../../log/map.mjs';
import {SelectionEvent, SelectTimeEvent, SynchronizeSelectionEvent, ToolTipEvent,} from '../events.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; export const kTimelineHeight = 200;
...@@ -41,9 +40,9 @@ export class TimelineTrackBase extends V8CustomElement { ...@@ -41,9 +40,9 @@ export class TimelineTrackBase extends V8CustomElement {
} }
_initEventListeners() { _initEventListeners() {
this._legend.onFilter = (type) => this._handleFilterTimeline(); this._legend.onFilter = this._handleFilterTimeline.bind(this);
this.timelineNode.addEventListener( this.timelineNode.addEventListener(
'scroll', e => this._handleTimelineScroll(e)); 'scroll', this._handleTimelineScroll.bind(this));
this.hitPanelNode.onclick = this._handleClick.bind(this); this.hitPanelNode.onclick = this._handleClick.bind(this);
this.hitPanelNode.ondblclick = this._handleDoubleClick.bind(this); this.hitPanelNode.ondblclick = this._handleDoubleClick.bind(this);
this.hitPanelNode.onmousemove = this._handleMouseMove.bind(this); this.hitPanelNode.onmousemove = this._handleMouseMove.bind(this);
...@@ -64,6 +63,7 @@ export class TimelineTrackBase extends V8CustomElement { ...@@ -64,6 +63,7 @@ export class TimelineTrackBase extends V8CustomElement {
_handleFilterTimeline(type) { _handleFilterTimeline(type) {
this._updateChunks(); this._updateChunks();
this._legend.update(true);
} }
set data(timeline) { set data(timeline) {
...@@ -597,24 +597,39 @@ class Legend { ...@@ -597,24 +597,39 @@ class Legend {
return this._typesFilters.get(logEntry.type); return this._typesFilters.get(logEntry.type);
} }
update() { update(force = false) {
if (this._lastSelection === this.selection) return; if (!force && this._lastSelection === this.selection) return;
this._lastSelection = this.selection; this._lastSelection = this.selection;
const tbody = DOM.tbody(); const tbody = DOM.tbody();
const missingTypes = new Set(this._typesFilters.keys()); const missingTypes = new Set(this._typesFilters.keys());
this._checkDurationField(); this._checkDurationField();
this.selection.getBreakdown(undefined, this._enableDuration) let selectionDuration = 0;
.forEach(group => { const breakdown =
tbody.appendChild(this._addTypeRow(group)); this.selection.getBreakdown(undefined, this._enableDuration);
missingTypes.delete(group.key); if (this._enableDuration) {
}); if (this.selection.cachedDuration === undefined) {
missingTypes.forEach( this.selection.cachedDuration = this._breakdownTotalDuration(breakdown);
key => tbody.appendChild(this._addRow('', key, 0, '0%'))); }
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) { if (this._timeline.selection) {
tbody.appendChild( tbody.appendChild(this._addRow(
this._addRow('', 'Selection', this.selection.length, '100%')); '', '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); this._table.tBodies[0].replaceWith(tbody);
} }
...@@ -628,8 +643,9 @@ class Legend { ...@@ -628,8 +643,9 @@ class Legend {
_addRow(colorNode, type, count, countPercent, duration, durationPercent) { _addRow(colorNode, type, count, countPercent, duration, durationPercent) {
const row = DOM.tr(); const row = DOM.tr();
row.appendChild(DOM.td(colorNode)); const colorCell = row.appendChild(DOM.td(colorNode, 'color'));
const typeCell = row.appendChild(DOM.td(type)); colorCell.setAttribute('title', `Toggle '${type}' entries.`);
const typeCell = row.appendChild(DOM.td(type, 'text'));
typeCell.setAttribute('title', type); typeCell.setAttribute('title', type);
row.appendChild(DOM.td(count.toString())); row.appendChild(DOM.td(count.toString()));
row.appendChild(DOM.td(countPercent)); row.appendChild(DOM.td(countPercent));
...@@ -640,26 +656,31 @@ class Legend { ...@@ -640,26 +656,31 @@ class Legend {
return row return row
} }
_addTypeRow(group) { _addTypeRow(group, selectionDuration) {
const color = this.colorForType(group.key); 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)) { if (this._typesFilters.get(group.key)) {
colorDiv.style.backgroundColor = color; colorDiv.style.backgroundColor = color;
} else { } else {
colorDiv.style.borderColor = color;
colorDiv.style.backgroundColor = CSSColor.backgroundImage; colorDiv.style.backgroundColor = CSSColor.backgroundImage;
} }
let duration = 0; let duration = 0;
let durationPercent = '';
if (this._enableDuration) { if (this._enableDuration) {
const entries = group.entries; // group.duration was added in _breakdownTotalDuration.
for (let i = 0; i < entries.length; i++) { duration = group.duration;
duration += entries[i].duration; durationPercent = selectionDuration == 0 ?
} '0%' :
this._formatPercent(duration / selectionDuration);
} }
let countPercent = const countPercent =
`${(group.length / this.selection.length * 100).toFixed(1)}%`; this._formatPercent(group.length / this.selection.length);
const row = this._addRow( const row = this._addRow(
colorDiv, group.key, group.length, countPercent, duration, ''); colorDiv, group.key, group.length, countPercent, duration,
durationPercent);
row.className = 'clickable'; row.className = 'clickable';
row.onclick = this._typeClickHandler; row.onclick = this._typeClickHandler;
row.data = group.key; row.data = group.key;
...@@ -671,4 +692,26 @@ class Legend { ...@@ -671,4 +692,26 @@ class Legend {
this._typesFilters.set(type, !this._typesFilters.get(type)); this._typesFilters.set(type, !this._typesFilters.get(type));
this.onFilter(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 @@ ...@@ -2,10 +2,9 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import {delay} from '../../helper.mjs';
import {Timeline} from '../../timeline.mjs'; import {Timeline} from '../../timeline.mjs';
import {SelectTimeEvent} from '../events.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' import {TimelineTrackBase} from './timeline-track-base.mjs'
......
...@@ -69,14 +69,21 @@ found in the LICENSE file. --> ...@@ -69,14 +69,21 @@ found in the LICENSE file. -->
height: calc(var(--view-height) + 12px); height: calc(var(--view-height) + 12px);
overflow-y: scroll; overflow-y: scroll;
margin-right: -10px; margin-right: -10px;
padding-right: 2px; padding: 0 2px 0 2px;
width: 400px;
border-left: 1px solid var(--border-color);
} }
#legendTable { #legendTable {
width: 280px; width: 100%;
border-collapse: collapse; border-collapse: collapse;
} }
thead {
border-top: 1px solid var(--border-color);
border-bottom: 1px solid var(--border-color);
}
th, th,
td { td {
padding: 1px 3px 2px 3px; padding: 1px 3px 2px 3px;
...@@ -84,24 +91,20 @@ found in the LICENSE file. --> ...@@ -84,24 +91,20 @@ found in the LICENSE file. -->
#legendTable td { #legendTable td {
padding-top: 3px; padding-top: 3px;
text-align: right;
} }
/* Center colors */ /* Center colors */
#legendTable td:nth-of-type(4n+1) { #legendTable .color {
text-align: center; text-align: center;
} }
/* Left align text*/ /* Left align text*/
#legendTable td:nth-of-type(4n+2) { #legendTable .text {
text-align: left; text-align: left;
width: 100%; width: 100%;
max-width: 200px; max-width: 200px;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
/* right align numbers */
#legendTable td:nth-of-type(4n+3),
#legendTable td:nth-of-type(4n+4) {
text-align: right;
}
.timeline { .timeline {
background-color: var(--timeline-background-color); background-color: var(--timeline-background-color);
...@@ -163,6 +166,9 @@ found in the LICENSE file. --> ...@@ -163,6 +166,9 @@ found in the LICENSE file. -->
.legend { .legend {
flex: initial; flex: initial;
} }
.colorbox.empty {
opacity: 0.5;
}
</style> </style>
<style> <style>
/* SVG styles */ /* SVG styles */
...@@ -235,7 +241,7 @@ found in the LICENSE file. --> ...@@ -235,7 +241,7 @@ found in the LICENSE file. -->
<thead> <thead>
<tr> <tr>
<td></td> <td></td>
<td>Type</td> <td class="text">Type</td>
<td>Count</td> <td>Count</td>
<td></td> <td></td>
</tr> </tr>
......
...@@ -2,10 +2,10 @@ ...@@ -2,10 +2,10 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import {delay} from '../../helper.mjs';
import {TickLogEntry} from '../../log/tick.mjs'; import {TickLogEntry} from '../../log/tick.mjs';
import {Timeline} from '../../timeline.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' import {TimelineTrackStackedBase} from './timeline-track-stacked-base.mjs'
class Flame { 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