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