// Copyright 2021 the V8 project authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import {kChunkVisualWidth, MapLogEntry} from '../../log/map.mjs'; import {FocusEvent} from '../events.mjs'; import {CSSColor, DOM} from '../helper.mjs'; import {TimelineTrackBase} from './timeline-track-base.mjs' DOM.defineCustomElement('view/timeline/timeline-track', 'timeline-track-map', (templateText) => class TimelineTrackMap extends TimelineTrackBase { constructor() { super(templateText); this.navigation = new Navigation(this) } _handleKeyDown(event) {} getMapStyle(map) { return map.edge && map.edge.from ? CSSColor.onBackgroundColor : CSSColor.onPrimaryColor; } markMap(map) { const [x, y] = map.position(this.chunks); const strokeColor = this.getMapStyle(map); return `` } markSelectedMap(map) { const [x, y] = map.position(this.chunks); const strokeColor = this.getMapStyle(map); return `` } _drawAnnotations(logEntry, time) { if (!(logEntry instanceof MapLogEntry)) return; if (!logEntry.edge) { this.timelineAnnotationsNode.innerHTML = ''; return; } // Draw the trace of maps in reverse order to make sure the outgoing // transitions of previous maps aren't drawn over. const kOpaque = 1.0; let stack = []; let current = logEntry; while (current !== undefined) { stack.push(current); current = current.parent; } // Draw outgoing refs as fuzzy background. Skip the last map entry. let buffer = ''; let nofEdges = 0; const kMaxOutgoingEdges = 100; for (let i = stack.length - 2; i >= 0; i--) { const map = stack[i].parent; nofEdges += map.children.length; if (nofEdges > kMaxOutgoingEdges) break; buffer += this.drawOutgoingEdges(map, 0.4, 1); } // Draw main connection. let labelOffset = 15; let xPrev = 0; for (let i = stack.length - 1; i >= 0; i--) { let map = stack[i]; if (map.edge) { const [xTo, data] = this.drawEdge(map.edge, kOpaque, labelOffset); buffer += data; if (xTo == xPrev) { labelOffset += 10; } else { labelOffset = 15 } xPrev = xTo; } buffer += this.markMap(map); } buffer += this.drawOutgoingEdges(logEntry, 0.9, 3); // Mark selected map buffer += this.markSelectedMap(logEntry); this.timelineAnnotationsNode.innerHTML = buffer; } drawEdge(edge, opacity, labelOffset = 20) { let buffer = ''; if (!edge.from || !edge.to) return [-1, buffer]; const [xFrom, yFrom] = edge.from.position(this.chunks); const [xTo, yTo] = edge.to.position(this.chunks); const sameChunk = xTo == xFrom; if (sameChunk) labelOffset += 10; const color = this._legend.colorForType(edge.type); const offsetX = 20; const midX = xFrom + (xTo - xFrom) / 2; const midY = (yFrom + yTo) / 2 - 100; if (!sameChunk) { if (opacity == 1.0) { buffer += `` } buffer += `` } else { if (opacity == 1.0) { buffer += ``; } buffer += ``; } if (opacity == 1.0) { const centerX = sameChunk ? xTo : ((xFrom / 2 + midX + xTo / 2) / 2) | 0; const centerY = sameChunk ? yTo : ((yFrom / 2 + midY + yTo / 2) / 2) | 0; const centerYTo = centerY - labelOffset; buffer += ``; buffer += `${ edge.toString()}`; } return [xTo, buffer]; } drawOutgoingEdges(map, opacity = 1.0, max = 10, depth = 0) { let buffer = ''; if (!map || depth >= max) return buffer; const limit = Math.min(map.children.length, 100) for (let i = 0; i < limit; i++) { const edge = map.children[i]; const [xTo, data] = this.drawEdge(edge, opacity); buffer += data; buffer += this.drawOutgoingEdges(edge.to, opacity * 0.5, max, depth + 1); } return buffer; } }) class Navigation { constructor(track) { this._track = track; this._track.addEventListener('keydown', this._handleKeyDown.bind(this)); this._map = undefined; } _handleKeyDown(event) { if (!this._track.isFocused) return; let handled = false; switch (event.key) { case 'ArrowDown': handled = true; if (event.shiftKey) { this.selectPrevEdge(); } else { this.moveInChunk(-1); } break; case 'ArrowUp': handled = true; if (event.shiftKey) { this.selectNextEdge(); } else { this.moveInChunk(1); } break; case 'ArrowLeft': handled = true; this.moveInChunks(false); break; case 'ArrowRight': handled = true; this.moveInChunks(true); break; case 'Enter': handled = true; this.selectMap(); break } if (handled) { event.stopPropagation(); event.preventDefault(); return false; } } get map() { return this._track.focusedEntry; } set map(map) { this._track.focusedEntry = map; } get chunks() { return this._track.chunks; } selectMap() { if (!this.map) return; this._track.dispatchEvent(new FocusEvent(this.map)) } selectNextEdge() { if (!this.map) return; if (this.map.children.length != 1) return; this.show(this.map.children[0].to); } selectPrevEdge() { if (!this.map) return; if (!this.map.parent) return; this.map = this.map.parent; } selectDefaultMap() { this.map = this.chunks[0].at(0); } moveInChunks(next) { if (!this.map) return this.selectDefaultMap(); let chunkIndex = this.map.chunkIndex(this.chunks); let currentChunk = this.chunks[chunkIndex]; let currentIndex = currentChunk.indexOf(this.map); let newChunk; if (next) { newChunk = chunk.next(this.chunks); } else { newChunk = chunk.prev(this.chunks); } if (!newChunk) return; let newIndex = Math.min(currentIndex, newChunk.size() - 1); this.map = newChunk.at(newIndex); } moveInChunk(delta) { if (!this.map) return this.selectDefaultMap(); let chunkIndex = this.map.chunkIndex(this.chunks) let chunk = this.chunks[chunkIndex]; let index = chunk.indexOf(this.map) + delta; let map; if (index < 0) { map = chunk.prev(this.chunks).last(); } else if (index >= chunk.size()) { map = chunk.next(this.chunks).first() } else { map = chunk.at(index); } this.map = map; } }