// Copyright 2023 The Chromium 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 type * as Protocol from '../../generated/protocol.js';
import {describeWithEnvironment} from '../../testing/EnvironmentHelpers.js';
import * as CPUProfile from '../cpu_profile/cpu_profile.js';

function makeCallFrame(functionName: string): Protocol.Runtime.CallFrame {
  return {
    functionName,
    scriptId: 'ScriptId',
    url: '',
    lineNumber: 0,
    columnNumber: 0,
  } as unknown as Protocol.Runtime.CallFrame;
}

function getFrameTreeAsString(cpuProfileDataModel: CPUProfile.CPUProfileDataModel.CPUProfileDataModel): string {
  interface Entry {
    ts: number;
    dur: number;
    name: string;
    selfTime: number;
    id: number;
    depth: number;
  }
  const trackingStack: Entry[] = [];
  const resultStack: Entry[] = [];
  let result = '\n';
  const onFrameOpen =
      (depth: number, node: CPUProfile.ProfileTreeModel.ProfileNode, _sampleIndex: number, ts: number) => {
        trackingStack.push({depth, id: node.id, name: node.callFrame.functionName, ts, selfTime: 0, dur: 0});
      };
  const onFrameClose =
      (_depth: number, node: CPUProfile.ProfileTreeModel.ProfileNode, _sampleIndex: number, _ts: number, dur: number,
       selfTime: number) => {
        const entry = trackingStack.pop();
        if (!entry || entry.id !== node.id) {
          throw new Error('Frame open and Frame close callbacks are not balanced');
        }
        entry.dur = dur;
        entry.selfTime = selfTime;
        resultStack.push(entry);
      };
  cpuProfileDataModel.forEachFrame(onFrameOpen, onFrameClose);
  resultStack.sort((a, b) => b.ts - a.ts);
  while (resultStack.length) {
    const entry = resultStack.pop();
    if (!entry) {
      break;
    }
    const {depth, name, ts, dur, selfTime} = entry;
    result += '  '.repeat(depth) +
        `name: ${name} ts: ${ts} dur: ${Math.round(dur * 100) / 100} selfTime: ${Math.round(selfTime * 100) / 100}`;
    result += resultStack.length ? '\n' : '';
  }
  return result;
}

describe('ProfileTreeModel', function() {
  it('calculates self and total times correctly for a CPU profile', () => {
    // Create the following tree:
    //
    //       root (self = 10)
    //     /                 \
    //    A (self = 0)     D (self = 10)
    //  /             \                 \
    // B (self = 20)  C (self = 10)     E (self = 20)
    const callFrameRoot = makeCallFrame('root');
    const root = new CPUProfile.ProfileTreeModel.ProfileNode(callFrameRoot);
    root.self = 10;

    const callFrameA = makeCallFrame('A');
    const nodeA = new CPUProfile.ProfileTreeModel.ProfileNode(callFrameA);
    root.children.push(nodeA);
    nodeA.self = 0;

    const callFrameB = makeCallFrame('B');
    const nodeB = new CPUProfile.ProfileTreeModel.ProfileNode(callFrameB);
    nodeA.children.push(nodeB);
    nodeB.self = 20;

    const callFrameC = makeCallFrame('C');
    const nodeC = new CPUProfile.ProfileTreeModel.ProfileNode(callFrameC);
    nodeA.children.push(nodeC);
    nodeC.self = 10;

    const callFrameD = makeCallFrame('D');
    const nodeD = new CPUProfile.ProfileTreeModel.ProfileNode(callFrameD);
    root.children.push(nodeD);
    nodeD.self = 10;

    const callFrameE = makeCallFrame('E');
    const nodeE = new CPUProfile.ProfileTreeModel.ProfileNode(callFrameE);
    nodeD.children.push(nodeE);
    nodeE.self = 20;

    const profileTreeModel = new CPUProfile.ProfileTreeModel.ProfileTreeModel();
    profileTreeModel.initialize(root);

    assert.strictEqual(profileTreeModel.total, 70);

    assert.strictEqual(root.total, 70);
    assert.strictEqual(root.self, 10);
    assert.strictEqual(nodeA.total, 30);
    assert.strictEqual(nodeA.self, 0);
    assert.strictEqual(nodeB.total, 20);
    assert.strictEqual(nodeB.self, 20);
    assert.strictEqual(nodeC.total, 10);
    assert.strictEqual(nodeC.self, 10);
    assert.strictEqual(nodeD.total, 30);
    assert.strictEqual(nodeD.self, 10);
    assert.strictEqual(nodeE.total, 20);
    assert.strictEqual(nodeE.self, 20);
  });
  it('calculates depth correctly for the nodes in a profile tree', () => {
    // Create the following tree:
    //
    //       root
    //      /    \
    //     A      D
    //   /   \     \
    //  B     C     E
    //               \
    //                F
    const callFrameRoot = makeCallFrame('root');
    const root = new CPUProfile.ProfileTreeModel.ProfileNode(callFrameRoot);

    const callFrameA = makeCallFrame('A');
    const nodeA = new CPUProfile.ProfileTreeModel.ProfileNode(callFrameA);
    root.children.push(nodeA);

    const callFrameB = makeCallFrame('B');
    const nodeB = new CPUProfile.ProfileTreeModel.ProfileNode(callFrameB);
    nodeA.children.push(nodeB);

    const callFrameC = makeCallFrame('C');
    const nodeC = new CPUProfile.ProfileTreeModel.ProfileNode(callFrameC);
    nodeA.children.push(nodeC);

    const callFrameD = makeCallFrame('D');
    const nodeD = new CPUProfile.ProfileTreeModel.ProfileNode(callFrameD);
    root.children.push(nodeD);

    const callFrameE = makeCallFrame('E');
    const nodeE = new CPUProfile.ProfileTreeModel.ProfileNode(callFrameE);
    nodeD.children.push(nodeE);

    const callFrameF = makeCallFrame('F');
    const nodeF = new CPUProfile.ProfileTreeModel.ProfileNode(callFrameF);
    nodeE.children.push(nodeF);

    const profileTreeModel = new CPUProfile.ProfileTreeModel.ProfileTreeModel();
    profileTreeModel.initialize(root);

    assert.strictEqual(profileTreeModel.maxDepth, 3);
    assert.strictEqual(root.depth, -1);
    assert.strictEqual(nodeA.depth, 0);
    assert.strictEqual(nodeB.depth, 1);
    assert.strictEqual(nodeC.depth, 1);
    assert.strictEqual(nodeD.depth, 0);
    assert.strictEqual(nodeE.depth, 1);
    assert.strictEqual(nodeF.depth, 2);
  });
});

describeWithEnvironment('CPUProfileDataModel', () => {
  const buildBasicProfile = () => {
    const scriptId = 'Peperoni' as Protocol.Runtime.ScriptId;
    const url = '';
    const lineNumber = -1;
    const columnNumber = -1;
    const profile: Protocol.Profiler.Profile = {
      startTime: 1000,
      endTime: 3000,
      nodes: [
        {
          id: 1,
          hitCount: 0,
          callFrame: {functionName: '(root)', scriptId, url, lineNumber, columnNumber},
          children: [2, 3],
        },
        {id: 2, hitCount: 3, callFrame: {functionName: 'a', scriptId, url, lineNumber, columnNumber}, children: [4, 5]},
        {id: 3, hitCount: 3, callFrame: {functionName: 'b', scriptId, url, lineNumber, columnNumber}, children: [6]},
        {id: 4, hitCount: 2, callFrame: {functionName: 'c', scriptId, url, lineNumber, columnNumber}},
        {id: 5, hitCount: 1, callFrame: {functionName: 'd', scriptId, url, lineNumber, columnNumber}},
        {id: 6, hitCount: 2, callFrame: {functionName: 'e', scriptId, url, lineNumber, columnNumber}, children: [7]},
        {id: 7, hitCount: 2, callFrame: {functionName: 'f', scriptId, url, lineNumber, columnNumber}},
      ],
      samples: [2, 2, 4, 5, 4, 2, 3, 6, 6, 7, 7, 3, 3],
      timeDeltas: new Array(13).fill(100),
    };
    return profile;
  };
  it('builds a tree from a CPU profile', () => {
    const profile = buildBasicProfile();
    // Profile contains this tree:
    //
    //       1
    //     /   \
    //    2     3
    //  /   \     \
    // 4     5     6
    //              \
    //               7
    const cpuProfileDataModel = new CPUProfile.CPUProfileDataModel.CPUProfileDataModel(profile);
    assert.deepEqual(cpuProfileDataModel.root.children.map(n => n.id), [3, 2]);

    const node2 = cpuProfileDataModel.root.children[1];
    assert.strictEqual(node2.id, 2);
    assert.deepEqual(node2.children.map(n => n.id), [5, 4]);

    const node3 = cpuProfileDataModel.root.children[0];
    assert.strictEqual(node3.id, 3);
    assert.deepEqual(node3.children.map(n => n.id), [6]);

    const node6 = node3.children[0];
    assert.strictEqual(node6.id, 6);
    assert.deepEqual(node6.children.map(n => n.id), [7]);
  });
  it('parses JS call frames from a CPU profile', () => {
    // Calls in the profile look roughly like
    //
    // |---------------a--------------||---------b---------|
    //        |---c---||--d--||---c---|  |-------e------|
    //                                      |-----f-----|
    const profile = buildBasicProfile();
    const cpuProfileDataModel = new CPUProfile.CPUProfileDataModel.CPUProfileDataModel(profile);
    const treeAsString = getFrameTreeAsString(cpuProfileDataModel);

    assert.strictEqual(treeAsString, `
name: a ts: 1.1 dur: 0.6 selfTime: 0.3
  name: c ts: 1.3 dur: 0.1 selfTime: 0.1
  name: d ts: 1.4 dur: 0.1 selfTime: 0.1
  name: c ts: 1.5 dur: 0.1 selfTime: 0.1
name: b ts: 1.7 dur: 0.7 selfTime: 0.3
  name: e ts: 1.8 dur: 0.4 selfTime: 0.2
    name: f ts: 2 dur: 0.2 selfTime: 0.2`);
  });

  it('parses a CPU profile without hitcounts', () => {
    const profile = buildBasicProfile();
    for (const node of profile.nodes) {
      node.hitCount = undefined;
    }
    const cpuProfileDataModel = new CPUProfile.CPUProfileDataModel.CPUProfileDataModel(profile);
    const treeAsString = getFrameTreeAsString(cpuProfileDataModel);
    assert.strictEqual(treeAsString, `
name: a ts: 1.1 dur: 0.6 selfTime: 0.3
  name: c ts: 1.3 dur: 0.1 selfTime: 0.1
  name: d ts: 1.4 dur: 0.1 selfTime: 0.1
  name: c ts: 1.5 dur: 0.1 selfTime: 0.1
name: b ts: 1.7 dur: 0.7 selfTime: 0.3
  name: e ts: 1.8 dur: 0.4 selfTime: 0.2
    name: f ts: 2 dur: 0.2 selfTime: 0.2`);
  });
  it('fixes missing samples by replacing them with neighboring stacks', () => {
    const scriptId = 'Peperoni' as Protocol.Runtime.ScriptId;
    const url = '';
    const lineNumber = -1;
    const columnNumber = -1;

    // The calls in the profile look roughly like:
    //
    // |program||-bar-||program||-bar-||program||-bar-||GC||program||-bar-||program||-bar-||-baz-||program||-bar-|
    // |program|       |program|       |program||-foo-|    |program||-foo-||program||-foo-|       |program||-foo-|
    //
    // Which, after accounting for fixable program calls (missing samples), should look as (program samples are
    // replaced with the preceding samples if the bottom frame of both neighboring samples is the same):
    //
    // |program||----------bar------||program||-------bar---------||-baz-||program||-bar-|
    // |program|       |-----foo----||program||-------foo---------|       |program||-foo-|
    //                   |-GC-|
    const profile: Protocol.Profiler.Profile = {
      startTime: 1000,
      endTime: 4000,
      nodes: [
        {
          id: 1,
          hitCount: 0,
          callFrame: {functionName: '(root)', scriptId, url, lineNumber, columnNumber},
          children: [2, 3, 4, 5],
        },
        {
          id: 2,
          hitCount: 1000,
          callFrame: {functionName: '(garbage collector)', scriptId, url, lineNumber, columnNumber},
        },
        {id: 3, hitCount: 1000, callFrame: {functionName: '(program)', scriptId, url, lineNumber, columnNumber}},
        {
          id: 4,
          hitCount: 1000,
          callFrame: {functionName: 'bar', scriptId, url, lineNumber, columnNumber},
          children: [6],
        },
        {id: 5, hitCount: 1000, callFrame: {functionName: 'baz', scriptId, url, lineNumber, columnNumber}},
        {id: 6, hitCount: 1000, callFrame: {functionName: 'foo', scriptId, url, lineNumber, columnNumber}},
      ],
      samples: [3, 4, 3, 4, 3, 6, 2, 2, 3, 6, 6, 3, 6, 5, 3, 6],
    };
    profile.timeDeltas = profile.samples?.map(_ => 1000);
    profile.endTime = profile.startTime + (profile.timeDeltas?.length || 0) * 1000;
    const cpuProfileDataModel = new CPUProfile.CPUProfileDataModel.CPUProfileDataModel(profile);
    const treeAsString = getFrameTreeAsString(cpuProfileDataModel);
    assert.strictEqual(treeAsString, `
name: (program) ts: 2 dur: 1 selfTime: 1
name: bar ts: 3 dur: 7 selfTime: 4
  name: foo ts: 7 dur: 3 selfTime: 1
    name: (garbage collector) ts: 8 dur: 2 selfTime: 2
name: (program) ts: 10 dur: 1 selfTime: 1
name: bar ts: 11 dur: 4 selfTime: 0
  name: foo ts: 11 dur: 4 selfTime: 4
name: baz ts: 15 dur: 1 selfTime: 1
name: (program) ts: 16 dur: 1 selfTime: 1
name: bar ts: 17 dur: 1 selfTime: 0
  name: foo ts: 17 dur: 1 selfTime: 1`);
  });
  it('parses a CPU with GC nodes correctly', () => {
    const scriptId = 'Peperoni' as Protocol.Runtime.ScriptId;
    const url = '';
    const lineNumber = -1;
    const columnNumber = -1;

    // Profile contains this tree:
    //
    //       root
    //     /      \
    //   GC       foo
    //               \
    //                bar

    // The calls in the profile look roughly like:
    //
    // |-------------------foo----------------||--GC--|
    // |---------bar--------|         |--bar--|
    //
    // Which, after accounting for the GC call, should be fixed as:
    // |-----------------------foo------------|
    // |---bar---||---bar---|         |--bar--|
    //                                 |--GC--|
    const profile: Protocol.Profiler.Profile = {
      startTime: 1000,
      endTime: 4000,
      nodes: [
        {
          id: 1,
          hitCount: 0,
          callFrame: {functionName: '(root)', scriptId, url, lineNumber, columnNumber},
          children: [2, 3],
        },
        {
          id: 2,
          hitCount: 1000,
          callFrame: {functionName: '(garbage collector)', scriptId, url, lineNumber, columnNumber},
        },
        {
          id: 3,
          hitCount: 1000,
          callFrame: {functionName: 'foo', scriptId, url, lineNumber, columnNumber},
          children: [4],
        },
        {id: 4, hitCount: 1000, callFrame: {functionName: 'bar', scriptId, url, lineNumber, columnNumber}},
      ],
      timeDeltas: [500, 250, 1000, 250, 1000],
      samples: [4, 4, 3, 4, 2],
    };
    const cpuProfileDataModel = new CPUProfile.CPUProfileDataModel.CPUProfileDataModel(profile);
    const treeAsString = getFrameTreeAsString(cpuProfileDataModel);
    assert.strictEqual(treeAsString, `
name: foo ts: 1.5 dur: 3.13 selfTime: 0.25
  name: bar ts: 1.5 dur: 1.25 selfTime: 1.25
  name: bar ts: 3 dur: 1.63 selfTime: 1
    name: (garbage collector) ts: 4 dur: 0.63 selfTime: 0.63`);
  });
});
