• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2021 the V8 project authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import {kChunkVisualWidth, MapLogEntry} from '../../log/map.mjs';
6import {FocusEvent} from '../events.mjs';
7import {CSSColor, DOM} from '../helper.mjs';
8
9import {TimelineTrackBase} from './timeline-track-base.mjs'
10
11DOM.defineCustomElement('view/timeline/timeline-track', 'timeline-track-map',
12                        (templateText) =>
13                            class TimelineTrackMap extends TimelineTrackBase {
14  constructor() {
15    super(templateText);
16    this.navigation = new Navigation(this)
17  }
18
19  _handleKeyDown(event) {}
20
21  getMapStyle(map) {
22    return map.edge && map.edge.from ? CSSColor.onBackgroundColor :
23                                       CSSColor.onPrimaryColor;
24  }
25
26  markMap(map) {
27    const [x, y] = map.position(this.chunks);
28    const strokeColor = this.getMapStyle(map);
29    return `<circle cx=${x} cy=${y} r=${2} stroke=${
30        strokeColor} class=annotationPoint />`
31  }
32
33  markSelectedMap(map) {
34    const [x, y] = map.position(this.chunks);
35    const strokeColor = this.getMapStyle(map);
36    return `<circle cx=${x} cy=${y} r=${3} stroke=${
37        strokeColor} class=annotationPoint />`
38  }
39
40  _drawAnnotations(logEntry, time) {
41    if (!(logEntry instanceof MapLogEntry)) return;
42    if (!logEntry.edge) {
43      this.timelineAnnotationsNode.innerHTML = '';
44      return;
45    }
46    // Draw the trace of maps in reverse order to make sure the outgoing
47    // transitions of previous maps aren't drawn over.
48    const kOpaque = 1.0;
49    let stack = [];
50    let current = logEntry;
51    while (current !== undefined) {
52      stack.push(current);
53      current = current.parent;
54    }
55
56    // Draw outgoing refs as fuzzy background. Skip the last map entry.
57    let buffer = '';
58    let nofEdges = 0;
59    const kMaxOutgoingEdges = 100;
60    for (let i = stack.length - 2; i >= 0; i--) {
61      const map = stack[i].parent;
62      nofEdges += map.children.length;
63      if (nofEdges > kMaxOutgoingEdges) break;
64      buffer += this.drawOutgoingEdges(map, 0.4, 1);
65    }
66
67    // Draw main connection.
68    let labelOffset = 15;
69    let xPrev = 0;
70    for (let i = stack.length - 1; i >= 0; i--) {
71      let map = stack[i];
72      if (map.edge) {
73        const [xTo, data] = this.drawEdge(map.edge, kOpaque, labelOffset);
74        buffer += data;
75        if (xTo == xPrev) {
76          labelOffset += 10;
77        } else {
78          labelOffset = 15
79        }
80        xPrev = xTo;
81      }
82      buffer += this.markMap(map);
83    }
84
85    buffer += this.drawOutgoingEdges(logEntry, 0.9, 3);
86    // Mark selected map
87    buffer += this.markSelectedMap(logEntry);
88    this.timelineAnnotationsNode.innerHTML = buffer;
89  }
90
91  drawEdge(edge, opacity, labelOffset = 20) {
92    let buffer = '';
93    if (!edge.from || !edge.to) return [-1, buffer];
94    const [xFrom, yFrom] = edge.from.position(this.chunks);
95    const [xTo, yTo] = edge.to.position(this.chunks);
96    const sameChunk = xTo == xFrom;
97    if (sameChunk) labelOffset += 10;
98    const color = this._legend.colorForType(edge.type);
99    const offsetX = 20;
100    const midX = xFrom + (xTo - xFrom) / 2;
101    const midY = (yFrom + yTo) / 2 - 100;
102    if (!sameChunk) {
103      if (opacity == 1.0) {
104        buffer += `<path d="M ${xFrom} ${yFrom} Q ${midX} ${midY}, ${xTo} ${
105            yTo}" class=strokeBG />`
106      }
107      buffer += `<path d="M ${xFrom} ${yFrom} Q ${midX} ${midY}, ${xTo} ${
108          yTo}" stroke=${color} fill=none opacity=${opacity} />`
109    } else {
110      if (opacity == 1.0) {
111        buffer += `<line x1=${xFrom} x2=${xTo} y1=${yFrom} y2=${
112            yTo} class=strokeBG />`;
113      }
114      buffer += `<line x1=${xFrom} x2=${xTo} y1=${yFrom} y2=${yTo} stroke=${
115          color} fill=none opacity=${opacity} />`;
116    }
117    if (opacity == 1.0) {
118      const centerX = sameChunk ? xTo : ((xFrom / 2 + midX + xTo / 2) / 2) | 0;
119      const centerY = sameChunk ? yTo : ((yFrom / 2 + midY + yTo / 2) / 2) | 0;
120      const centerYTo = centerY - labelOffset;
121      buffer += `<line x1=${centerX} x2=${centerX + offsetX} y1=${centerY} y2=${
122          centerYTo} stroke=${color} fill=none opacity=${opacity} />`;
123      buffer += `<text x=${centerX + offsetX + 2} y=${
124          centerYTo} class=annotationLabel opacity=${opacity} >${
125          edge.toString()}</text>`;
126    }
127    return [xTo, buffer];
128  }
129
130  drawOutgoingEdges(map, opacity = 1.0, max = 10, depth = 0) {
131    let buffer = '';
132    if (!map || depth >= max) return buffer;
133    const limit = Math.min(map.children.length, 100)
134    for (let i = 0; i < limit; i++) {
135      const edge = map.children[i];
136      const [xTo, data] = this.drawEdge(edge, opacity);
137      buffer += data;
138      buffer += this.drawOutgoingEdges(edge.to, opacity * 0.5, max, depth + 1);
139    }
140    return buffer;
141  }
142})
143
144class Navigation {
145  constructor(track) {
146    this._track = track;
147    this._track.addEventListener('keydown', this._handleKeyDown.bind(this));
148    this._map = undefined;
149  }
150
151  _handleKeyDown(event) {
152    if (!this._track.isFocused) return;
153    let handled = false;
154    switch (event.key) {
155      case 'ArrowDown':
156        handled = true;
157        if (event.shiftKey) {
158          this.selectPrevEdge();
159        } else {
160          this.moveInChunk(-1);
161        }
162        break;
163      case 'ArrowUp':
164        handled = true;
165        if (event.shiftKey) {
166          this.selectNextEdge();
167        } else {
168          this.moveInChunk(1);
169        }
170        break;
171      case 'ArrowLeft':
172        handled = true;
173        this.moveInChunks(false);
174        break;
175      case 'ArrowRight':
176        handled = true;
177        this.moveInChunks(true);
178        break;
179      case 'Enter':
180        handled = true;
181        this.selectMap();
182        break
183    }
184    if (handled) {
185      event.stopPropagation();
186      event.preventDefault();
187      return false;
188    }
189  }
190
191  get map() {
192    return this._track.focusedEntry;
193  }
194
195  set map(map) {
196    this._track.focusedEntry = map;
197  }
198
199  get chunks() {
200    return this._track.chunks;
201  }
202
203  selectMap() {
204    if (!this.map) return;
205    this._track.dispatchEvent(new FocusEvent(this.map))
206  }
207
208  selectNextEdge() {
209    if (!this.map) return;
210    if (this.map.children.length != 1) return;
211    this.show(this.map.children[0].to);
212  }
213
214  selectPrevEdge() {
215    if (!this.map) return;
216    if (!this.map.parent) return;
217    this.map = this.map.parent;
218  }
219
220  selectDefaultMap() {
221    this.map = this.chunks[0].at(0);
222  }
223
224  moveInChunks(next) {
225    if (!this.map) return this.selectDefaultMap();
226    let chunkIndex = this.map.chunkIndex(this.chunks);
227    let currentChunk = this.chunks[chunkIndex];
228    let currentIndex = currentChunk.indexOf(this.map);
229    let newChunk;
230    if (next) {
231      newChunk = chunk.next(this.chunks);
232    } else {
233      newChunk = chunk.prev(this.chunks);
234    }
235    if (!newChunk) return;
236    let newIndex = Math.min(currentIndex, newChunk.size() - 1);
237    this.map = newChunk.at(newIndex);
238  }
239
240  moveInChunk(delta) {
241    if (!this.map) return this.selectDefaultMap();
242    let chunkIndex = this.map.chunkIndex(this.chunks)
243    let chunk = this.chunks[chunkIndex];
244    let index = chunk.indexOf(this.map) + delta;
245    let map;
246    if (index < 0) {
247      map = chunk.prev(this.chunks).last();
248    } else if (index >= chunk.size()) {
249      map = chunk.next(this.chunks).first()
250    } else {
251      map = chunk.at(index);
252    }
253    this.map = map;
254  }
255}
256