1// Copyright 2009 the V8 project authors. All rights reserved. 2// Redistribution and use in source and binary forms, with or without 3// modification, are permitted provided that the following conditions are 4// met: 5// 6// * Redistributions of source code must retain the above copyright 7// notice, this list of conditions and the following disclaimer. 8// * Redistributions in binary form must reproduce the above 9// copyright notice, this list of conditions and the following 10// disclaimer in the documentation and/or other materials provided 11// with the distribution. 12// * Neither the name of Google Inc. nor the names of its 13// contributors may be used to endorse or promote products derived 14// from this software without specific prior written permission. 15// 16// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 17// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 18// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 19// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 20// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 21// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 22// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 23// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 24// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 26// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 28import { CodeMap, CodeEntry } from "./codemap.mjs"; 29import { ConsArray } from "./consarray.mjs"; 30import { WebInspector } from "./sourcemap.mjs"; 31 32// Used to associate log entries with source positions in scripts. 33// TODO: move to separate modules 34export class SourcePosition { 35 script = null; 36 line = -1; 37 column = -1; 38 entries = []; 39 isFunction = false; 40 originalPosition = undefined; 41 42 constructor(script, line, column) { 43 this.script = script; 44 this.line = line; 45 this.column = column; 46 } 47 48 addEntry(entry) { 49 this.entries.push(entry); 50 } 51 52 toString() { 53 return `${this.script.name}:${this.line}:${this.column}`; 54 } 55 56 get functionPosition() { 57 // TODO(cbruni) 58 return undefined; 59 } 60 61 get toolTipDict() { 62 return { 63 title: this.toString(), 64 __this__: this, 65 script: this.script, 66 entries: this.entries, 67 } 68 } 69} 70 71export class Script { 72 url; 73 source = ""; 74 name; 75 sourcePosition = undefined; 76 // Map<line, Map<column, SourcePosition>> 77 lineToColumn = new Map(); 78 _entries = []; 79 _sourceMapState = "unknown"; 80 81 constructor(id) { 82 this.id = id; 83 this.sourcePositions = []; 84 } 85 86 update(url, source) { 87 this.url = url; 88 this.name = Script.getShortestUniqueName(url, this); 89 this.source = source; 90 } 91 92 get length() { 93 return this.source.length; 94 } 95 96 get entries() { 97 return this._entries; 98 } 99 100 get startLine() { 101 return this.sourcePosition?.line ?? 1; 102 } 103 104 get sourceMapState() { 105 return this._sourceMapState; 106 } 107 108 findFunctionSourcePosition(sourcePosition) { 109 // TODO(cbruni): implement 110 return undefined; 111 } 112 113 addSourcePosition(line, column, entry) { 114 let sourcePosition = this.lineToColumn.get(line)?.get(column); 115 if (sourcePosition === undefined) { 116 sourcePosition = new SourcePosition(this, line, column,) 117 this._addSourcePosition(line, column, sourcePosition); 118 } 119 if (this.sourcePosition === undefined && entry.entry?.type === "Script") { 120 // Mark the source position of scripts, for inline scripts which don't 121 // start at line 1. 122 this.sourcePosition = sourcePosition; 123 } 124 sourcePosition.addEntry(entry); 125 this._entries.push(entry); 126 return sourcePosition; 127 } 128 129 _addSourcePosition(line, column, sourcePosition) { 130 let columnToSourcePosition; 131 if (this.lineToColumn.has(line)) { 132 columnToSourcePosition = this.lineToColumn.get(line); 133 } else { 134 columnToSourcePosition = new Map(); 135 this.lineToColumn.set(line, columnToSourcePosition); 136 } 137 this.sourcePositions.push(sourcePosition); 138 columnToSourcePosition.set(column, sourcePosition); 139 } 140 141 toString() { 142 return `Script(${this.id}): ${this.name}`; 143 } 144 145 get toolTipDict() { 146 return { 147 title: this.toString(), 148 __this__: this, 149 id: this.id, 150 url: this.url, 151 source: this.source, 152 sourcePositions: this.sourcePositions 153 } 154 } 155 156 static getShortestUniqueName(url, script) { 157 const parts = url.split('/'); 158 const filename = parts[parts.length -1]; 159 const dict = this._dict ?? (this._dict = new Map()); 160 const matchingScripts = dict.get(filename); 161 if (matchingScripts == undefined) { 162 dict.set(filename, [script]); 163 return filename; 164 } 165 // TODO: find shortest unique substring 166 // Update all matching scripts to have a unique filename again. 167 for (let matchingScript of matchingScripts) { 168 matchingScript.name = script.url 169 } 170 matchingScripts.push(script); 171 return url; 172 } 173 174 ensureSourceMapCalculated(sourceMapFetchPrefix=undefined) { 175 if (this._sourceMapState !== "unknown") return; 176 177 const sourceMapURLMatch = 178 this.source.match(/\/\/# sourceMappingURL=(.*)\n/); 179 if (!sourceMapURLMatch) { 180 this._sourceMapState = "none"; 181 return; 182 } 183 184 this._sourceMapState = "loading"; 185 let sourceMapURL = sourceMapURLMatch[1]; 186 (async () => { 187 try { 188 let sourceMapPayload; 189 try { 190 sourceMapPayload = await fetch(sourceMapURL); 191 } catch (e) { 192 if (e instanceof TypeError && sourceMapFetchPrefix) { 193 // Try again with fetch prefix. 194 // TODO(leszeks): Remove the retry once the prefix is 195 // configurable. 196 sourceMapPayload = 197 await fetch(sourceMapFetchPrefix + sourceMapURL); 198 } else { 199 throw e; 200 } 201 } 202 sourceMapPayload = await sourceMapPayload.text(); 203 204 if (sourceMapPayload.startsWith(')]}')) { 205 sourceMapPayload = 206 sourceMapPayload.substring(sourceMapPayload.indexOf('\n')); 207 } 208 sourceMapPayload = JSON.parse(sourceMapPayload); 209 const sourceMap = 210 new WebInspector.SourceMap(sourceMapURL, sourceMapPayload); 211 212 const startLine = this.startLine; 213 for (const sourcePosition of this.sourcePositions) { 214 const line = sourcePosition.line - startLine; 215 const column = sourcePosition.column - 1; 216 const mapping = sourceMap.findEntry(line, column); 217 if (mapping) { 218 sourcePosition.originalPosition = { 219 source: new URL(mapping[2], sourceMapURL).href, 220 line: mapping[3] + 1, 221 column: mapping[4] + 1 222 }; 223 } else { 224 sourcePosition.originalPosition = {source: null, line:0, column:0}; 225 } 226 } 227 this._sourceMapState = "loaded"; 228 } catch (e) { 229 console.error(e); 230 this._sourceMapState = "failed"; 231 } 232 })(); 233 } 234} 235 236 237const kOffsetPairRegex = /C([0-9]+)O([0-9]+)/g; 238class SourcePositionTable { 239 constructor(encodedTable) { 240 this._offsets = []; 241 while (true) { 242 const regexResult = kOffsetPairRegex.exec(encodedTable); 243 if (!regexResult) break; 244 const codeOffset = parseInt(regexResult[1]); 245 const scriptOffset = parseInt(regexResult[2]); 246 if (isNaN(codeOffset) || isNaN(scriptOffset)) continue; 247 this._offsets.push({code: codeOffset, script: scriptOffset}); 248 } 249 } 250 251 getScriptOffset(codeOffset) { 252 if (codeOffset < 0) { 253 throw new Exception(`Invalid codeOffset=${codeOffset}, should be >= 0`); 254 } 255 for (let i = this.offsetTable.length - 1; i >= 0; i--) { 256 const offset = this._offsets[i]; 257 if (offset.code <= codeOffset) { 258 return offset.script; 259 } 260 } 261 return this._offsets[0].script; 262 } 263} 264 265 266class SourceInfo { 267 script; 268 start; 269 end; 270 positions; 271 inlined; 272 fns; 273 disassemble; 274 275 setSourcePositionInfo( 276 script, startPos, endPos, sourcePositionTableData, inliningPositions, 277 inlinedFunctions) { 278 this.script = script; 279 this.start = startPos; 280 this.end = endPos; 281 this.positions = sourcePositionTableData; 282 this.inlined = inliningPositions; 283 this.fns = inlinedFunctions; 284 this.sourcePositionTable = new SourcePositionTable(sourcePositionTableData); 285 } 286 287 setDisassemble(code) { 288 this.disassemble = code; 289 } 290 291 getSourceCode() { 292 return this.script.source?.substring(this.start, this.end); 293 } 294} 295 296const kProfileOperationMove = 0; 297const kProfileOperationDelete = 1; 298const kProfileOperationTick = 2; 299 300/** 301 * Creates a profile object for processing profiling-related events 302 * and calculating function execution times. 303 * 304 * @constructor 305 */ 306export class Profile { 307 codeMap_ = new CodeMap(); 308 topDownTree_ = new CallTree(); 309 bottomUpTree_ = new CallTree(); 310 c_entries_ = {__proto__:null}; 311 scripts_ = []; 312 urlToScript_ = new Map(); 313 warnings = new Set(); 314 315 serializeVMSymbols() { 316 let result = this.codeMap_.getAllStaticEntriesWithAddresses(); 317 result.concat(this.codeMap_.getAllLibraryEntriesWithAddresses()) 318 return result.map(([startAddress, codeEntry]) => { 319 return [codeEntry.getName(), startAddress, startAddress + codeEntry.size] 320 }); 321 } 322 323 /** 324 * Returns whether a function with the specified name must be skipped. 325 * Should be overridden by subclasses. 326 * 327 * @param {string} name Function name. 328 */ 329 skipThisFunction(name) { 330 return false; 331 } 332 333 /** 334 * Enum for profiler operations that involve looking up existing 335 * code entries. 336 * 337 * @enum {number} 338 */ 339 static Operation = { 340 MOVE: kProfileOperationMove, 341 DELETE: kProfileOperationDelete, 342 TICK: kProfileOperationTick 343 } 344 345 /** 346 * Enum for code state regarding its dynamic optimization. 347 * 348 * @enum {number} 349 */ 350 static CodeState = { 351 COMPILED: 0, 352 IGNITION: 1, 353 BASELINE: 2, 354 TURBOFAN: 5, 355 } 356 357 static VMState = { 358 JS: 0, 359 GC: 1, 360 PARSER: 2, 361 BYTECODE_COMPILER: 3, 362 // TODO(cbruni): add BASELINE_COMPILER 363 COMPILER: 4, 364 OTHER: 5, 365 EXTERNAL: 6, 366 IDLE: 7, 367 } 368 369 static CodeType = { 370 CPP: 0, 371 SHARED_LIB: 1 372 } 373 374 /** 375 * Parser for dynamic code optimization state. 376 */ 377 static parseState(s) { 378 switch (s) { 379 case '': 380 return this.CodeState.COMPILED; 381 case '~': 382 return this.CodeState.IGNITION; 383 case '^': 384 return this.CodeState.BASELINE; 385 case '*': 386 return this.CodeState.TURBOFAN; 387 } 388 throw new Error(`unknown code state: ${s}`); 389 } 390 391 static getKindFromState(state) { 392 if (state === this.CodeState.COMPILED) { 393 return "Builtin"; 394 } else if (state === this.CodeState.IGNITION) { 395 return "Unopt"; 396 } else if (state === this.CodeState.BASELINE) { 397 return "Baseline"; 398 } else if (state === this.CodeState.TURBOFAN) { 399 return "Opt"; 400 } 401 throw new Error(`unknown code state: ${state}`); 402 } 403 404 static vmStateString(state) { 405 switch (state) { 406 case this.VMState.JS: 407 return 'JS'; 408 case this.VMState.GC: 409 return 'GC'; 410 case this.VMState.PARSER: 411 return 'Parse'; 412 case this.VMState.BYTECODE_COMPILER: 413 return 'Compile Bytecode'; 414 case this.VMState.COMPILER: 415 return 'Compile'; 416 case this.VMState.OTHER: 417 return 'Other'; 418 case this.VMState.EXTERNAL: 419 return 'External'; 420 case this.VMState.IDLE: 421 return 'Idle'; 422 } 423 return 'unknown'; 424 } 425 426 /** 427 * Called whenever the specified operation has failed finding a function 428 * containing the specified address. Should be overriden by subclasses. 429 * See the Profile.Operation enum for the list of 430 * possible operations. 431 * 432 * @param {number} operation Operation. 433 * @param {number} addr Address of the unknown code. 434 * @param {number} opt_stackPos If an unknown address is encountered 435 * during stack strace processing, specifies a position of the frame 436 * containing the address. 437 */ 438 handleUnknownCode(operation, addr, opt_stackPos) { } 439 440 /** 441 * Registers a library. 442 * 443 * @param {string} name Code entry name. 444 * @param {number} startAddr Starting address. 445 * @param {number} endAddr Ending address. 446 */ 447 addLibrary(name, startAddr, endAddr) { 448 const entry = new CodeEntry(endAddr - startAddr, name, 'SHARED_LIB'); 449 this.codeMap_.addLibrary(startAddr, entry); 450 return entry; 451 } 452 453 /** 454 * Registers statically compiled code entry. 455 * 456 * @param {string} name Code entry name. 457 * @param {number} startAddr Starting address. 458 * @param {number} endAddr Ending address. 459 */ 460 addStaticCode(name, startAddr, endAddr) { 461 const entry = new CodeEntry(endAddr - startAddr, name, 'CPP'); 462 this.codeMap_.addStaticCode(startAddr, entry); 463 return entry; 464 } 465 466 /** 467 * Registers dynamic (JIT-compiled) code entry. 468 * 469 * @param {string} type Code entry type. 470 * @param {string} name Code entry name. 471 * @param {number} start Starting address. 472 * @param {number} size Code entry size. 473 */ 474 addCode(type, name, timestamp, start, size) { 475 const entry = new DynamicCodeEntry(size, type, name); 476 this.codeMap_.addCode(start, entry); 477 return entry; 478 } 479 480 /** 481 * Registers dynamic (JIT-compiled) code entry. 482 * 483 * @param {string} type Code entry type. 484 * @param {string} name Code entry name. 485 * @param {number} start Starting address. 486 * @param {number} size Code entry size. 487 * @param {number} funcAddr Shared function object address. 488 * @param {Profile.CodeState} state Optimization state. 489 */ 490 addFuncCode(type, name, timestamp, start, size, funcAddr, state) { 491 // As code and functions are in the same address space, 492 // it is safe to put them in a single code map. 493 let func = this.codeMap_.findDynamicEntryByStartAddress(funcAddr); 494 if (func === null) { 495 func = new FunctionEntry(name); 496 this.codeMap_.addCode(funcAddr, func); 497 } else if (func.name !== name) { 498 // Function object has been overwritten with a new one. 499 func.name = name; 500 } 501 let entry = this.codeMap_.findDynamicEntryByStartAddress(start); 502 if (entry !== null) { 503 if (entry.size === size && entry.func === func) { 504 // Entry state has changed. 505 entry.state = state; 506 } else { 507 this.codeMap_.deleteCode(start); 508 entry = null; 509 } 510 } 511 if (entry === null) { 512 entry = new DynamicFuncCodeEntry(size, type, func, state); 513 this.codeMap_.addCode(start, entry); 514 } 515 return entry; 516 } 517 518 /** 519 * Reports about moving of a dynamic code entry. 520 * 521 * @param {number} from Current code entry address. 522 * @param {number} to New code entry address. 523 */ 524 moveCode(from, to) { 525 try { 526 this.codeMap_.moveCode(from, to); 527 } catch (e) { 528 this.handleUnknownCode(kProfileOperationMove, from); 529 } 530 } 531 532 deoptCode(timestamp, code, inliningId, scriptOffset, bailoutType, 533 sourcePositionText, deoptReasonText) { 534 } 535 536 /** 537 * Reports about deletion of a dynamic code entry. 538 * 539 * @param {number} start Starting address. 540 */ 541 deleteCode(start) { 542 try { 543 this.codeMap_.deleteCode(start); 544 } catch (e) { 545 this.handleUnknownCode(kProfileOperationDelete, start); 546 } 547 } 548 549 /** 550 * Adds source positions for given code. 551 */ 552 addSourcePositions(start, scriptId, startPos, endPos, sourcePositionTable, 553 inliningPositions, inlinedFunctions) { 554 const script = this.getOrCreateScript(scriptId); 555 const entry = this.codeMap_.findDynamicEntryByStartAddress(start); 556 if (entry === null) return; 557 // Resolve the inlined functions list. 558 if (inlinedFunctions.length > 0) { 559 inlinedFunctions = inlinedFunctions.substring(1).split("S"); 560 for (let i = 0; i < inlinedFunctions.length; i++) { 561 const funcAddr = parseInt(inlinedFunctions[i]); 562 const func = this.codeMap_.findDynamicEntryByStartAddress(funcAddr); 563 if (func === null || func.funcId === undefined) { 564 // TODO: fix 565 this.warnings.add(`Could not find function ${inlinedFunctions[i]}`); 566 inlinedFunctions[i] = null; 567 } else { 568 inlinedFunctions[i] = func.funcId; 569 } 570 } 571 } else { 572 inlinedFunctions = []; 573 } 574 575 this.getOrCreateSourceInfo(entry).setSourcePositionInfo( 576 script, startPos, endPos, sourcePositionTable, inliningPositions, 577 inlinedFunctions); 578 } 579 580 addDisassemble(start, kind, disassemble) { 581 const entry = this.codeMap_.findDynamicEntryByStartAddress(start); 582 if (entry !== null) { 583 this.getOrCreateSourceInfo(entry).setDisassemble(disassemble); 584 } 585 return entry; 586 } 587 588 getOrCreateSourceInfo(entry) { 589 return entry.source ?? (entry.source = new SourceInfo()); 590 } 591 592 addScriptSource(id, url, source) { 593 const script = this.getOrCreateScript(id); 594 script.update(url, source); 595 this.urlToScript_.set(url, script); 596 } 597 598 getOrCreateScript(id) { 599 let script = this.scripts_[id]; 600 if (script === undefined) { 601 script = new Script(id); 602 this.scripts_[id] = script; 603 } 604 return script; 605 } 606 607 getScript(url) { 608 return this.urlToScript_.get(url); 609 } 610 611 /** 612 * Reports about moving of a dynamic code entry. 613 * 614 * @param {number} from Current code entry address. 615 * @param {number} to New code entry address. 616 */ 617 moveFunc(from, to) { 618 if (this.codeMap_.findDynamicEntryByStartAddress(from)) { 619 this.codeMap_.moveCode(from, to); 620 } 621 } 622 623 /** 624 * Retrieves a code entry by an address. 625 * 626 * @param {number} addr Entry address. 627 */ 628 findEntry(addr) { 629 return this.codeMap_.findEntry(addr); 630 } 631 632 /** 633 * Records a tick event. Stack must contain a sequence of 634 * addresses starting with the program counter value. 635 * 636 * @param {Array<number>} stack Stack sample. 637 */ 638 recordTick(time_ns, vmState, stack) { 639 const {nameStack, entryStack} = this.resolveAndFilterFuncs_(stack); 640 this.bottomUpTree_.addPath(nameStack); 641 nameStack.reverse(); 642 this.topDownTree_.addPath(nameStack); 643 return entryStack; 644 } 645 646 /** 647 * Translates addresses into function names and filters unneeded 648 * functions. 649 * 650 * @param {Array<number>} stack Stack sample. 651 */ 652 resolveAndFilterFuncs_(stack) { 653 const nameStack = []; 654 const entryStack = []; 655 let last_seen_c_function = ''; 656 let look_for_first_c_function = false; 657 for (let i = 0; i < stack.length; ++i) { 658 const pc = stack[i]; 659 const entry = this.codeMap_.findEntry(pc); 660 if (entry !== null) { 661 entryStack.push(entry); 662 const name = entry.getName(); 663 if (i === 0 && (entry.type === 'CPP' || entry.type === 'SHARED_LIB')) { 664 look_for_first_c_function = true; 665 } 666 if (look_for_first_c_function && entry.type === 'CPP') { 667 last_seen_c_function = name; 668 } 669 if (!this.skipThisFunction(name)) { 670 nameStack.push(name); 671 } 672 } else { 673 this.handleUnknownCode(kProfileOperationTick, pc, i); 674 if (i === 0) nameStack.push("UNKNOWN"); 675 entryStack.push(pc); 676 } 677 if (look_for_first_c_function && i > 0 && 678 (entry === null || entry.type !== 'CPP') 679 && last_seen_c_function !== '') { 680 if (this.c_entries_[last_seen_c_function] === undefined) { 681 this.c_entries_[last_seen_c_function] = 0; 682 } 683 this.c_entries_[last_seen_c_function]++; 684 look_for_first_c_function = false; // Found it, we're done. 685 } 686 } 687 return {nameStack, entryStack}; 688 } 689 690 /** 691 * Performs a BF traversal of the top down call graph. 692 * 693 * @param {function(CallTreeNode)} f Visitor function. 694 */ 695 traverseTopDownTree(f) { 696 this.topDownTree_.traverse(f); 697 } 698 699 /** 700 * Performs a BF traversal of the bottom up call graph. 701 * 702 * @param {function(CallTreeNode)} f Visitor function. 703 */ 704 traverseBottomUpTree(f) { 705 this.bottomUpTree_.traverse(f); 706 } 707 708 /** 709 * Calculates a top down profile for a node with the specified label. 710 * If no name specified, returns the whole top down calls tree. 711 * 712 * @param {string} opt_label Node label. 713 */ 714 getTopDownProfile(opt_label) { 715 return this.getTreeProfile_(this.topDownTree_, opt_label); 716 } 717 718 /** 719 * Calculates a bottom up profile for a node with the specified label. 720 * If no name specified, returns the whole bottom up calls tree. 721 * 722 * @param {string} opt_label Node label. 723 */ 724 getBottomUpProfile(opt_label) { 725 return this.getTreeProfile_(this.bottomUpTree_, opt_label); 726 } 727 728 /** 729 * Helper function for calculating a tree profile. 730 * 731 * @param {Profile.CallTree} tree Call tree. 732 * @param {string} opt_label Node label. 733 */ 734 getTreeProfile_(tree, opt_label) { 735 if (!opt_label) { 736 tree.computeTotalWeights(); 737 return tree; 738 } else { 739 const subTree = tree.cloneSubtree(opt_label); 740 subTree.computeTotalWeights(); 741 return subTree; 742 } 743 } 744 745 /** 746 * Calculates a flat profile of callees starting from a node with 747 * the specified label. If no name specified, starts from the root. 748 * 749 * @param {string} opt_label Starting node label. 750 */ 751 getFlatProfile(opt_label) { 752 const counters = new CallTree(); 753 const rootLabel = opt_label || CallTree.ROOT_NODE_LABEL; 754 const precs = {__proto__:null}; 755 precs[rootLabel] = 0; 756 const root = counters.findOrAddChild(rootLabel); 757 758 this.topDownTree_.computeTotalWeights(); 759 this.topDownTree_.traverseInDepth( 760 function onEnter(node) { 761 if (!(node.label in precs)) { 762 precs[node.label] = 0; 763 } 764 const nodeLabelIsRootLabel = node.label == rootLabel; 765 if (nodeLabelIsRootLabel || precs[rootLabel] > 0) { 766 if (precs[rootLabel] == 0) { 767 root.selfWeight += node.selfWeight; 768 root.totalWeight += node.totalWeight; 769 } else { 770 const rec = root.findOrAddChild(node.label); 771 rec.selfWeight += node.selfWeight; 772 if (nodeLabelIsRootLabel || precs[node.label] == 0) { 773 rec.totalWeight += node.totalWeight; 774 } 775 } 776 precs[node.label]++; 777 } 778 }, 779 function onExit(node) { 780 if (node.label == rootLabel || precs[rootLabel] > 0) { 781 precs[node.label]--; 782 } 783 }, 784 null); 785 786 if (!opt_label) { 787 // If we have created a flat profile for the whole program, we don't 788 // need an explicit root in it. Thus, replace the counters tree 789 // root with the node corresponding to the whole program. 790 counters.root_ = root; 791 } else { 792 // Propagate weights so percents can be calculated correctly. 793 counters.getRoot().selfWeight = root.selfWeight; 794 counters.getRoot().totalWeight = root.totalWeight; 795 } 796 return counters; 797 } 798 799 getCEntryProfile() { 800 const result = [new CEntryNode("TOTAL", 0)]; 801 let total_ticks = 0; 802 for (let f in this.c_entries_) { 803 const ticks = this.c_entries_[f]; 804 total_ticks += ticks; 805 result.push(new CEntryNode(f, ticks)); 806 } 807 result[0].ticks = total_ticks; // Sorting will keep this at index 0. 808 result.sort((n1, n2) => n2.ticks - n1.ticks || (n2.name < n1.name ? -1 : 1)); 809 return result; 810 } 811 812 813 /** 814 * Cleans up function entries that are not referenced by code entries. 815 */ 816 cleanUpFuncEntries() { 817 const referencedFuncEntries = []; 818 const entries = this.codeMap_.getAllDynamicEntriesWithAddresses(); 819 for (let i = 0, l = entries.length; i < l; ++i) { 820 if (entries[i][1].constructor === FunctionEntry) { 821 entries[i][1].used = false; 822 } 823 } 824 for (let i = 0, l = entries.length; i < l; ++i) { 825 if ("func" in entries[i][1]) { 826 entries[i][1].func.used = true; 827 } 828 } 829 for (let i = 0, l = entries.length; i < l; ++i) { 830 if (entries[i][1].constructor === FunctionEntry && 831 !entries[i][1].used) { 832 this.codeMap_.deleteCode(entries[i][0]); 833 } 834 } 835 } 836} 837 838class CEntryNode { 839 constructor(name, ticks) { 840 this.name = name; 841 this.ticks = ticks; 842 } 843} 844 845 846/** 847 * Creates a dynamic code entry. 848 * 849 * @param {number} size Code size. 850 * @param {string} type Code type. 851 * @param {string} name Function name. 852 * @constructor 853 */ 854class DynamicCodeEntry extends CodeEntry { 855 constructor(size, type, name) { 856 super(size, name, type); 857 } 858 859 getName() { 860 return this.type + ': ' + this.name; 861 } 862 863 /** 864 * Returns raw node name (without type decoration). 865 */ 866 getRawName() { 867 return this.name; 868 } 869 870 isJSFunction() { 871 return false; 872 } 873 874 toString() { 875 return this.getName() + ': ' + this.size.toString(16); 876 } 877} 878 879 880/** 881 * Creates a dynamic code entry. 882 * 883 * @param {number} size Code size. 884 * @param {string} type Code type. 885 * @param {FunctionEntry} func Shared function entry. 886 * @param {Profile.CodeState} state Code optimization state. 887 * @constructor 888 */ 889class DynamicFuncCodeEntry extends CodeEntry { 890 constructor(size, type, func, state) { 891 super(size, '', type); 892 this.func = func; 893 func.addDynamicCode(this); 894 this.state = state; 895 } 896 897 get functionName() { 898 return this.func.functionName; 899 } 900 901 getSourceCode() { 902 return this.source?.getSourceCode(); 903 } 904 905 static STATE_PREFIX = ["", "~", "^", "-", "+", "*"]; 906 getState() { 907 return DynamicFuncCodeEntry.STATE_PREFIX[this.state]; 908 } 909 910 getName() { 911 const name = this.func.getName(); 912 return this.type + ': ' + this.getState() + name; 913 } 914 915 /** 916 * Returns raw node name (without type decoration). 917 */ 918 getRawName() { 919 return this.func.getName(); 920 } 921 922 isJSFunction() { 923 return true; 924 } 925 926 toString() { 927 return this.getName() + ': ' + this.size.toString(16); 928 } 929} 930 931/** 932 * Creates a shared function object entry. 933 * 934 * @param {string} name Function name. 935 * @constructor 936 */ 937class FunctionEntry extends CodeEntry { 938 939 // Contains the list of generated code for this function. 940 _codeEntries = new Set(); 941 942 constructor(name) { 943 super(0, name); 944 const index = name.lastIndexOf(' '); 945 this.functionName = 1 <= index ? name.substring(0, index) : '<anonymous>'; 946 } 947 948 addDynamicCode(code) { 949 if (code.func != this) { 950 throw new Error("Adding dynamic code to wrong function"); 951 } 952 this._codeEntries.add(code); 953 } 954 955 getSourceCode() { 956 // All code entries should map to the same source positions. 957 return this._codeEntries.values().next().value.getSourceCode(); 958 } 959 960 get codeEntries() { 961 return this._codeEntries; 962 } 963 964 /** 965 * Returns node name. 966 */ 967 getName() { 968 let name = this.name; 969 if (name.length == 0) { 970 return '<anonymous>'; 971 } else if (name.charAt(0) == ' ') { 972 // An anonymous function with location: " aaa.js:10". 973 return `<anonymous>${name}`; 974 } 975 return name; 976 } 977} 978 979/** 980 * Constructs a call graph. 981 * 982 * @constructor 983 */ 984class CallTree { 985 root_ = new CallTreeNode(CallTree.ROOT_NODE_LABEL); 986 totalsComputed_ = false; 987 988 /** 989 * The label of the root node. 990 */ 991 static ROOT_NODE_LABEL = ''; 992 993 /** 994 * Returns the tree root. 995 */ 996 getRoot() { 997 return this.root_; 998 } 999 1000 /** 1001 * Adds the specified call path, constructing nodes as necessary. 1002 * 1003 * @param {Array<string>} path Call path. 1004 */ 1005 addPath(path) { 1006 if (path.length == 0) return; 1007 let curr = this.root_; 1008 for (let i = 0; i < path.length; ++i) { 1009 curr = curr.findOrAddChild(path[i]); 1010 } 1011 curr.selfWeight++; 1012 this.totalsComputed_ = false; 1013 } 1014 1015 /** 1016 * Finds an immediate child of the specified parent with the specified 1017 * label, creates a child node if necessary. If a parent node isn't 1018 * specified, uses tree root. 1019 * 1020 * @param {string} label Child node label. 1021 */ 1022 findOrAddChild(label) { 1023 return this.root_.findOrAddChild(label); 1024 } 1025 1026 /** 1027 * Creates a subtree by cloning and merging all subtrees rooted at nodes 1028 * with a given label. E.g. cloning the following call tree on label 'A' 1029 * will give the following result: 1030 * 1031 * <A>--<B> <B> 1032 * / / 1033 * <root> == clone on 'A' ==> <root>--<A> 1034 * \ \ 1035 * <C>--<A>--<D> <D> 1036 * 1037 * And <A>'s selfWeight will be the sum of selfWeights of <A>'s from the 1038 * source call tree. 1039 * 1040 * @param {string} label The label of the new root node. 1041 */ 1042 cloneSubtree(label) { 1043 const subTree = new CallTree(); 1044 this.traverse((node, parent) => { 1045 if (!parent && node.label != label) { 1046 return null; 1047 } 1048 const child = (parent ? parent : subTree).findOrAddChild(node.label); 1049 child.selfWeight += node.selfWeight; 1050 return child; 1051 }); 1052 return subTree; 1053 } 1054 1055 /** 1056 * Computes total weights in the call graph. 1057 */ 1058 computeTotalWeights() { 1059 if (this.totalsComputed_) return; 1060 this.root_.computeTotalWeight(); 1061 this.totalsComputed_ = true; 1062 } 1063 1064 /** 1065 * Traverses the call graph in preorder. This function can be used for 1066 * building optionally modified tree clones. This is the boilerplate code 1067 * for this scenario: 1068 * 1069 * callTree.traverse(function(node, parentClone) { 1070 * var nodeClone = cloneNode(node); 1071 * if (parentClone) 1072 * parentClone.addChild(nodeClone); 1073 * return nodeClone; 1074 * }); 1075 * 1076 * @param {function(CallTreeNode, *)} f Visitor function. 1077 * The second parameter is the result of calling 'f' on the parent node. 1078 */ 1079 traverse(f) { 1080 const pairsToProcess = new ConsArray(); 1081 pairsToProcess.concat([{ node: this.root_, param: null }]); 1082 while (!pairsToProcess.atEnd()) { 1083 const pair = pairsToProcess.next(); 1084 const node = pair.node; 1085 const newParam = f(node, pair.param); 1086 const morePairsToProcess = []; 1087 node.forEachChild((child) => { 1088 morePairsToProcess.push({ node: child, param: newParam }); 1089 }); 1090 pairsToProcess.concat(morePairsToProcess); 1091 } 1092 } 1093 1094 /** 1095 * Performs an indepth call graph traversal. 1096 * 1097 * @param {function(CallTreeNode)} enter A function called 1098 * prior to visiting node's children. 1099 * @param {function(CallTreeNode)} exit A function called 1100 * after visiting node's children. 1101 */ 1102 traverseInDepth(enter, exit) { 1103 function traverse(node) { 1104 enter(node); 1105 node.forEachChild(traverse); 1106 exit(node); 1107 } 1108 traverse(this.root_); 1109 } 1110} 1111 1112 1113/** 1114 * Constructs a call graph node. 1115 * 1116 * @param {string} label Node label. 1117 * @param {CallTreeNode} opt_parent Node parent. 1118 */ 1119class CallTreeNode { 1120 1121 constructor(label, opt_parent) { 1122 // Node self weight (how many times this node was the last node in 1123 // a call path). 1124 this.selfWeight = 0; 1125 // Node total weight (includes weights of all children). 1126 this.totalWeight = 0; 1127 this. children = { __proto__:null }; 1128 this.label = label; 1129 this.parent = opt_parent; 1130 } 1131 1132 1133 /** 1134 * Adds a child node. 1135 * 1136 * @param {string} label Child node label. 1137 */ 1138 addChild(label) { 1139 const child = new CallTreeNode(label, this); 1140 this.children[label] = child; 1141 return child; 1142 } 1143 1144 /** 1145 * Computes node's total weight. 1146 */ 1147 computeTotalWeight() { 1148 let totalWeight = this.selfWeight; 1149 this.forEachChild(function (child) { 1150 totalWeight += child.computeTotalWeight(); 1151 }); 1152 return this.totalWeight = totalWeight; 1153 } 1154 1155 /** 1156 * Returns all node's children as an array. 1157 */ 1158 exportChildren() { 1159 const result = []; 1160 this.forEachChild(function (node) { result.push(node); }); 1161 return result; 1162 } 1163 1164 /** 1165 * Finds an immediate child with the specified label. 1166 * 1167 * @param {string} label Child node label. 1168 */ 1169 findChild(label) { 1170 const found = this.children[label]; 1171 return found === undefined ? null : found; 1172 } 1173 1174 /** 1175 * Finds an immediate child with the specified label, creates a child 1176 * node if necessary. 1177 * 1178 * @param {string} label Child node label. 1179 */ 1180 findOrAddChild(label) { 1181 const found = this.findChild(label) 1182 if (found === null) return this.addChild(label); 1183 return found; 1184 } 1185 1186 /** 1187 * Calls the specified function for every child. 1188 * 1189 * @param {function(CallTreeNode)} f Visitor function. 1190 */ 1191 forEachChild(f) { 1192 for (let c in this.children) { 1193 f(this.children[c]); 1194 } 1195 } 1196 1197 /** 1198 * Walks up from the current node up to the call tree root. 1199 * 1200 * @param {function(CallTreeNode)} f Visitor function. 1201 */ 1202 walkUpToRoot(f) { 1203 for (let curr = this; curr !== null; curr = curr.parent) { 1204 f(curr); 1205 } 1206 } 1207 1208 /** 1209 * Tries to find a node with the specified path. 1210 * 1211 * @param {Array<string>} labels The path. 1212 * @param {function(CallTreeNode)} opt_f Visitor function. 1213 */ 1214 descendToChild(labels, opt_f) { 1215 let curr = this; 1216 for (let pos = 0; pos < labels.length && curr != null; pos++) { 1217 const child = curr.findChild(labels[pos]); 1218 if (opt_f) { 1219 opt_f(child, pos); 1220 } 1221 curr = child; 1222 } 1223 return curr; 1224 } 1225} 1226 1227export function JsonProfile() { 1228 this.codeMap_ = new CodeMap(); 1229 this.codeEntries_ = []; 1230 this.functionEntries_ = []; 1231 this.ticks_ = []; 1232 this.scripts_ = []; 1233} 1234 1235JsonProfile.prototype.addLibrary = function ( 1236 name, startAddr, endAddr) { 1237 const entry = new CodeEntry( 1238 endAddr - startAddr, name, 'SHARED_LIB'); 1239 this.codeMap_.addLibrary(startAddr, entry); 1240 1241 entry.codeId = this.codeEntries_.length; 1242 this.codeEntries_.push({ name: entry.name, type: entry.type }); 1243 return entry; 1244}; 1245 1246JsonProfile.prototype.addStaticCode = function ( 1247 name, startAddr, endAddr) { 1248 const entry = new CodeEntry( 1249 endAddr - startAddr, name, 'CPP'); 1250 this.codeMap_.addStaticCode(startAddr, entry); 1251 1252 entry.codeId = this.codeEntries_.length; 1253 this.codeEntries_.push({ name: entry.name, type: entry.type }); 1254 return entry; 1255}; 1256 1257JsonProfile.prototype.addCode = function ( 1258 kind, name, timestamp, start, size) { 1259 let codeId = this.codeEntries_.length; 1260 // Find out if we have a static code entry for the code. If yes, we will 1261 // make sure it is written to the JSON file just once. 1262 let staticEntry = this.codeMap_.findAddress(start); 1263 if (staticEntry && staticEntry.entry.type === 'CPP') { 1264 codeId = staticEntry.entry.codeId; 1265 } 1266 1267 const entry = new CodeEntry(size, name, 'CODE'); 1268 this.codeMap_.addCode(start, entry); 1269 1270 entry.codeId = codeId; 1271 this.codeEntries_[codeId] = { 1272 name: entry.name, 1273 timestamp: timestamp, 1274 type: entry.type, 1275 kind: kind, 1276 }; 1277 1278 return entry; 1279}; 1280 1281JsonProfile.prototype.addFuncCode = function ( 1282 kind, name, timestamp, start, size, funcAddr, state) { 1283 // As code and functions are in the same address space, 1284 // it is safe to put them in a single code map. 1285 let func = this.codeMap_.findDynamicEntryByStartAddress(funcAddr); 1286 if (!func) { 1287 func = new CodeEntry(0, name, 'SFI'); 1288 this.codeMap_.addCode(funcAddr, func); 1289 1290 func.funcId = this.functionEntries_.length; 1291 this.functionEntries_.push({ name, codes: [] }); 1292 } else if (func.name !== name) { 1293 // Function object has been overwritten with a new one. 1294 func.name = name; 1295 1296 func.funcId = this.functionEntries_.length; 1297 this.functionEntries_.push({ name, codes: [] }); 1298 } 1299 // TODO(jarin): Insert the code object into the SFI's code list. 1300 let entry = this.codeMap_.findDynamicEntryByStartAddress(start); 1301 if (entry) { 1302 if (entry.size === size && entry.func === func) { 1303 // Entry state has changed. 1304 entry.state = state; 1305 } else { 1306 this.codeMap_.deleteCode(start); 1307 entry = null; 1308 } 1309 } 1310 if (!entry) { 1311 entry = new CodeEntry(size, name, 'JS'); 1312 this.codeMap_.addCode(start, entry); 1313 1314 entry.codeId = this.codeEntries_.length; 1315 1316 this.functionEntries_[func.funcId].codes.push(entry.codeId); 1317 1318 kind = Profile.getKindFromState(state); 1319 1320 this.codeEntries_.push({ 1321 name: entry.name, 1322 type: entry.type, 1323 kind: kind, 1324 func: func.funcId, 1325 tm: timestamp, 1326 }); 1327 } 1328 return entry; 1329}; 1330 1331JsonProfile.prototype.moveCode = function (from, to) { 1332 try { 1333 this.codeMap_.moveCode(from, to); 1334 } catch (e) { 1335 printErr(`Move: unknown source ${from}`); 1336 } 1337}; 1338 1339JsonProfile.prototype.addSourcePositions = function ( 1340 start, script, startPos, endPos, sourcePositions, inliningPositions, 1341 inlinedFunctions) { 1342 const entry = this.codeMap_.findDynamicEntryByStartAddress(start); 1343 if (!entry) return; 1344 const codeId = entry.codeId; 1345 1346 // Resolve the inlined functions list. 1347 if (inlinedFunctions.length > 0) { 1348 inlinedFunctions = inlinedFunctions.substring(1).split("S"); 1349 for (let i = 0; i < inlinedFunctions.length; i++) { 1350 const funcAddr = parseInt(inlinedFunctions[i]); 1351 const func = this.codeMap_.findDynamicEntryByStartAddress(funcAddr); 1352 if (!func || func.funcId === undefined) { 1353 printErr(`Could not find function ${inlinedFunctions[i]}`); 1354 inlinedFunctions[i] = null; 1355 } else { 1356 inlinedFunctions[i] = func.funcId; 1357 } 1358 } 1359 } else { 1360 inlinedFunctions = []; 1361 } 1362 1363 this.codeEntries_[entry.codeId].source = { 1364 script: script, 1365 start: startPos, 1366 end: endPos, 1367 positions: sourcePositions, 1368 inlined: inliningPositions, 1369 fns: inlinedFunctions 1370 }; 1371}; 1372 1373JsonProfile.prototype.addScriptSource = function (id, url, source) { 1374 const script = new Script(id); 1375 script.update(url, source); 1376 this.scripts_[id] = script; 1377}; 1378 1379JsonProfile.prototype.deoptCode = function ( 1380 timestamp, code, inliningId, scriptOffset, bailoutType, 1381 sourcePositionText, deoptReasonText) { 1382 let entry = this.codeMap_.findDynamicEntryByStartAddress(code); 1383 if (entry) { 1384 let codeId = entry.codeId; 1385 if (!this.codeEntries_[codeId].deopt) { 1386 // Only add the deopt if there was no deopt before. 1387 // The subsequent deoptimizations should be lazy deopts for 1388 // other on-stack activations. 1389 this.codeEntries_[codeId].deopt = { 1390 tm: timestamp, 1391 inliningId: inliningId, 1392 scriptOffset: scriptOffset, 1393 posText: sourcePositionText, 1394 reason: deoptReasonText, 1395 bailoutType: bailoutType, 1396 }; 1397 } 1398 } 1399}; 1400 1401JsonProfile.prototype.deleteCode = function (start) { 1402 try { 1403 this.codeMap_.deleteCode(start); 1404 } catch (e) { 1405 printErr(`Delete: unknown address ${start}`); 1406 } 1407}; 1408 1409JsonProfile.prototype.moveFunc = function (from, to) { 1410 if (this.codeMap_.findDynamicEntryByStartAddress(from)) { 1411 this.codeMap_.moveCode(from, to); 1412 } 1413}; 1414 1415JsonProfile.prototype.findEntry = function (addr) { 1416 return this.codeMap_.findEntry(addr); 1417}; 1418 1419JsonProfile.prototype.recordTick = function (time_ns, vmState, stack) { 1420 // TODO(jarin) Resolve the frame-less case (when top of stack is 1421 // known code). 1422 const processedStack = []; 1423 for (let i = 0; i < stack.length; i++) { 1424 const resolved = this.codeMap_.findAddress(stack[i]); 1425 if (resolved) { 1426 processedStack.push(resolved.entry.codeId, resolved.offset); 1427 } else { 1428 processedStack.push(-1, stack[i]); 1429 } 1430 } 1431 this.ticks_.push({ tm: time_ns, vm: vmState, s: processedStack }); 1432}; 1433 1434function writeJson(s) { 1435 write(JSON.stringify(s, null, 2)); 1436} 1437 1438JsonProfile.prototype.writeJson = function () { 1439 // Write out the JSON in a partially manual way to avoid creating too-large 1440 // strings in one JSON.stringify call when there are a lot of ticks. 1441 write('{\n') 1442 1443 write(' "code": '); 1444 writeJson(this.codeEntries_); 1445 write(',\n'); 1446 1447 write(' "functions": '); 1448 writeJson(this.functionEntries_); 1449 write(',\n'); 1450 1451 write(' "ticks": [\n'); 1452 for (let i = 0; i < this.ticks_.length; i++) { 1453 write(' '); 1454 writeJson(this.ticks_[i]); 1455 if (i < this.ticks_.length - 1) { 1456 write(',\n'); 1457 } else { 1458 write('\n'); 1459 } 1460 } 1461 write(' ],\n'); 1462 1463 write(' "scripts": '); 1464 writeJson(this.scripts_); 1465 1466 write('}\n'); 1467}; 1468