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