current commit
221011.2 · deps: update V8 to 10.7.193.13
// Copyright 2022 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import {TickLogEntry} from './log/tick.mjs';

const kForward = 1;
const kBackwards = -1;

/**
 * The StackSorter sorts ticks by recursively grouping the most frequent frames
 * at each stack-depth to the beginning. This is used to generate flame graphs.
 *
 * Example
 *  tick1 = [a1, b1]
 *  tick2 = [a0, b0]
 *  tick3 = [a1, b1, c1]
 *  tick4 = [a1, b0, c0, d0]
 *
 * If we sort this recursively from the beginning we get:
 *  tick1 = [a1, b1]
 *  tick3 = [a1, b1, c1]
 *  tick4 = [a1, b0, c0, d0]
 *  tick2 = [a0, b0]
 *
 * The idea is to group common stacks together to generate easily readable
 * graphs where we can quickly discover which function is the highest incoming
 * or outgoing contributor.
 */
export class StackSorter {
  static fromTop(array, maxDepth) {
    return new this(array, maxDepth, kForward).sorted();
  }

  static fromBottom(array, maxDepth) {
    return new this(array, maxDepth, kBackwards).sorted();
  }

  constructor(array, maxDepth, direction) {
    this.stacks = array;
    this.maxDepth = maxDepth;
    if (direction !== kForward && direction !== kBackwards) {
      throw new Error('Invalid direction');
    }
    this.direction = direction;
  }

  sorted() {
    const startLevel = this.direction == kForward ? 0 : this.maxDepth - 1;
    this._sort(0, this.stacks.length, startLevel);
    return this.stacks;
  }

  _sort(start, end, stackIndex) {
    if (stackIndex >= this.maxDepth || stackIndex < 0) return;
    const length = end - start;
    if (length <= 1) return;
    let counts;

    {
      const kNoFrame = -1;
      let bag = new Map();

      for (let i = start; i < end; i++) {
        let code = this.stacks[i][stackIndex] ?? kNoFrame;
        const count = bag.get(code) ?? 0;
        bag.set(code, count + 1);
      }

      // If all the frames are the same at the current stackIndex, check the
      // next stackIndex.
      if (bag.size === 1) {
        return this._sort(start, end, stackIndex + this.direction);
      }

      counts = Array.from(bag)
      counts.sort((a, b) => b[1] - a[1]);

      let currentIndex = start;
      // Reuse bag since we've copied out the counts;
      let insertionIndices = bag;
      for (let i = 0; i < counts.length; i++) {
        const pair = counts[i];
        const code = pair[0];
        const count = pair[1];
        insertionIndices.set(code, currentIndex);
        currentIndex += count;
      }
      // TODO do copy-less
      let stacksSegment = this.stacks.slice(start, end);
      for (let i = 0; i < length; i++) {
        const stack = stacksSegment[i];
        const entry = stack[stackIndex] ?? kNoFrame;
        const insertionIndex = insertionIndices.get(entry);
        if (!Number.isFinite(insertionIndex)) {
          throw 'Invalid insertionIndex: ' + insertionIndex;
        }
        if (insertionIndex < start || insertionIndex >= end) {
          throw 'Invalid insertionIndex: ' + insertionIndex;
        }
        this.stacks[insertionIndex] = stack;
        insertionIndices.set(entry, insertionIndex + 1);
      }
      // Free up variables before recursing.
      insertionIndices = bag = stacksSegment = undefined;
    }

    // Sort sub-segments
    let segmentStart = start;
    let segmentEnd = start;
    for (let i = 0; i < counts.length; i++) {
      const segmentLength = counts[i][1];
      segmentEnd = segmentStart + segmentLength - 1;
      this._sort(segmentStart, segmentEnd, stackIndex + this.direction);
      segmentStart = segmentEnd;
    }
  }
}

export class ProfileNode {
  constructor(codeEntry) {
    this.codeEntry = codeEntry;
    this.inCodeEntries = [];
    // [tick0, framePos0, tick1, framePos1, ...]
    this.ticksAndPosition = [];
    this.outCodeEntries = [];
    this._selfDuration = 0;
    this._totalDuration = 0;
  }

  stacksOut() {
    const slicedStacks = [];
    let maxDepth = 0;
    for (let i = 0; i < this.ticksAndPosition.length; i += 2) {
      // tick.stack = [topN, ..., top0, this.codeEntry, bottom0, ..., bottomN];
      const tick = this.ticksAndPosition[i];
      const stackIndex = this.ticksAndPosition[i + 1];
      // slice = [topN, ..., top0]
      const slice = tick.stack.slice(0, stackIndex);
      maxDepth = Math.max(maxDepth, slice.length);
      slicedStacks.push(slice);
    }
    // Before:
    //  stack1 = [f4, f3, f2, f1]
    //  stack2 = [f2, f1]
    // After:
    //  stack1 = [f4, f3, f2, f1]
    //  stack2 = [  ,   , f2, f1]
    for (let i = 0; i < slicedStacks.length; i++) {
      const stack = slicedStacks[i];
      const length = stack.length;
      if (length < maxDepth) {
        // Pad stacks at the beginning
        stack.splice(maxDepth - length, 0, undefined);
      }
    }
    // Start sorting at the bottom-most frame: top0 => topN / f1 => fN
    return StackSorter.fromBottom(slicedStacks, maxDepth);
  }

  stacksIn() {
    const slicedStacks = [];
    let maxDepth = 0;
    for (let i = 0; i < this.ticksAndPosition.length; i += 2) {
      // tick.stack = [topN, ..., top0, this.codeEntry, bottom0..., bottomN];
      const tick = this.ticksAndPosition[i];
      const stackIndex = this.ticksAndPosition[i + 1];
      // slice = [bottom0, ..., bottomN]
      const slice = tick.stack.slice(stackIndex + 1);
      maxDepth = Math.max(maxDepth, slice.length);
      slicedStacks.push(slice);
    }
    // Start storting at the top-most frame: bottom0 => bottomN
    return StackSorter.fromTop(slicedStacks, maxDepth);
  }

  startTime() {
    return this.ticksAndPosition[0].startTime;
  }

  endTime() {
    return this.ticksAndPosition[this.ticksAndPosition.length - 2].endTime;
  }

  duration() {
    return this.endTime() - this.startTime();
  }

  selfCount() {
    return this.totalCount() - this.outCodeEntries.length;
  }

  totalCount() {
    return this.ticksAndPosition.length / 2;
  }

  isLeaf() {
    return this.selfCount() == this.totalCount();
  }

  totalDuration() {
    let duration = 0;
    for (let entry of this.ticksAndPosition) duration += entry.duration;
    return duration;
  }

  selfDuration() {
    let duration = this.totalDuration();
    for (let entry of this.outCodeEntries) duration -= entry.duration;
    return duration;
  }
}

export class Flame {
  constructor(time, logEntry, depth, duration = -1) {
    this._time = time;
    this._logEntry = logEntry;
    this.depth = depth;
    this._duration = duration;
    this.parent = undefined;
    this.children = [];
  }

  static add(time, logEntry, stack, flames) {
    const depth = stack.length;
    const newFlame = new Flame(time, logEntry, depth);
    if (depth > 0) {
      const parent = stack[depth - 1];
      newFlame.parent = parent;
      parent.children.push(newFlame);
    }
    flames.push(newFlame);
    stack.push(newFlame);
  }

  stop(time) {
    if (this._duration !== -1) throw new Error('Already stopped');
    this._duration = time - this._time
  }

  get time() {
    return this._time;
  }

  get logEntry() {
    return this._logEntry;
  }

  get startTime() {
    return this._time;
  }

  get endTime() {
    return this._time + this._duration;
  }

  get duration() {
    return this._duration;
  }

  get type() {
    return TickLogEntry.extractCodeEntryType(this._logEntry?.entry);
  }

  get name() {
    return this._logEntry?.name;
  }
}

export class FlameBuilder {
  static forTime(ticks, reverseDepth) {
    return new this(ticks, true, reverseDepth);
  }

  static forTicks(ticks, reverseDepth = false) {
    return new this(ticks, false, reverseDepth);
  }

  constructor(ticks, useTime, reverseDepth) {
    this.maxDepth = 0;
    let tempFlames = this.flames = [];
    this.reverseDepth = reverseDepth;

    if (ticks.length == 0) return;

    if (!(ticks[0] instanceof TickLogEntry) && !Array.isArray(ticks[0])) {
      throw new Error(
          'Expected ticks array: `[TickLogEntry, ..]`, or raw stacks: `[[CodeEntry, ...], [...]]`');
    }
    // flameStack = [bottom, ..., top];
    const flameStack = [];
    let maxDepth = 0;
    const ticksLength = ticks.length;
    for (let tickIndex = 0; tickIndex < ticksLength; tickIndex++) {
      const tick = ticks[tickIndex];
      // tick is either a Tick log entry, or an Array
      const tickStack = tick.stack ?? tick;
      const tickStackLength = tickStack.length;
      const timeValue = useTime ? tick.time : tickIndex;
      maxDepth = Math.max(maxDepth, tickStackLength);
      // tick.stack = [top, .... , bottom];
      for (let stackIndex = tickStackLength - 1; stackIndex >= 0;
           stackIndex--) {
        const codeEntry = tickStack[stackIndex];
        // Assume that all higher stack entries are undefined as well.
        if (codeEntry === undefined) break;
        // codeEntry is either a CodeEntry or a raw pc.
        const logEntry = codeEntry?.logEntry ?? codeEntry;
        const flameStackIndex = tickStackLength - stackIndex - 1;
        if (flameStackIndex < flameStack.length) {
          if (flameStack[flameStackIndex].logEntry === logEntry) continue;
          // A lower frame changed, close all higher Flames.
          for (let k = flameStackIndex; k < flameStack.length; k++) {
            flameStack[k].stop(timeValue);
          }
          flameStack.length = flameStackIndex;
        }
        Flame.add(timeValue, logEntry, flameStack, tempFlames);
      }
      // Stop all Flames that are deeper nested than the current stack.
      if (tickStackLength < flameStack.length) {
        for (let k = tickStackLength; k < flameStack.length; k++) {
          flameStack[k].stop(timeValue);
        }
        flameStack.length = tickStackLength;
      }
    }
    this.maxDepth = maxDepth;

    const lastTime = useTime ? ticks[ticksLength - 1].time : ticksLength - 1;
    for (let k = 0; k < flameStack.length; k++) {
      flameStack[k].stop(lastTime);
    }
  }
}