• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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