1// Copyright 2020 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 {LogEntry} from './log.mjs'; 6 7// =========================================================================== 8// Map Log Events 9 10export const kChunkHeight = 200; 11export const kChunkWidth = 10; 12export const kChunkVisualWidth = 6; 13 14function define(prototype, name, fn) { 15 Object.defineProperty(prototype, name, {value: fn, enumerable: false}); 16} 17 18define(Array.prototype, 'max', function(fn) { 19 if (this.length === 0) return undefined; 20 if (fn === undefined) fn = (each) => each; 21 let max = fn(this[0]); 22 for (let i = 1; i < this.length; i++) { 23 max = Math.max(max, fn(this[i])); 24 } 25 return max; 26}) 27define(Array.prototype, 'first', function() { 28 return this[0] 29}); 30define(Array.prototype, 'last', function() { 31 return this[this.length - 1] 32}); 33 34// =========================================================================== 35// Map Log Events 36 37export class MapLogEntry extends LogEntry { 38 constructor(id, time) { 39 if (!time) throw new Error('Invalid time'); 40 // Use MapLogEntry.type getter instead of property, since we only know the 41 // type lazily from the incoming transition. 42 super(undefined, time); 43 this.id = id; 44 MapLogEntry.set(id, this); 45 this.edge = undefined; 46 this.children = []; 47 this.depth = 0; 48 this._isDeprecated = false; 49 this.deprecatedTargets = null; 50 this.leftId = 0; 51 this.rightId = 0; 52 this.entry = undefined; 53 this.description = ''; 54 } 55 56 get functionName() { 57 return this.entry?.functionName; 58 } 59 60 get code() { 61 return this.entry?.logEntry; 62 } 63 64 toString() { 65 return `Map(${this.id})`; 66 } 67 68 finalizeRootMap(id) { 69 let stack = [this]; 70 while (stack.length > 0) { 71 let current = stack.pop(); 72 if (current.leftId !== 0) { 73 console.warn('Skipping potential parent loop between maps:', current) 74 continue; 75 } 76 current.finalize(id); 77 id += 1; 78 current.children.forEach(edge => stack.push(edge.to)); 79 // TODO implement rightId 80 } 81 return id; 82 } 83 84 finalize(id) { 85 // Initialize preorder tree traversal Ids for fast subtree inclusion checks 86 if (id <= 0) throw 'invalid id'; 87 let currentId = id; 88 this.leftId = currentId 89 } 90 91 get parent() { 92 return this.edge?.from; 93 } 94 95 isDeprecated() { 96 return this._isDeprecated; 97 } 98 99 deprecate() { 100 this._isDeprecated = true; 101 } 102 103 isRoot() { 104 return this.edge === undefined || this.edge.from === undefined; 105 } 106 107 contains(map) { 108 return this.leftId < map.leftId && map.rightId < this.rightId; 109 } 110 111 addEdge(edge) { 112 this.children.push(edge); 113 } 114 115 chunkIndex(chunks) { 116 // Did anybody say O(n)? 117 for (let i = 0; i < chunks.length; i++) { 118 let chunk = chunks[i]; 119 if (chunk.isEmpty()) continue; 120 if (chunk.last().time < this.time) continue; 121 return i; 122 } 123 return -1; 124 } 125 126 position(chunks) { 127 const index = this.chunkIndex(chunks); 128 if (index === -1) return [0, 0]; 129 const xFrom = (index * kChunkWidth + kChunkVisualWidth / 2) | 0; 130 const yFrom = kChunkHeight - chunks[index].yOffset(this) | 0; 131 return [xFrom, yFrom]; 132 } 133 134 transitions() { 135 let transitions = Object.create(null); 136 let current = this; 137 while (current) { 138 let edge = current.edge; 139 if (edge && edge.isTransition()) { 140 transitions[edge.name] = edge; 141 } 142 current = current.parent; 143 } 144 return transitions; 145 } 146 147 get type() { 148 return this.edge?.type ?? 'new'; 149 } 150 151 get reason() { 152 return this.edge?.reason; 153 } 154 155 get property() { 156 return this.edge?.name; 157 } 158 159 isBootstrapped() { 160 return this.edge === undefined; 161 } 162 163 getParents() { 164 let parents = []; 165 let current = this.parent; 166 while (current) { 167 parents.push(current); 168 current = current.parent; 169 } 170 return parents; 171 } 172 173 static get(id, time = undefined) { 174 let maps = this.cache.get(id); 175 if (maps === undefined) return undefined; 176 if (time !== undefined) { 177 for (let i = 1; i < maps.length; i++) { 178 if (maps[i].time > time) { 179 return maps[i - 1]; 180 } 181 } 182 } 183 // default return the latest 184 return maps[maps.length - 1]; 185 } 186 187 static set(id, map) { 188 if (this.cache.has(id)) { 189 this.cache.get(id).push(map); 190 } else { 191 this.cache.set(id, [map]); 192 } 193 } 194 195 static get propertyNames() { 196 return [ 197 'type', 'reason', 'property', 'parent', 'functionName', 'sourcePosition', 198 'script', 'code', 'id', 'description' 199 ]; 200 } 201} 202 203MapLogEntry.cache = new Map(); 204 205// =========================================================================== 206export class Edge { 207 constructor(type, name, reason, time, from, to) { 208 this.type = type; 209 this.name = name; 210 this.reason = reason; 211 this.time = time; 212 this.from = from; 213 this.to = to; 214 } 215 216 updateFrom(edge) { 217 if (this.to !== edge.to || this.from !== edge.from) { 218 throw new Error('Invalid Edge updated', this, to); 219 } 220 this.type = edge.type; 221 this.name = edge.name; 222 this.reason = edge.reason; 223 this.time = edge.time; 224 } 225 226 finishSetup() { 227 const from = this.from; 228 const to = this.to; 229 if (to?.time < from?.time) { 230 // This happens for map deprecation where the transition tree is converted 231 // in reverse order. 232 console.warn('Invalid time order'); 233 } 234 if (from) from.addEdge(this); 235 if (to === undefined) return; 236 to.edge = this; 237 if (from === undefined) return; 238 if (to === from) throw 'From and to must be distinct.'; 239 let newDepth = from.depth + 1; 240 if (to.depth > 0 && to.depth != newDepth) { 241 console.warn('Depth has already been initialized'); 242 } 243 to.depth = newDepth; 244 } 245 246 chunkIndex(chunks) { 247 // Did anybody say O(n)? 248 for (let i = 0; i < chunks.length; i++) { 249 let chunk = chunks[i]; 250 if (chunk.isEmpty()) continue; 251 if (chunk.last().time < this.time) continue; 252 return i; 253 } 254 return -1; 255 } 256 257 parentEdge() { 258 if (!this.from) return undefined; 259 return this.from.edge; 260 } 261 262 chainLength() { 263 let length = 0; 264 let prev = this; 265 while (prev) { 266 prev = this.parent; 267 length++; 268 } 269 return length; 270 } 271 272 isTransition() { 273 return this.type === 'Transition' 274 } 275 276 isFastToSlow() { 277 return this.type === 'Normalize' 278 } 279 280 isSlowToFast() { 281 return this.type === 'SlowToFast' 282 } 283 284 isInitial() { 285 return this.type === 'InitialMap' 286 } 287 288 isBootstrapped() { 289 return this.type === 'new' 290 } 291 292 isReplaceDescriptors() { 293 return this.type === 'ReplaceDescriptors' 294 } 295 296 isCopyAsPrototype() { 297 return this.reason === 'CopyAsPrototype' 298 } 299 300 isOptimizeAsPrototype() { 301 return this.reason === 'OptimizeAsPrototype' 302 } 303 304 symbol() { 305 if (this.isTransition()) return '+'; 306 if (this.isFastToSlow()) return '⊡'; 307 if (this.isSlowToFast()) return '⊛'; 308 if (this.isReplaceDescriptors()) { 309 if (this.name) return '+'; 310 return '∥'; 311 } 312 return ''; 313 } 314 315 toString() { 316 let s = this.symbol(); 317 if (this.isTransition()) return s + this.name; 318 if (this.isFastToSlow()) return s + this.reason; 319 if (this.isCopyAsPrototype()) return s + 'Copy as Prototype'; 320 if (this.isOptimizeAsPrototype()) { 321 return s + 'Optimize as Prototype'; 322 } 323 if (this.isReplaceDescriptors() && this.name) { 324 return this.type + ' ' + this.symbol() + this.name; 325 } 326 return this.type + ' ' + (this.reason ? this.reason : '') + ' ' + 327 (this.name ? this.name : '') 328 } 329}