Commit 94f05366 authored by Camillo Bruni's avatar Camillo Bruni Committed by Commit Bot

[tools] System-analyzer timeline improvements

- Timeline.selection is now a Timeline as well
- Allow remove the current timeline-track selection by double-clicking
  outside-the selection
- Update the timeline-track stats based on the current selection
- Simplify DOM element creation methods
- Add separate SelectionHandler class for timeline-track

Bug: v8:10644
Change-Id: I4f15d6ab4f5ec6b7330e22769472ca3074b00edd
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/2565130
Commit-Queue: Camillo Bruni <cbruni@chromium.org>
Reviewed-by: 's avatarSathya Gunasekaran  <gsathya@chromium.org>
Cr-Commit-Position: refs/heads/master@{#71497}
parent 21cec63d
...@@ -44,7 +44,9 @@ import { LogEntry} from "../../../tools/system-analyzer/log/log.mjs"; ...@@ -44,7 +44,9 @@ import { LogEntry} from "../../../tools/system-analyzer/log/log.mjs";
let startTime = time; let startTime = time;
let endTime = time + 2; let endTime = time + 2;
timeline.selectTimeRange(startTime, endTime); timeline.selectTimeRange(startTime, endTime);
assertArrayEquals(timeline.selection, [entry1, entry2]); assertArrayEquals(timeline.selection.startTime, startTime);
assertArrayEquals(timeline.selection.endTime, endTime);
assertArrayEquals(timeline.selection.values, [entry1, entry2]);
let entryIdx = timeline.find(time + 1); let entryIdx = timeline.find(time + 1);
let entry = timeline.at(entryIdx); let entry = timeline.at(entryIdx);
assertEquals(entry.time, time + 1); assertEquals(entry.time, time + 1);
......
...@@ -34,8 +34,7 @@ export class Group { ...@@ -34,8 +34,7 @@ export class Group {
static groupBy(entries, property) { static groupBy(entries, property) {
let accumulator = Object.create(null); let accumulator = Object.create(null);
let length = entries.length; let length = entries.length;
for (let i = 0; i < length; i++) { for (let entry of entries) {
let entry = entries[i];
let key = entry[property]; let key = entry[property];
if (accumulator[key] == undefined) { if (accumulator[key] == undefined) {
accumulator[key] = new Group(property, key, entry); accumulator[key] = new Group(property, key, entry);
......
...@@ -144,6 +144,7 @@ button:hover { ...@@ -144,6 +144,7 @@ button:hover {
} }
.colorbox { .colorbox {
display: inline-block;
width: 10px; width: 10px;
height: 10px; height: 10px;
border: 1px var(--background-color) solid; border: 1px var(--background-color) solid;
......
...@@ -64,11 +64,12 @@ class App { ...@@ -64,11 +64,12 @@ class App {
} }
handleShowEntries(e) { handleShowEntries(e) {
if (e.entries[0] instanceof MapLogEntry) { const entry = e.entries[0];
if (entry instanceof MapLogEntry) {
this.showMapEntries(e.entries); this.showMapEntries(e.entries);
} else if (e.entries[0] instanceof IcLogEntry) { } else if (entry instanceof IcLogEntry) {
this.showIcEntries(e.entries); this.showIcEntries(e.entries);
} else if (e.entries[0] instanceof SourcePosition) { } else if (entry instanceof SourcePosition) {
this.showSourcePositionEntries(e.entries); this.showSourcePositionEntries(e.entries);
} else { } else {
throw new Error('Unknown selection type!'); throw new Error('Unknown selection type!');
...@@ -117,11 +118,12 @@ class App { ...@@ -117,11 +118,12 @@ class App {
} }
handleShowEntryDetail(e) { handleShowEntryDetail(e) {
if (e.entry instanceof MapLogEntry) { const entry = e.entry;
if (entry instanceof MapLogEntry) {
this.selectMapLogEntry(e.entry); this.selectMapLogEntry(e.entry);
} else if (e.entry instanceof IcLogEntry) { } else if (entry instanceof IcLogEntry) {
this.selectICLogEntry(e.entry); this.selectICLogEntry(e.entry);
} else if (e.entry instanceof SourcePosition) { } else if (entry instanceof SourcePosition) {
this.selectSourcePosition(e.entry); this.selectSourcePosition(e.entry);
} else { } else {
throw new Error('Unknown selection type!'); throw new Error('Unknown selection type!');
...@@ -137,7 +139,7 @@ class App { ...@@ -137,7 +139,7 @@ class App {
selectICLogEntry(entry) { selectICLogEntry(entry) {
this._state.ic = entry; this._state.ic = entry;
this._view.icPanel.selectedLogEntries = [entry]; this._view.icPanel.selectedEntry = [entry];
} }
selectSourcePosition(sourcePositions) { selectSourcePosition(sourcePositions) {
......
...@@ -9,13 +9,13 @@ class Timeline { ...@@ -9,13 +9,13 @@ class Timeline {
_values; _values;
// Current selection, subset of #values: // Current selection, subset of #values:
_selection; _selection;
_uniqueTypes; _breakdown;
constructor(model) { constructor(model, values = [], startTime = 0, endTime = 0) {
this._model = model; this._model = model;
this._values = []; this._values = values;
this.startTime = 0; this.startTime = startTime;
this.endTime = 0; this.endTime = endTime;
} }
get model() { get model() {
...@@ -35,16 +35,19 @@ class Timeline { ...@@ -35,16 +35,19 @@ class Timeline {
} }
selectTimeRange(startTime, endTime) { selectTimeRange(startTime, endTime) {
this._selection = this.range(startTime, endTime); const items = this.range(startTime, endTime);
this._selection = new Timeline(this._model, items, startTime, endTime);
}
clearSelection() {
this._selection = undefined;
} }
getChunks(windowSizeMs) { getChunks(windowSizeMs) {
// TODO(zcankara) Fill this one
return this.chunkSizes(windowSizeMs); return this.chunkSizes(windowSizeMs);
} }
get values() { get values() {
// TODO(zcankara) Not to break something delete later
return this._values; return this._values;
} }
...@@ -94,6 +97,10 @@ class Timeline { ...@@ -94,6 +97,10 @@ class Timeline {
return this._values.length; return this._values.length;
} }
slice(startIndex, endIndex) {
return this._values.slice(startIndex, endIndex);
}
first() { first() {
return this._values[0]; return this._values[0];
} }
...@@ -102,11 +109,17 @@ class Timeline { ...@@ -102,11 +109,17 @@ class Timeline {
return this._values[this._values.length - 1]; return this._values[this._values.length - 1];
} }
* [Symbol.iterator]() {
yield* this._values;
}
duration() { duration() {
if (this.isEmpty()) return 0;
return this.last().time - this.first().time; return this.last().time - this.first().time;
} }
forEachChunkSize(count, fn) { forEachChunkSize(count, fn) {
if (this.isEmpty()) return;
const increment = this.duration() / count; const increment = this.duration() / count;
let currentTime = this.first().time + increment; let currentTime = this.first().time + increment;
let index = 0; let index = 0;
...@@ -162,24 +175,12 @@ class Timeline { ...@@ -162,24 +175,12 @@ class Timeline {
return minIndex; return minIndex;
} }
_initializeTypes() { getBreakdown(keyFunction) {
const types = new Map(); if (keyFunction) return breakdown(this._values, keyFunction);
let index = 0; if (this._breakdown === undefined) {
for (const entry of this.all) { this._breakdown = breakdown(this._values, each => each.type);
let entries = types.get(entry.type);
if (entries != undefined) {
entries.push(entry)
} else {
entries = [entry];
entries.index = index++;
types.set(entry.type, entries)
}
} }
return this._uniqueTypes = types; return this._breakdown;
}
get uniqueTypes() {
return this._uniqueTypes ?? this._initializeTypes();
} }
depthHistogram() { depthHistogram() {
...@@ -259,26 +260,7 @@ class Chunk { ...@@ -259,26 +260,7 @@ class Chunk {
} }
getBreakdown(keyFunction) { getBreakdown(keyFunction) {
if (this.items.length === 0) return []; return breakdown(this.items, keyFunction);
if (keyFunction === void 0) {
keyFunction = each => each;
}
const typeToindex = new Map();
const breakdown = [];
// This is performance critical, resorting to for-loop
for (let i = 0; i < this.items.length; i++) {
const each = this.items[i];
const type = keyFunction(each);
const index = typeToindex.get(type);
if (index === void 0) {
typeToindex.set(type, breakdown.length);
breakdown.push([type, 0]);
} else {
breakdown[index][1]++;
}
}
// Sort by count
return breakdown.sort((a, b) => a[1] - b[1]);
} }
filter() { filter() {
...@@ -286,4 +268,26 @@ class Chunk { ...@@ -286,4 +268,26 @@ class Chunk {
} }
} }
function breakdown(array, keyFunction) {
if (array.length === 0) return [];
if (keyFunction === undefined) keyFunction = each => each;
const typeToindex = new Map();
const breakdown = [];
let id = 0;
// This is performance critical, resorting to for-loop
for (let i = 0; i < array.length; i++) {
const each = array[i];
const type = keyFunction(each);
const index = typeToindex.get(type);
if (index === void 0) {
typeToindex.set(type, breakdown.length);
breakdown.push({type: type, count: 0, id: id++});
} else {
breakdown[index].count++;
}
}
// Sort by count
return breakdown.sort((a, b) => b.count - a.count);
}
export {Timeline, Chunk}; export {Timeline, Chunk};
...@@ -30,7 +30,7 @@ export class SelectTimeEvent extends CustomEvent { ...@@ -30,7 +30,7 @@ export class SelectTimeEvent extends CustomEvent {
static get name() { static get name() {
return 'timerangeselect'; return 'timerangeselect';
} }
constructor(start, end) { constructor(start = -1, end = Infinity) {
super(SelectTimeEvent.name, {bubbles: true, composed: true}); super(SelectTimeEvent.name, {bubbles: true, composed: true});
this.start = start; this.start = start;
this.end = end; this.end = end;
......
...@@ -13,7 +13,7 @@ class CSSColor { ...@@ -13,7 +13,7 @@ class CSSColor {
if (color === undefined) { if (color === undefined) {
throw new Error(`CSS color does not exist: ${name}`); throw new Error(`CSS color does not exist: ${name}`);
} }
this._cache.set(name, color); this._cache.set(name, color.trim());
return color; return color;
} }
static reset() { static reset() {
...@@ -74,51 +74,61 @@ class CSSColor { ...@@ -74,51 +74,61 @@ class CSSColor {
static get violet() { static get violet() {
return this.get('violet'); return this.get('violet');
} }
static at(index) {
return this.list[index % this.list.length];
}
static get list() {
if (!this._colors) {
this._colors = [
this.green,
this.violet,
this.orange,
this.yellow,
this.primaryColor,
this.red,
this.blue,
this.yellow,
this.secondaryColor,
];
}
return this._colors;
}
} }
const kColors = [
CSSColor.green,
CSSColor.violet,
CSSColor.orange,
CSSColor.yellow,
CSSColor.primaryColor,
CSSColor.red,
CSSColor.blue,
CSSColor.yellow,
CSSColor.secondaryColor,
];
class DOM { class DOM {
static div(classes) { static element(type, classes) {
const node = document.createElement('div'); const node = document.createElement(type);
if (classes !== void 0) { if (classes === undefined) return node;
if (typeof classes === 'string') { if (typeof classes === 'string') {
node.className = classes; node.className = classes;
} else { } else {
classes.forEach(cls => node.classList.add(cls)); classes.forEach(cls => node.classList.add(cls));
}
} }
return node; return node;
} }
static text(string) {
return document.createTextNode(string);
}
static div(classes) {
return this.element('div', classes);
}
static span(classes) { static span(classes) {
const node = document.createElement('span'); return this.element('span', classes);
if (classes !== undefined) {
if (typeof classes === 'string') {
node.className = classes;
} else {
classes.forEach(cls => node.classList.add(cls));
}
}
return node;
} }
static table(className) {
const node = document.createElement('table'); static table(classes) {
if (className) node.className = className; return this.element('table', classes);
return node; }
static tbody(classes) {
return this.element('tbody', classes);
} }
static td(textOrNode, className) { static td(textOrNode, className) {
const node = document.createElement('td'); const node = this.element('td');
if (typeof textOrNode === 'object') { if (typeof textOrNode === 'object') {
node.appendChild(textOrNode); node.appendChild(textOrNode);
} else if (textOrNode) { } else if (textOrNode) {
...@@ -128,14 +138,8 @@ class DOM { ...@@ -128,14 +138,8 @@ class DOM {
return node; return node;
} }
static tr(className) { static tr(classes) {
const node = document.createElement('tr'); return this.element('tr', classes);
if (className) node.className = className;
return node;
}
static text(string) {
return document.createTextNode(string);
} }
static removeAllChildren(node) { static removeAllChildren(node) {
...@@ -225,7 +229,6 @@ export * from '../helper.mjs'; ...@@ -225,7 +229,6 @@ export * from '../helper.mjs';
export { export {
DOM, DOM,
$, $,
kColors,
V8CustomElement, V8CustomElement,
CSSColor, CSSColor,
LazyTable, LazyTable,
......
...@@ -12,6 +12,7 @@ import {DOM, V8CustomElement} from './helper.mjs'; ...@@ -12,6 +12,7 @@ import {DOM, V8CustomElement} from './helper.mjs';
DOM.defineCustomElement( DOM.defineCustomElement(
'view/ic-panel', (templateText) => class ICPanel extends V8CustomElement { 'view/ic-panel', (templateText) => class ICPanel extends V8CustomElement {
_selectedLogEntries; _selectedLogEntries;
_selectedLogEntry;
_timeline; _timeline;
_detailsClickHandler = this.handleDetailsClick.bind(this); _detailsClickHandler = this.handleDetailsClick.bind(this);
...@@ -54,6 +55,10 @@ DOM.defineCustomElement( ...@@ -54,6 +55,10 @@ DOM.defineCustomElement(
this.update(); this.update();
} }
set selectedLogEntry(entry) {
// TODO: show details
}
_update() { _update() {
this._updateCount(); this._updateCount();
this._updateTable(); this._updateTable();
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
// 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 {FocusEvent, ToolTipEvent} from '../events.mjs'; import {FocusEvent, ToolTipEvent} from '../events.mjs';
import {kColors} from '../helper.mjs'; import {CSSColor} from '../helper.mjs';
import {DOM, V8CustomElement} from '../helper.mjs'; import {DOM, V8CustomElement} from '../helper.mjs';
DOM.defineCustomElement( DOM.defineCustomElement(
...@@ -10,6 +10,7 @@ DOM.defineCustomElement( ...@@ -10,6 +10,7 @@ DOM.defineCustomElement(
(templateText) => class MapTransitions extends V8CustomElement { (templateText) => class MapTransitions extends V8CustomElement {
_timeline; _timeline;
_map; _map;
_edgeToColor = new Map();
_selectedMapLogEntries; _selectedMapLogEntries;
_displayedMapsInTree; _displayedMapsInTree;
_toggleSubtreeHandler = this._handleToggleSubtree.bind(this); _toggleSubtreeHandler = this._handleToggleSubtree.bind(this);
...@@ -42,6 +43,10 @@ DOM.defineCustomElement( ...@@ -42,6 +43,10 @@ DOM.defineCustomElement(
set timeline(timeline) { set timeline(timeline) {
this._timeline = timeline; this._timeline = timeline;
this._edgeToColor.clear();
timeline.getBreakdown().forEach(breakdown => {
this._edgeToColor.set(breakdown.type, CSSColor.at(breakdown.id));
});
} }
set selectedMapLogEntries(list) { set selectedMapLogEntries(list) {
...@@ -53,11 +58,6 @@ DOM.defineCustomElement( ...@@ -53,11 +58,6 @@ DOM.defineCustomElement(
return this._selectedMapLogEntries; return this._selectedMapLogEntries;
} }
_edgeToColor(edge) {
const index = this._timeline.uniqueTypes.get(edge.type).index
return kColors[index % kColors.length];
}
_handleTransitionViewChange(e) { _handleTransitionViewChange(e) {
this.tooltip.style.left = e.pageX + 'px'; this.tooltip.style.left = e.pageX + 'px';
this.tooltip.style.top = e.pageY + 'px'; this.tooltip.style.top = e.pageY + 'px';
...@@ -124,7 +124,7 @@ DOM.defineCustomElement( ...@@ -124,7 +124,7 @@ DOM.defineCustomElement(
_addTransitionEdge(map) { _addTransitionEdge(map) {
let classes = ['transitionEdge']; let classes = ['transitionEdge'];
let edge = DOM.div(classes); let edge = DOM.div(classes);
edge.style.backgroundColor = this._edgeToColor(map.edge); edge.style.backgroundColor = this._edgeToColor.get(map.edge.type);
let labelNode = DOM.div('transitionLabel'); let labelNode = DOM.div('transitionLabel');
labelNode.innerText = map.edge.toString(); labelNode.innerText = map.edge.toString();
edge.appendChild(labelNode); edge.appendChild(labelNode);
...@@ -153,7 +153,8 @@ DOM.defineCustomElement( ...@@ -153,7 +153,8 @@ DOM.defineCustomElement(
_addMapNode(map) { _addMapNode(map) {
let node = DOM.div('map'); let node = DOM.div('map');
if (map.edge) node.style.backgroundColor = this._edgeToColor(map.edge); if (map.edge)
node.style.backgroundColor = this._edgeToColor.get(map.edge.type);
node.map = map; node.map = map;
node.onclick = this._selectMapHandler node.onclick = this._selectMapHandler
node.onmouseover = this._mouseoverMapHandler node.onmouseover = this._mouseoverMapHandler
......
...@@ -85,6 +85,10 @@ found in the LICENSE file. --> ...@@ -85,6 +85,10 @@ found in the LICENSE file. -->
#legend td:nth-of-type(4n+4) { #legend td:nth-of-type(4n+4) {
text-align: right; text-align: right;
} }
/* Center colors */
#legend td:nth-of-type(4n+1) {
text-align: center;;
}
.legendTypeColumn { .legendTypeColumn {
width: 100%; width: 100%;
...@@ -94,8 +98,8 @@ found in the LICENSE file. --> ...@@ -94,8 +98,8 @@ found in the LICENSE file. -->
background-color: var(--timeline-background-color); background-color: var(--timeline-background-color);
} }
#timeline .rightHandle, #rightHandle,
#timeline .leftHandle { #leftHandle {
background-color: rgba(200, 200, 200, 0.5); background-color: rgba(200, 200, 200, 0.5);
height: 100%; height: 100%;
width: 5px; width: 5px;
...@@ -103,14 +107,14 @@ found in the LICENSE file. --> ...@@ -103,14 +107,14 @@ found in the LICENSE file. -->
z-index: 3; z-index: 3;
cursor: col-resize; cursor: col-resize;
} }
#timeline .leftHandle { #leftHandle {
border-left: 1px solid var(--on-surface-color); border-left: 1px solid var(--on-surface-color);
} }
#timeline .rightHandle { #rightHandle {
border-right: 1px solid var(--on-surface-color); border-right: 1px solid var(--on-surface-color);
} }
#timeline .selection { #selectionBackground {
background-color: rgba(133, 68, 163, 0.5); background-color: rgba(133, 68, 163, 0.5);
height: 100%; height: 100%;
position: absolute; position: absolute;
...@@ -125,13 +129,14 @@ found in the LICENSE file. --> ...@@ -125,13 +129,14 @@ found in the LICENSE file. -->
<td>Percent</td> <td>Percent</td>
</tr> </tr>
</thead> </thead>
<tbody id="legendContent"> <tbody></tbody>
</tbody>
</table> </table>
<div id="timeline"> <div id="timeline">
<div class="leftHandle"></div> <div id="selection">
<div class="selection"></div> <div id="leftHandle"></div>
<div class="rightHandle"></div> <div id="selectionBackground"></div>
<div id="rightHandle"></div>
</div>
<div id="timelineLabel">Frequency</div> <div id="timelineLabel">Frequency</div>
<div id="timelineChunks"></div> <div id="timelineChunks"></div>
<canvas id="timelineCanvas"></canvas> <canvas id="timelineCanvas"></canvas>
......
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