Commit 899f4ccd authored by Camillo Bruni's avatar Camillo Bruni Committed by V8 LUCI CQ

[tools][system-analyzer] Various fixes

- Add tests and fix Chunk calculations in Timeline class
- Cache DOM nodes directly as properties in TimelineTrackBase
- Keep track of last focused entry in timeline tracks and reuse it
  to position the tooltip when the view is locked

Bug: v8:10644
Change-Id: I356dcf7eed220df89f6a7ff926f00f78b119160e
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/2968943
Commit-Queue: Camillo Bruni <cbruni@chromium.org>
Reviewed-by: 's avatarPatrick Thier <pthier@chromium.org>
Cr-Commit-Position: refs/heads/master@{#75224}
parent 049760ec
......@@ -6,40 +6,88 @@ import { Timeline } from "../../../tools/system-analyzer/timeline.mjs";
import { LogEntry} from "../../../tools/system-analyzer/log/log.mjs";
class TestLogEntry extends LogEntry {
toString() {
return `TestLogEntry(${this.id}, ${this.time})`;
}
}
(function testTimeline() {
let timeline = new Timeline();
assertTrue(timeline.isEmpty());
let id1 = "0x3e7e082470cd";
let id2 = "0x3e7e082470ad";
let time = 10;
let entry1 = new LogEntry(id1, time + 0);
let entry2 = new LogEntry(id1, time + 1);
let entry3 = new LogEntry(id1, time + 2);
let entry4 = new LogEntry(id1, time + 3);
let entry5 = new LogEntry(id2, time + 3);
let entry6 = new LogEntry(id1, time + 4);
let entry1 = new TestLogEntry(id1, time + 0);
let entry2 = new TestLogEntry(id1, time + 1);
let entry3 = new TestLogEntry(id1, time + 2);
let entry4 = new TestLogEntry(id1, time + 3);
let entry5 = new TestLogEntry(id2, time + 3);
let entry6 = new TestLogEntry(id1, time + 4);
timeline.push(entry1);
assertFalse(timeline.isEmpty());
assertEquals(timeline.first(), entry1);
assertEquals(timeline.at(0), entry1);
assertEquals(timeline.last(), entry1);
timeline.push(entry2);
assertEquals(timeline.first(), entry1);
assertEquals(timeline.at(1), entry2);
assertEquals(timeline.last(), entry2);
timeline.push(entry3);
timeline.push(entry4);
timeline.push(entry5);
timeline.push(entry6);
assertEquals(timeline.find(time + 0), 0);
assertEquals(timeline.find(time + 1), 1);
assertEquals(timeline.find(time + 2), 2);
assertEquals(timeline.find(time + 3), 3);
assertEquals(timeline.find(time + 4), 5);
assertEquals(timeline.find(time + 5), 5);
assertEquals(timeline.first(), entry1);
assertEquals(timeline.at(0), entry1);
assertEquals(timeline.at(1), entry2);
assertEquals(timeline.at(2), entry3);
assertEquals(timeline.at(3), entry4);
assertEquals(timeline.at(4), entry5);
assertEquals(timeline.at(5), entry6);
assertEquals(timeline.last(), entry6);
assertEquals(timeline.length, 6);
assertEquals(timeline.all.length, 6);
assertEquals(timeline.startTime, entry1.time);
assertEquals(timeline.endTime, entry6.time);
assertEquals(timeline.duration(), 4);
assertEquals(timeline.find(time + 2.00), 2);
assertEquals(timeline.find(time + 2.01), 3);
assertEquals(timeline.find(time + 2.90), 3);
assertEquals(timeline.find(time + 3.01), 5);
assertEquals(timeline.find(time + 3.90), 5);
assertEquals(timeline.find(time + 4.00), 5);
assertEquals(timeline.findFirst(time - 1), 0);
assertEquals(timeline.findFirst(time + 0), 0);
assertEquals(timeline.findFirst(time + 1), 1);
assertEquals(timeline.findFirst(time + 2), 2);
assertEquals(timeline.findFirst(time + 3), 3);
assertEquals(timeline.findFirst(time + 4), 5);
assertEquals(timeline.findFirst(time + 5), 5);
assertEquals(timeline.findFirst(time + 2.00), 2);
assertEquals(timeline.findFirst(time + 2.01), 3);
assertEquals(timeline.findFirst(time + 2.90), 3);
assertEquals(timeline.findFirst(time + 3.01), 5);
assertEquals(timeline.findFirst(time + 3.90), 5);
assertEquals(timeline.findFirst(time + 4.00), 5);
assertEquals(timeline.findLast(time - 1), -1);
assertEquals(timeline.findLast(time + 0), 0);
assertEquals(timeline.findLast(time + 1), 1);
assertEquals(timeline.findLast(time + 2), 2);
assertEquals(timeline.findLast(time + 3), 4);
assertEquals(timeline.findLast(time + 4), 5);
assertEquals(timeline.findLast(time + 5), 5);
assertEquals(timeline.findLast(time + 2.00), 2);
assertEquals(timeline.findLast(time + 2.01), 2);
assertEquals(timeline.findLast(time + 2.90), 2);
assertEquals(timeline.findLast(time + 3.01), 4);
assertEquals(timeline.findLast(time + 3.90), 4);
assertEquals(timeline.findLast(time + 4.00), 5);
let startTime = time;
let endTime = time + 2;
......@@ -51,3 +99,73 @@ import { LogEntry} from "../../../tools/system-analyzer/log/log.mjs";
let entry = timeline.at(entryIdx);
assertEquals(entry.time, time + 1);
})();
(function testChunks() {
const id1 = "0x3e7e082470cd";
const time = 10;
const entry1 = new TestLogEntry(id1, time + 0);
const entry2 = new TestLogEntry(id1, time + 1);
const entry3 = new TestLogEntry(id1, time + 2);
const entry4 = new TestLogEntry(id1, time + 3);
const entries = [entry1, entry2, entry3, entry4];
const timeline1 = new Timeline(TestLogEntry, entries);
assertEquals(timeline1.length, 4);
assertEquals(timeline1.startTime, time);
assertEquals(timeline1.endTime, time + 3);
const chunks1_1 = timeline1.chunks(1);
assertEquals(chunks1_1.length, 1);
assertEquals(chunks1_1[0].start, timeline1.startTime);
assertEquals(chunks1_1[0].end, timeline1.endTime);
assertArrayEquals(chunks1_1[0].items, entries);
const chunks1_4 = timeline1.chunks(4);
assertEquals(chunks1_4.length, 4);
assertEquals(chunks1_4[0].start, timeline1.startTime);
assertArrayEquals(chunks1_4[0].items, [entry1]);
assertEquals(chunks1_4[1].start, 10.75);
assertArrayEquals(chunks1_4[1].items, [entry2]);
assertEquals(chunks1_4[2].start, 11.5);
assertArrayEquals(chunks1_4[2].items, [entry3]);
assertEquals(chunks1_4[3].end, timeline1.endTime);
assertArrayEquals(chunks1_4[3].items, [entry4]);
const timeline2 = new Timeline(TestLogEntry, entries, 0, 100);
assertEquals(timeline2.length, 4);
assertEquals(timeline2.startTime, 0);
assertEquals(timeline2.endTime, 100);
assertEquals(timeline2.duration(), 100);
const chunks2_1 = timeline2.chunks(1);
assertEquals(chunks2_1.length, 1);
assertEquals(chunks2_1[0].start, timeline2.startTime);
assertEquals(chunks2_1[0].end, timeline2.endTime);
assertArrayEquals(chunks1_1[0].items, entries);
const chunks2_2 = timeline2.chunks(2);
assertEquals(chunks2_2.length, 2);
assertEquals(chunks2_2[0].start, 0);
assertEquals(chunks2_2[0].end, 50);
assertArrayEquals(chunks2_2[0].items, entries);
assertEquals(chunks2_2[1].start, 50);
assertEquals(chunks2_2[1].end, 100);
assertArrayEquals(chunks2_2[1].items, []);
const chunks2_4 = timeline2.chunks(4);
assertEquals(chunks2_4.length, 4);
assertEquals(chunks2_4[0].start, 0);
assertEquals(chunks2_4[0].end, 25);
assertArrayEquals(chunks2_4[0].items, entries);
assertEquals(chunks2_4[1].start, 25);
assertEquals(chunks2_4[1].end, 50);
assertArrayEquals(chunks2_4[1].items, []);
assertEquals(chunks2_4[2].start, 50);
assertEquals(chunks2_4[2].end, 75);
assertArrayEquals(chunks2_4[2].items, []);
assertEquals(chunks2_4[3].start, 75);
assertEquals(chunks2_4[3].end, 100);
assertArrayEquals(chunks2_4[3].items, []);
})();
\ No newline at end of file
......@@ -222,6 +222,9 @@ export class Processor extends LogReader {
processSharedLibrary(name, start, end, aslr_slide) {
const entry = this._profile.addLibrary(name, start, end);
entry.logEntry = new SharedLibLogEntry(entry);
// Many events rely on having a script around, creating fake entries for
// shared libraries.
this._profile.addScriptSource(-1, name, '');
}
processCodeCreation(type, kind, timestamp, start, size, name, maybe_func) {
......
......@@ -13,14 +13,17 @@ class Timeline {
_selection;
_breakdown;
constructor(model, values = [], startTime = 0, endTime = 0) {
constructor(model, values = [], startTime = null, endTime = null) {
this._model = model;
this._values = values;
this.startTime = startTime;
this.endTime = endTime;
if (values.length > 0) {
if (startTime === 0) this.startTime = values[0].time;
if (endTime === 0) this.endTime = values[values.length - 1].time;
if (startTime === null) this.startTime = values[0].time;
if (endTime === null) this.endTime = values[values.length - 1].time;
} else {
if (startTime === null) this.startTime = 0;
if (endTime === null) this.endTime = 0;
}
}
......@@ -130,15 +133,16 @@ class Timeline {
forEachChunkSize(count, fn) {
if (this.isEmpty()) return;
const increment = this.duration() / count;
let currentTime = this.first().time + increment;
let currentTime = this.startTime;
let index = 0;
for (let i = 0; i < count; i++) {
const nextIndex = this.find(currentTime, index);
for (let i = 0; i < count - 1; i++) {
const nextTime = currentTime + increment;
const nextIndex = this.findLast(nextTime, index);
fn(index, nextIndex, currentTime, nextTime);
index = nextIndex;
index = nextIndex + 1;
currentTime = nextTime;
}
fn(index, this._values.length - 1, currentTime, this.endTime);
}
chunkSizes(count) {
......@@ -150,7 +154,7 @@ class Timeline {
chunks(count, predicate = undefined) {
const chunks = [];
this.forEachChunkSize(count, (start, end, startTime, endTime) => {
let items = this._values.slice(start, end);
let items = this._values.slice(start, end + 1);
if (predicate !== undefined) items = items.filter(predicate);
chunks.push(new Chunk(chunks.length, startTime, endTime, items));
});
......@@ -165,11 +169,29 @@ class Timeline {
return this._values.slice(firstIndex, lastIndex);
}
// Return the first index for the first element at {time}.
// Return the first index with element.time >= time.
find(time, offset = 0) {
return this.findFirst(time, offset);
}
findFirst(time, offset = 0) {
return this._find(this._values, each => each.time - time, offset);
}
// Return the last index with element.time <= time.
findLast(time, offset = 0) {
const nextTime = time + 1;
let index = (this.last().time <= nextTime) ?
this.length :
this.findFirst(nextTime + 1, offset);
// Typically we just have to look at the previous element.
while (index > 0) {
index--;
if (this._values[index].time <= time) return index;
}
return -1;
}
// Return the first index for which compareFn(item) is >= 0;
_find(array, compareFn, offset = 0) {
let minIndex = offset;
......
......@@ -64,6 +64,10 @@ found in the LICENSE file. -->
animation: spin 1s ease-in-out infinite;
}
#label {
user-select: none;
}
@keyframes spin {
0% {
transform: rotate(0deg);
......
......@@ -14,6 +14,7 @@ export class TimelineTrackBase extends V8CustomElement {
_nofChunks = 500;
_chunks = [];
_selectedEntry;
_focusedEntry;
_timeToPixel;
_timeStartPixelOffset;
_legend;
......@@ -27,6 +28,16 @@ export class TimelineTrackBase extends V8CustomElement {
this._selectionHandler = new SelectionHandler(this);
this._legend = new Legend(this.$('#legendTable'));
this._legend.onFilter = (type) => this._handleFilterTimeline();
this.timelineChunks = this.$('#timelineChunks');
this.timelineSamples = this.$('#timelineSamples');
this.timelineNode = this.$('#timeline');
this.toolTipTargetNode = this.$('#toolTipTarget');
this.hitPanelNode = this.$('#hitPanel');
this.timelineAnnotationsNode = this.$('#timelineAnnotations');
this.timelineMarkersNode = this.$('#timelineMarkers');
this._scalableContentNode = this.$('#scalableContent');
this.timelineNode.addEventListener(
'scroll', e => this._handleTimelineScroll(e));
this.hitPanelNode.onclick = this._handleClick.bind(this);
......@@ -109,47 +120,6 @@ export class TimelineTrackBase extends V8CustomElement {
return relativePosX;
}
get toolTipTargetNode() {
return this.$('#toolTipTarget');
}
get timelineChunks() {
if (this._timelineChunks === undefined) {
this._timelineChunks = this.$('#timelineChunks');
}
return this._timelineChunks;
}
get timelineSamples() {
if (this._timelineSamples === undefined) {
this._timelineSamples = this.$('#timelineSamples');
}
return this._timelineSamples;
}
get timelineNode() {
if (this._timelineNode === undefined) {
this._timelineNode = this.$('#timeline');
}
return this._timelineNode;
}
get hitPanelNode() {
return this.$('#hitPanel');
}
get timelineAnnotationsNode() {
return this.$('#timelineAnnotations');
}
get timelineMarkersNode() {
return this.$('#timelineMarkers');
}
get _scalableContentNode() {
return this.$('#scalableContent');
}
set nofChunks(count) {
this._nofChunks = count | 0;
this._updateChunks();
......@@ -304,15 +274,30 @@ export class TimelineTrackBase extends V8CustomElement {
}
_drawAnnotations(logEntry, time) {
// Subclass responsibility.
if (!this._focusedEntry) return;
this._drawEntryMark(this._focusedEntry);
}
_drawEntryMark(entry) {
const [x, y] = this._positionForEntry(entry);
const color = this._legend.colorForType(entry.type);
const mark =
`<circle cx=${x} cy=${y} r=3 stroke=${color} class=annotationPoint />`;
this.timelineAnnotationsNode.innerHTML = mark;
}
_handleUnlockedMouseEvent(event) {
const logEntry = this._getEntryForEvent(event);
if (!logEntry) return false;
this.dispatchEvent(new ToolTipEvent(logEntry, this.toolTipTargetNode));
this._focusedEntry = this._getEntryForEvent(event);
if (!this._focusedEntry) return false;
this._updateToolTip(event);
const time = this.positionToTime(event.pageX);
this._drawAnnotations(logEntry, time);
this._drawAnnotations(this._focusedEntry, time);
}
_updateToolTip(event) {
this.dispatchEvent(
new ToolTipEvent(this._focusedEntry, this.toolTipTargetNode));
event.stopImmediatePropagation();
}
_handleClick(event) {
......@@ -322,8 +307,6 @@ export class TimelineTrackBase extends V8CustomElement {
// Do this unconditionally since we want the tooltip to be update to the
// latest locked state.
this._handleUnlockedMouseEvent(event);
event.stopImmediatePropagation();
event.stopPropagation();
return false;
}
......@@ -341,18 +324,34 @@ export class TimelineTrackBase extends V8CustomElement {
_handleMouseMove(event) {
if (event.button !== 0) return;
if (this._selectionHandler.isSelecting) return false;
if (this.isLocked) return false;
if (this.isLocked) {
this._updateToolTip(event);
return false;
}
this._handleUnlockedMouseEvent(event);
}
_getChunkForEvent(event) {
const time = this.positionToTime(event.pageX);
return this._chunkForTime(time);
}
_chunkForTime(time) {
const chunkIndex = ((time - this._timeline.startTime) /
this._timeline.duration() * this._nofChunks) |
0;
return this.chunks[chunkIndex];
}
_positionForEntry(entry) {
const chunk = this._chunkForTime(entry.time);
if (chunk === undefined) return [-1, -1];
const xFrom = (chunk.index * kChunkWidth + kChunkVisualWidth / 2) | 0;
const yFrom = kTimelineHeight - chunk.yOffset(entry) | 0;
console.log(xFrom, yFrom);
return [xFrom, yFrom];
}
_getEntryForEvent(event) {
const chunk = this._getChunkForEvent(event);
if (chunk?.isEmpty() ?? true) return false;
......
......@@ -33,7 +33,7 @@ DOM.defineCustomElement('view/timeline/timeline-track', 'timeline-track-map',
strokeColor} class=annotationPoint />`
}
_drawAnnotations(logEntry) {
_drawAnnotations(logEntry, time) {
if (!(logEntry instanceof MapLogEntry)) return;
if (!logEntry.edge) {
this.timelineAnnotationsNode.innerHTML = '';
......
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