// Copyright 2022 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 * as Utils from '../../../panels/timeline/utils/utils.js';
import {describeWithEnvironment} from '../../../testing/EnvironmentHelpers.js';
import {
  getAllNodes,
  getEventsIn,
  getRootAt,
  makeBeginEvent,
  makeCompleteEvent,
  makeEndEvent,
  makeInstantEvent,
  prettyPrint,
} from '../../../testing/TraceHelpers.js';
import {TraceLoader} from '../../../testing/TraceLoader.js';
import * as Trace from '../trace.js';

const MAIN_FRAME_PID = 2154214;
const SUB_FRAME_PID = 2236065;
const SUB_FRAME_PID_2 = 2236084;
const SUB_FRAME_PID_3 = 2236123;

async function handleEventsFromTraceFile(
    context: Mocha.Suite|Mocha.Context|null, file: string): Promise<Trace.Handlers.Types.ParsedTrace> {
  const {parsedTrace} = await TraceLoader.traceEngine(context, file);
  return parsedTrace;
}

describeWithEnvironment('RendererHandler', function() {
  it('finds all the renderers in a real world profile', async () => {
    const {Renderer: renderers} = await handleEventsFromTraceFile(this, 'multiple-navigations-with-iframes.json.gz');
    assert.strictEqual(renderers.processes.size, 4);

    const pids = [...renderers.processes].map(([pid]) => pid);
    assert.deepEqual(
        pids,
        [
          MAIN_FRAME_PID,   // Main frame process: localhost:5000
          SUB_FRAME_PID,    // Sub frame process (trace start): example.com
          SUB_FRAME_PID_2,  // Sub frame process (after first navigation): example.com
          SUB_FRAME_PID_3,  // Sub frame process (after second navigation): example.com
        ],
        'Process IDs do not match expectations');

    const origins = [...renderers.processes].map(([, process]) => {
      return process.url ? new URL(process.url).origin : null;
    });
    assert.deepEqual(
        origins,
        [
          'http://localhost:5000',    // Main frame process: localhost:5000
          'https://www.example.com',  // Sub frame process (trace start): example.com
          'https://www.example.com',  // Sub frame process (after first navigation): example.com
          'https://www.example.com',  // Sub frame process (after second navigation): example.com
        ],
        'Process origins do not meet expectations');

    // Assert on whether it has correctly detected a given process to be on the
    // main frame or in a subframe.
    const isOnMainFrame = [...renderers.processes].map(([, process]) => process.isOnMainFrame);
    assert.deepEqual(
        isOnMainFrame,
        [
          true,   // Main frame process: localhost:5000
          false,  // Sub frame process (trace start): example.com
          false,  // Sub frame process (after first navigation): example.com
          false,  // Sub frame process (after second navigation): example.com
        ],
        'Processes are incorrectly assigned as being on the main frame');
  });

  it('finds all the main frame threads in a real world profile', async () => {
    const {Renderer: renderers} = await handleEventsFromTraceFile(this, 'multiple-navigations-with-iframes.json.gz');
    const frame = renderers.processes.get(Trace.Types.Events.ProcessID(MAIN_FRAME_PID)) as
        Trace.Handlers.ModelHandlers.Renderer.RendererProcess;
    const names = [...frame.threads].map(([, thread]) => thread.name).sort();
    assert.deepEqual(
        names,
        [
          'Chrome_ChildIOThread',
          'Compositor',
          'CompositorTileWorker1',
          'CompositorTileWorker2',
          'CompositorTileWorker3',
          'CompositorTileWorker4',
          'CrRendererMain',
          'ThreadPoolForegroundWorker',
          'ThreadPoolForegroundWorker',
          'ThreadPoolForegroundWorker',
          'ThreadPoolForegroundWorker',
          'ThreadPoolForegroundWorker',
          'ThreadPoolForegroundWorker',
          'ThreadPoolForegroundWorker',
          'ThreadPoolForegroundWorker',
          'ThreadPoolServiceThread',
        ],
        'Main frame thread names do not meet expectations before navigation');
  });

  it('finds all the sub frame threads in a real world profile', async () => {
    const {Renderer: renderers} = await handleEventsFromTraceFile(this, 'multiple-navigations-with-iframes.json.gz');
    const frame = renderers.processes.get(Trace.Types.Events.ProcessID(SUB_FRAME_PID)) as
        Trace.Handlers.ModelHandlers.Renderer.RendererProcess;
    const names = [...frame.threads].map(([, thread]) => thread.name).sort();
    assert.deepEqual(
        names,
        [
          'Chrome_ChildIOThread',
          'Compositor',
          'CrRendererMain',
          'ThreadPoolServiceThread',
        ],
        'Main frame thread names do not meet expectations after navigation');
  });

  it('finds all the roots on the main frame\'s main thread in a real world profile', async () => {
    const {Renderer: renderers} = await handleEventsFromTraceFile(this, 'multiple-navigations-with-iframes.json.gz');
    const frame = renderers.processes.get(Trace.Types.Events.ProcessID(MAIN_FRAME_PID)) as
        Trace.Handlers.ModelHandlers.Renderer.RendererProcess;
    const thread = [...frame.threads.values()].find(thread => thread.name === 'CrRendererMain');
    if (!thread) {
      assert(false, 'Main thread was not found');
    }

    const tree = thread.tree;
    if (!tree) {
      assert(false, 'Main thread has no tree of events');
    }
    assert.deepEqual([...tree.roots].map(root => root.id), [
      0,    1,    2,    3,    4,    5,    16,   18,   29,   38,   49,   58,   77,   183,  184,  185,  186,  188,  189,
      190,  199,  200,  201,  202,  211,  212,  213,  214,  229,  230,  232,  237,  239,  240,  242,  251,  252,  261,
      264,  265,  266,  267,  268,  279,  282,  284,  285,  286,  287,  288,  289,  290,  293,  294,  295,  296,  297,
      298,  299,  300,  301,  302,  303,  304,  305,  306,  328,  329,  330,  331,  332,  333,  334,  335,  336,  337,
      338,  339,  340,  341,  342,  343,  344,  345,  354,  355,  356,  359,  389,  408,  409,  410,  411,  412,  413,
      414,  415,  416,  417,  418,  419,  420,  421,  422,  423,  424,  425,  426,  427,  428,  429,  430,  431,  432,
      433,  441,  442,  443,  444,  445,  446,  447,  448,  455,  456,  457,  458,  459,  460,  461,  462,  463,  464,
      465,  466,  467,  468,  469,  479,  480,  481,  482,  483,  484,  485,  492,  493,  494,  495,  496,  498,  506,
      507,  508,  509,  510,  511,  516,  517,  518,  519,  520,  521,  522,  523,  524,  525,  526,  538,  540,  541,
      552,  555,  556,  565,  566,  575,  576,  585,  586,  595,  596,  605,  606,  615,  616,  625,  626,  635,  636,
      645,  646,  657,  660,  661,  662,  663,  674,  677,  678,  679,  680,  689,  690,  691,  692,  701,  702,  711,
      712,  721,  722,  733,  734,  737,  738,  739,  740,  749,  750,  751,  760,  761,  762,  771,  772,  773,  782,
      783,  784,  793,  794,  795,  796,  797,  808,  809,  810,  811,  835,  843,  844,  845,  846,  848,  861,  869,
      870,  871,  872,  873,  874,  875,  876,  877,  878,  881,  882,  883,  884,  885,  886,  887,  888,  889,  890,
      904,  905,  906,  907,  908,  909,  910,  911,  912,  913,  914,  915,  916,  917,  918,  919,  920,  921,  922,
      931,  932,  933,  936,  966,  967,  983,  984,  985,  986,  987,  988,  989,  990,  991,  992,  993,  994,  995,
      996,  997,  998,  999,  1000, 1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008, 1009, 1010, 1011, 1012, 1020, 1021,
      1022, 1023, 1024, 1031, 1032, 1033, 1034, 1035, 1036, 1037, 1038, 1048, 1049, 1050, 1051, 1052, 1053, 1054, 1055,
      1056, 1057, 1064, 1065, 1066, 1068, 1069, 1077, 1078, 1079, 1080, 1081, 1082, 1083, 1084, 1085, 1086, 1087, 1088,
      1089, 1090, 1091, 1092, 1102, 1104, 1105, 1106, 1107, 1116, 1117, 1118, 1127, 1128, 1129, 1138, 1139, 1140, 1141,
      1150, 1151, 1152, 1153, 1154, 1165, 1166, 1167, 1176, 1177, 1178, 1189, 1192, 1193, 1194, 1203, 1204, 1205, 1206,
      1215, 1216, 1225, 1226, 1235, 1236, 1237, 1246, 1247, 1256, 1257, 1266, 1267, 1276, 1277, 1286, 1287, 1298, 1301,
      1302, 1303, 1304, 1313, 1314, 1315, 1324, 1325, 1326, 1335, 1336, 1337, 1348, 1351, 1352, 1353, 1362, 1364, 1365,
      1366, 1367, 1368, 1369, 1370, 1371, 1378,
    ]);
  });

  it('finds all the roots on the sub frame\'s main thread in a real world profile', async () => {
    const {Renderer: renderers} = await handleEventsFromTraceFile(this, 'multiple-navigations-with-iframes.json.gz');
    const frame = renderers.processes.get(Trace.Types.Events.ProcessID(SUB_FRAME_PID)) as
        Trace.Handlers.ModelHandlers.Renderer.RendererProcess;
    const thread = [...frame.threads.values()].find(thread => thread.name === 'CrRendererMain');
    if (!thread) {
      assert(false, 'Main thread was not found');
    }

    const tree = thread.tree;
    if (!tree) {
      assert(false, 'Main thread has no tree of events');
    }
    assert.deepEqual(
        [...tree.roots].map(root => root.id), [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 14, 15, 16, 17, 18, 19, 20]);
  });

  it('builds a hierarchy for the main frame\'s main thread in a real world profile', async () => {
    const {Renderer: renderers} = await handleEventsFromTraceFile(this, 'multiple-navigations-with-iframes.json.gz');
    const frame = renderers.processes.get(Trace.Types.Events.ProcessID(MAIN_FRAME_PID)) as
        Trace.Handlers.ModelHandlers.Renderer.RendererProcess;
    const thread = [...frame.threads.values()].find(thread => thread.name === 'CrRendererMain');
    if (!thread) {
      assert(false, 'Main thread was not found');
    }

    const tree = thread.tree;
    if (!tree) {
      assert(false, 'Main thread has no tree of events');
    }

    const isRoot = (node: Trace.Helpers.TreeHelpers.TraceEntryNode) => node.depth === 0;
    const isInstant = (event: Trace.Types.Events.Event) => Trace.Types.Events.isInstant(event);
    const isLong = (event: Trace.Types.Events.Event) => Trace.Types.Events.isComplete(event) && event.dur > 1000;
    const isIncluded = (node: Trace.Helpers.TreeHelpers.TraceEntryNode, event: Trace.Types.Events.Event) =>
        (!isRoot(node) || isInstant(event) || isLong(event)) &&
        Boolean(Utils.EntryStyles.getEventStyle(event.name as Trace.Types.Events.Name));
    assert.strictEqual(prettyPrint(tree, isIncluded), `
............
-RunTask [2.21ms]
.
  -MajorGC [2.148ms]
...........................................................
-RunTask [15.436ms]
  -FrameStartedLoading [0ms]
  -EventDispatch (pagehide) [0.018ms]
  -EventDispatch (visibilitychange) [0.01ms]
  -EventDispatch (webkitvisibilitychange) [0.006ms]
.
  -EventDispatch (unload) [0.006ms]
.
  -ResourceSendRequest [0ms]
  -ResourceReceiveResponse [0ms]
...
  -ProfileCall (anonymous) [0.205ms]
    -ProfileCall (anonymous) [0.205ms]
.......................
-RunTask [3.402ms]
  -ParseHTML [2.593ms]
....
    -ParseHTML [0.064ms]
...
    -EventDispatch (readystatechange) [0.008ms]
.
    -EventDispatch (DOMContentLoaded) [0.004ms]
.
    -MarkDOMContent [0ms]
.
    -EventDispatch (readystatechange) [0.01ms]
    -EventDispatch (beforeunload) [0.013ms]
    -FrameStartedLoading [0ms]
.
  -ParseHTML [0.01ms]
..
  -EventDispatch (readystatechange) [0.008ms]
.
  -EventDispatch (DOMContentLoaded) [0.035ms]
.
  -UpdateLayoutTree [0.373ms]
    -InvalidateLayout [0ms]
  -MarkDOMContent [0ms]
-RunTask [2.675ms]
  -BeginMainThreadFrame [0ms]
  -Layout [0.854ms]
    -InvalidateLayout [0ms]
    -Layout [0.302ms]
      -UpdateLayoutTree [0.149ms]
.
  -UpdateLayerTree [0.338ms]
  -Paint [0.203ms]
..
  -firstPaint [0ms]
  -firstContentfulPaint [0ms]
.....
  -largestContentfulPaint::Candidate [0ms]
.................................
-RunTask [1.605ms]
  -EventDispatch (pagehide) [0.014ms]
  -EventDispatch (visibilitychange) [0.038ms]
  -EventDispatch (webkitvisibilitychange) [0.009ms]
  -EventDispatch (unload) [0.004ms]
.
  -ScheduleStyleRecalculation [0ms]
..............
-RunTask [1.231ms]
  -BeginMainThreadFrame [0ms]
  -UpdateLayoutTree [0.093ms]
.
  -UpdateLayerTree [0.186ms]
  -Paint [0.063ms]
  -Paint [0.084ms]
  -UpdateLayer [0.022ms]
  -UpdateLayer [0.006ms]
  -CompositeLayers [0.311ms]
............
-RunTask [1.663ms]
.
  -EventDispatch (readystatechange) [0.009ms]
.
  -EventDispatch (load) [0.014ms]
.
  -MarkLoad [0ms]
  -EventDispatch (pageshow) [0.007ms]
.......................................................................................
-RunTask [1.42ms]
.
  -UpdateLayerTree [0.023ms]
  -HitTest [0.057ms]
  -EventDispatch (mousemove) [0.018ms]
.
  -UpdateLayerTree [0.028ms]
  -HitTest [0.022ms]
.
  -UpdateLayerTree [0.01ms]
  -HitTest [0.002ms]
  -ScheduleStyleRecalculation [0ms]
  -EventDispatch (mousedown) [0.018ms]
  -UpdateLayoutTree [0.146ms]
.
  -UpdateLayerTree [0.031ms]
  -HitTest [0.016ms]
  -ScheduleStyleRecalculation [0ms]
  -UpdateLayoutTree [0.031ms]
  -EventDispatch (focus) [0.014ms]
  -EventDispatch (focusin) [0.005ms]
  -EventDispatch (DOMFocusIn) [0.005ms]
.
  -UpdateLayerTree [0.029ms]
.....
-RunTask [1.034ms]
.
  -UpdateLayerTree [0.021ms]
  -HitTest [0.038ms]
  -ScheduleStyleRecalculation [0ms]
  -EventDispatch (mouseup) [0.016ms]
  -EventDispatch (click) [0.44ms]
    -EventDispatch (beforeunload) [0.009ms]
    -FrameStartedLoading [0ms]
.
  -UpdateLayoutTree [0.137ms]
.
  -UpdateLayerTree [0.03ms]
....................
-RunTask [8.203ms]
  -EventDispatch (pagehide) [0.016ms]
  -EventDispatch (visibilitychange) [0.006ms]
  -EventDispatch (webkitvisibilitychange) [0.004ms]
  -EventDispatch (unload) [0.008ms]
..
  -ResourceSendRequest [0ms]
  -ResourceSendRequest [0ms]
  -ResourceReceiveResponse [0ms]
..........................
-RunTask [2.996ms]
  -ParseHTML [2.368ms]
....
    -ParseHTML [0.074ms]
...
    -EventDispatch (readystatechange) [0.01ms]
.
    -EventDispatch (DOMContentLoaded) [0.005ms]
.
    -MarkDOMContent [0ms]
.
    -EventDispatch (readystatechange) [0.008ms]
    -EventDispatch (beforeunload) [0.009ms]
    -FrameStartedLoading [0ms]
.
  -ParseHTML [0.009ms]
..
  -EventDispatch (readystatechange) [0.007ms]
.
  -EventDispatch (DOMContentLoaded) [0.005ms]
.
  -UpdateLayoutTree [0.301ms]
    -InvalidateLayout [0ms]
  -MarkDOMContent [0ms]
.
-RunTask [1.897ms]
  -BeginMainThreadFrame [0ms]
  -Layout [0.44ms]
    -InvalidateLayout [0ms]
.
  -UpdateLayerTree [0.247ms]
  -Paint [0.289ms]
..
  -firstPaint [0ms]
  -firstContentfulPaint [0ms]
..
  -largestContentfulPaint::Candidate [0ms]
....................................
-RunTask [1.304ms]
  -EventDispatch (pagehide) [0.016ms]
  -EventDispatch (visibilitychange) [0.009ms]
  -EventDispatch (webkitvisibilitychange) [0.004ms]
  -EventDispatch (unload) [0.015ms]
.
  -ScheduleStyleRecalculation [0ms]
......................................................................................................................`);
  });

  it('builds a hierarchy for the sub frame\'s main thread in a real world profile', async () => {
    const {Renderer: renderers} = await handleEventsFromTraceFile(this, 'multiple-navigations-with-iframes.json.gz');
    const frame = renderers.processes.get(Trace.Types.Events.ProcessID(SUB_FRAME_PID)) as
        Trace.Handlers.ModelHandlers.Renderer.RendererProcess;
    const thread = [...frame.threads.values()].find(thread => thread.name === 'CrRendererMain');
    if (!thread) {
      assert(false, 'Main thread was not found');
    }

    const tree = thread.tree;
    if (!tree) {
      assert(false, 'Main thread has no tree of events');
    }
    const isIncluded = (_node: Trace.Helpers.TreeHelpers.TraceEntryNode, event: Trace.Types.Events.Event) =>
        Boolean(Utils.EntryStyles.getEventStyle(event.name as Trace.Types.Events.Name));
    assert.strictEqual(prettyPrint(tree, isIncluded), `
-RunTask [0.13ms]
-RunTask [0.005ms]
-RunTask [0.009ms]
-RunTask [0.065ms]
-RunTask [0.084ms]
-RunTask [0.041ms]
-RunTask [0.057ms]
-RunTask [0.021ms]
-RunTask [0.009ms]
-RunTask [0.065ms]
-RunTask [0.078ms]
-RunTask [0.043ms]
-RunTask [0.077ms]
  -ScheduleStyleRecalculation [0ms]
-RunTask [0.415ms]
-RunTask [0ms]
-EventDispatch (pagehide) [0.012ms]
-EventDispatch (visibilitychange) [0.007ms]
-EventDispatch (webkitvisibilitychange) [0.016ms]
-EventDispatch (unload) [0.007ms]
.`);
  });

  it('has some correct known roots for the main frame\'s main thread in a real world profile', async () => {
    const {Renderer: renderers} = await handleEventsFromTraceFile(this, 'multiple-navigations-with-iframes.json.gz');
    const frame = renderers.processes.get(Trace.Types.Events.ProcessID(MAIN_FRAME_PID)) as
        Trace.Handlers.ModelHandlers.Renderer.RendererProcess;
    const thread = [...frame.threads.values()].find(thread => thread.name === 'CrRendererMain');
    if (!thread) {
      assert(false, 'Main thread was not found');
    }

    const tree = thread.tree;
    if (!tree) {
      assert(false, 'Main thread has no tree of events');
    }

    const event0 = getRootAt(thread, 1).entry;
    assert.deepEqual(event0 as unknown, {
      args: {},
      cat: 'disabled-by-default-devtools.timeline',
      dur: 132,
      name: 'RunTask',
      ph: 'X',
      pid: 2154214,
      tdur: 131,
      tid: 1,
      ts: 643492822363,
      tts: 291450,
    });
    assert.strictEqual(renderers.entryToNode.get(event0)?.selfTime, 132);

    const event1 = getRootAt(thread, 2).entry;
    assert.deepEqual(event1 as unknown, {
      args: {},
      cat: 'disabled-by-default-devtools.timeline',
      dur: 4,
      name: 'RunTask',
      ph: 'X',
      pid: 2154214,
      tdur: 4,
      tid: 1,
      ts: 643492822500,
      tts: 291586,
    });
    assert.strictEqual(renderers.entryToNode.get(event1)?.selfTime, 4);

    const eventLast = getRootAt(thread, tree.roots.size - 1).entry;
    assert.deepEqual(eventLast as unknown, {
      args: {},
      cat: 'disabled-by-default-devtools.timeline',
      dur: 67,
      name: 'RunTask',
      ph: 'X',
      pid: 2154214,
      tdur: 67,
      tid: 1,
      ts: 643499551460,
      tts: 949032,
    });
    assert.strictEqual(renderers.entryToNode.get(eventLast)?.selfTime, 35);
  });

  it('has some correct known roots for the sub frame\'s main thread in a real world profile', async () => {
    const {Renderer: renderers} = await handleEventsFromTraceFile(this, 'multiple-navigations-with-iframes.json.gz');
    const frame = renderers.processes.get(Trace.Types.Events.ProcessID(SUB_FRAME_PID)) as
        Trace.Handlers.ModelHandlers.Renderer.RendererProcess;
    const thread = [...frame.threads.values()].find(thread => thread.name === 'CrRendererMain');
    if (!thread) {
      assert(false, 'Main thread was not found');
    }

    const tree = thread.tree;
    if (!tree) {
      assert(false, 'Main thread has no tree of events');
    }

    const event0 = getRootAt(thread, 0).entry;
    assert.deepEqual(event0 as unknown, {
      args: {},
      cat: 'disabled-by-default-devtools.timeline',
      dur: 130,
      name: 'RunTask',
      ph: 'X',
      pid: 2236065,
      tdur: 129,
      tid: 1,
      ts: 643492822099,
      tts: 62157,
    });
    assert.strictEqual(renderers.entryToNode.get(event0)?.selfTime, 130);

    const event1 = getRootAt(thread, 1).entry;
    assert.deepEqual(event1 as unknown, {
      args: {},
      cat: 'disabled-by-default-devtools.timeline',
      dur: 5,
      name: 'RunTask',
      ph: 'X',
      pid: 2236065,
      tdur: 5,
      tid: 1,
      ts: 643492822234,
      tts: 62291,
    });
    assert.strictEqual(renderers.entryToNode.get(event1)?.selfTime, 5);

    const event2 = getRootAt(thread, 2).entry;

    assert.deepEqual(event2 as unknown, {
      args: {},
      cat: 'disabled-by-default-devtools.timeline',
      dur: 9,
      name: 'RunTask',
      ph: 'X',
      pid: 2236065,
      tdur: 9,
      tid: 1,
      ts: 643492822242,
      tts: 62299,
    });
    assert.strictEqual(renderers.entryToNode.get(event2)?.selfTime, 9);
  });

  it('can correctly sort a simple list of complete events', async () => {
    const data = [
      makeCompleteEvent('d0', 2, 1),
      makeCompleteEvent('b0', 1, 1),
      makeCompleteEvent('a0', 0, 1),
      makeCompleteEvent('a1', 0, 0.5),
      makeCompleteEvent('a2', 0.5, 0.5),
      makeCompleteEvent('c0', 1.5, 0.5),
      makeCompleteEvent('a4', 0.99, 0.01),
      makeCompleteEvent('b1', 1, 0.01),
      makeCompleteEvent('a3', 0.5, 0.25),
    ];

    Trace.Helpers.Trace.sortTraceEventsInPlace(data);

    assert.deepEqual(data.map(e => ({name: e.name, ts: e.ts, dur: e.dur})) as unknown, [
      {name: 'a0', ts: 0, dur: 1},
      {name: 'a1', ts: 0, dur: 0.5},
      {name: 'a2', ts: 0.5, dur: 0.5},
      {name: 'a3', ts: 0.5, dur: 0.25},
      {name: 'a4', ts: 0.99, dur: 0.01},
      {name: 'b0', ts: 1, dur: 1},
      {name: 'b1', ts: 1, dur: 0.01},
      {name: 'c0', ts: 1.5, dur: 0.5},
      {name: 'd0', ts: 2, dur: 1},
    ]);
  });

  it('can correctly sort a simple list of complete events interspersed with instant events', async () => {
    const data = [
      makeCompleteEvent('d0', 2, 1),
      makeInstantEvent('i0', 0),
      makeCompleteEvent('b0', 1, 1),
      makeInstantEvent('i1', 0.01),
      makeCompleteEvent('a0', 0, 1),
      makeInstantEvent('i2', 0.5),
      makeCompleteEvent('a1', 0, 0.5),
      makeInstantEvent('i3', 0.99),
      makeCompleteEvent('a2', 0.5, 0.5),
      makeInstantEvent('i4', 1),
      makeCompleteEvent('c0', 1.5, 0.5),
      makeInstantEvent('i5', 1.75),
      makeCompleteEvent('a4', 0.99, 0.01),
      makeInstantEvent('i6', 1.99),
      makeCompleteEvent('b1', 1, 0.01),
      makeInstantEvent('i7', 2),
      makeCompleteEvent('a3', 0.5, 0.25),
      makeInstantEvent('i8', 2.01),
    ];

    Trace.Helpers.Trace.sortTraceEventsInPlace(data);

    assert.deepEqual(data.map(e => ({name: e.name, ts: e.ts, dur: e.dur})) as unknown, [
      {name: 'a0', ts: 0, dur: 1},
      {name: 'a1', ts: 0, dur: 0.5},
      {name: 'i0', ts: 0, dur: undefined},
      {name: 'i1', ts: 0.01, dur: undefined},
      {name: 'a2', ts: 0.5, dur: 0.5},
      {name: 'a3', ts: 0.5, dur: 0.25},
      {name: 'i2', ts: 0.5, dur: undefined},
      {name: 'a4', ts: 0.99, dur: 0.01},
      {name: 'i3', ts: 0.99, dur: undefined},
      {name: 'b0', ts: 1, dur: 1},
      {name: 'b1', ts: 1, dur: 0.01},
      {name: 'i4', ts: 1, dur: undefined},
      {name: 'c0', ts: 1.5, dur: 0.5},
      {name: 'i5', ts: 1.75, dur: undefined},
      {name: 'i6', ts: 1.99, dur: undefined},
      {name: 'd0', ts: 2, dur: 1},
      {name: 'i7', ts: 2, dur: undefined},
      {name: 'i8', ts: 2.01, dur: undefined},
    ]);
  });

  it('can process multiple processes', async () => {
    /**
     * |------------- Task A -------------||-- Task E --|
     *  |-- Task B --||-- Task D --|
     *   |- Task C -|
     */
    const data1 = [
      makeCompleteEvent('A', 0, 10),  // 0..10
      makeCompleteEvent('B', 1, 3),   // 1..4
      makeCompleteEvent('D', 5, 3),   // 5..8
      makeCompleteEvent('C', 2, 1),   // 2..3
      makeCompleteEvent('E', 11, 3),  // 11..14
    ];

    /**
     * |-- Task F --||------------- Task G -------------|
     *               |-- Task H --||-- Task J --|
     *                 |- Task I -|
     */
    const data2 = [
      makeCompleteEvent('F', 0, 3),   // 0..3
      makeCompleteEvent('G', 3, 10),  // 3..13 (starts when F finishes)
      makeCompleteEvent('H', 3, 3),   // 3..6 (starts same time as G)
      makeCompleteEvent('J', 6, 3),   // 6..9 (starts when H finishes)
      makeCompleteEvent('I', 5, 1),   // 5..6 (finishes when H finishes)
    ];

    const processes = new Map([
      [
        Trace.Types.Events.ProcessID(0),
        {
          url: ('http://a.com'),
          isOnMainFrame: true,
          threads: new Map([[
            Trace.Types.Events.ThreadID(1),
            {name: 'Foo', entries: data1},
          ]]),
        } as Trace.Handlers.ModelHandlers.Renderer.RendererProcess,
      ],
      [
        Trace.Types.Events.ProcessID(2),
        {
          url: ('http://b.com'),
          isOnMainFrame: false,
          threads: new Map([[
            Trace.Types.Events.ThreadID(3),
            {name: 'Bar', entries: data2},
          ]]),
        } as Trace.Handlers.ModelHandlers.Renderer.RendererProcess,
      ],
    ]);

    await Trace.Handlers.ModelHandlers.Samples.finalize();
    Trace.Handlers.ModelHandlers.Renderer.buildHierarchy(processes, {filter: {has: () => true}});

    const firstThread = [...[...processes.values()][0].threads.values()][0];
    const secondThread = [...[...processes.values()][1].threads.values()][0];

    if (!firstThread.tree || !secondThread.tree) {
      assert(false, 'Trees not found');
    }

    assert.strictEqual(firstThread.tree.maxDepth, 3, 'Got the correct tree max depth for the first thread');
    assert.strictEqual(secondThread.tree.maxDepth, 3, 'Got the correct tree max depth for the second thread');

    const firstRoots = getEventsIn(firstThread.tree.roots.values());
    assert.deepEqual(firstRoots.map(e => e ? {name: e.name, ts: e.ts, dur: e.dur} : null) as unknown[], [
      {name: 'A', ts: 0, dur: 10},
      {name: 'E', ts: 11, dur: 3},
    ]);

    const secondRoots = getEventsIn(secondThread.tree.roots.values());
    assert.deepEqual(secondRoots.map(e => e ? {name: e.name, ts: e.ts, dur: e.dur} : null) as unknown[], [
      {name: 'F', ts: 0, dur: 3},
      {name: 'G', ts: 3, dur: 10},
    ]);
  });

  it('can assign origins to processes', async () => {
    const {Meta: metadata} = await handleEventsFromTraceFile(this, 'multiple-navigations-with-iframes.json.gz');
    const processes: Map<Trace.Types.Events.ProcessID, Trace.Handlers.ModelHandlers.Renderer.RendererProcess> =
        new Map();

    Trace.Handlers.ModelHandlers.Renderer.assignOrigin(processes, metadata.rendererProcessesByFrame);

    assert.deepEqual([...processes].map(([pid, p]) => [pid, p.url ? new URL(p.url).origin : null]), [
      [Trace.Types.Events.ProcessID(MAIN_FRAME_PID), 'http://localhost:5000'],
      [Trace.Types.Events.ProcessID(SUB_FRAME_PID), 'https://www.example.com'],
      [Trace.Types.Events.ProcessID(SUB_FRAME_PID_2), 'https://www.example.com'],
      [Trace.Types.Events.ProcessID(SUB_FRAME_PID_3), 'https://www.example.com'],
    ]);
  });

  it('can assign main frame flags to processes', async () => {
    const {Meta: metadata} = await handleEventsFromTraceFile(this, 'multiple-navigations-with-iframes.json.gz');
    const processes: Map<Trace.Types.Events.ProcessID, Trace.Handlers.ModelHandlers.Renderer.RendererProcess> =
        new Map();

    Trace.Handlers.ModelHandlers.Renderer.assignIsMainFrame(
        processes, metadata.mainFrameId, metadata.rendererProcessesByFrame);

    assert.deepEqual([...processes].map(([pid, p]) => [pid, p.isOnMainFrame]), [
      [Trace.Types.Events.ProcessID(MAIN_FRAME_PID), true],
      [Trace.Types.Events.ProcessID(SUB_FRAME_PID), false],
      [Trace.Types.Events.ProcessID(SUB_FRAME_PID_2), false],
      [Trace.Types.Events.ProcessID(SUB_FRAME_PID_3), false],
    ]);
  });

  it('can assign thread names to threads in processes', async () => {
    const {Meta: metadata} = await handleEventsFromTraceFile(this, 'multiple-navigations-with-iframes.json.gz');
    const {mainFrameId, rendererProcessesByFrame, threadsInProcess} = metadata;
    const processes: Map<Trace.Types.Events.ProcessID, Trace.Handlers.ModelHandlers.Renderer.RendererProcess> =
        new Map();

    Trace.Handlers.ModelHandlers.Renderer.assignMeta(
        processes, mainFrameId, rendererProcessesByFrame, threadsInProcess);

    assert.deepEqual([...processes].map(([pid, p]) => [pid, [...p.threads].map(([tid, t]) => [tid, t.name])]), [
      [
        Trace.Types.Events.ProcessID(MAIN_FRAME_PID),
        [
          [Trace.Types.Events.ThreadID(1), 'CrRendererMain'],
          [Trace.Types.Events.ThreadID(7), 'Compositor'],
          [Trace.Types.Events.ThreadID(2), 'ThreadPoolServiceThread'],
          [Trace.Types.Events.ThreadID(4), 'Chrome_ChildIOThread'],
          [Trace.Types.Events.ThreadID(24), 'ThreadPoolForegroundWorker'],
          [Trace.Types.Events.ThreadID(27), 'ThreadPoolForegroundWorker'],
          [Trace.Types.Events.ThreadID(17), 'ThreadPoolForegroundWorker'],
          [Trace.Types.Events.ThreadID(29), 'ThreadPoolForegroundWorker'],
          [Trace.Types.Events.ThreadID(25), 'ThreadPoolForegroundWorker'],
          [Trace.Types.Events.ThreadID(28), 'ThreadPoolForegroundWorker'],
          [Trace.Types.Events.ThreadID(30), 'ThreadPoolForegroundWorker'],
          [Trace.Types.Events.ThreadID(26), 'ThreadPoolForegroundWorker'],
          [Trace.Types.Events.ThreadID(11), 'CompositorTileWorker3'],
          [Trace.Types.Events.ThreadID(12), 'CompositorTileWorker4'],
          [Trace.Types.Events.ThreadID(10), 'CompositorTileWorker2'],
          [Trace.Types.Events.ThreadID(9), 'CompositorTileWorker1'],
        ],
      ],
      [
        Trace.Types.Events.ProcessID(SUB_FRAME_PID),
        [
          [Trace.Types.Events.ThreadID(2), 'ThreadPoolServiceThread'],
          [Trace.Types.Events.ThreadID(1), 'CrRendererMain'],
          [Trace.Types.Events.ThreadID(7), 'Compositor'],
          [Trace.Types.Events.ThreadID(4), 'Chrome_ChildIOThread'],
        ],
      ],
      [
        Trace.Types.Events.ProcessID(SUB_FRAME_PID_2),
        [
          [Trace.Types.Events.ThreadID(1), 'CrRendererMain'],
          [Trace.Types.Events.ThreadID(4), 'Chrome_ChildIOThread'],
          [Trace.Types.Events.ThreadID(8), 'Compositor'],
          [Trace.Types.Events.ThreadID(2), 'ThreadPoolServiceThread'],
          [Trace.Types.Events.ThreadID(10), 'CompositorTileWorker1'],
        ],
      ],
      [
        Trace.Types.Events.ProcessID(SUB_FRAME_PID_3),
        [
          [Trace.Types.Events.ThreadID(1), 'CrRendererMain'],
          [Trace.Types.Events.ThreadID(2), 'ThreadPoolServiceThread'],
          [Trace.Types.Events.ThreadID(4), 'Chrome_ChildIOThread'],
          [Trace.Types.Events.ThreadID(7), 'Compositor'],
          [Trace.Types.Events.ThreadID(10), 'CompositorTileWorker2'],
          [Trace.Types.Events.ThreadID(3), 'ThreadPoolForegroundWorker'],
        ],
      ],
    ]);
  });

  it('populates the map of trace events to tree nodes', async () => {
    const {Renderer: renderers} = await handleEventsFromTraceFile(this, 'multiple-navigations-with-iframes.json.gz');
    assert.strictEqual(renderers.entryToNode.size, 3591);
  });

  describe('Synthetic complete events', () => {
    async function handleEvents(traceEvents: Trace.Types.Events.Event[]):
        Promise<Trace.Handlers.ModelHandlers.Renderer.RendererHandlerData> {
      Trace.Handlers.ModelHandlers.Renderer.reset();
      Trace.Handlers.ModelHandlers.Meta.reset();
      Trace.Handlers.ModelHandlers.Samples.reset();

      for (const event of traceEvents) {
        Trace.Handlers.ModelHandlers.Meta.handleEvent(event);
        Trace.Handlers.ModelHandlers.Renderer.handleEvent(event);
      }

      await Trace.Handlers.ModelHandlers.Meta.finalize();
      await Trace.Handlers.ModelHandlers.Samples.finalize();
      await Trace.Handlers.ModelHandlers.Renderer.finalize();
      return Trace.Handlers.ModelHandlers.Renderer.data();
    }
    let defaultTraceEvents: readonly Trace.Types.Events.Event[];
    const pid = Trace.Types.Events.ProcessID(28274);
    const tid = Trace.Types.Events.ThreadID(775);
    beforeEach(async function() {
      defaultTraceEvents = await TraceLoader.rawEvents(this, 'missing-url.json.gz');
    });

    afterEach(() => {
      Trace.Handlers.ModelHandlers.Renderer.reset();
      Trace.Handlers.ModelHandlers.Meta.reset();
      Trace.Handlers.ModelHandlers.Samples.reset();
    });

    it('builds a hierarchy using begin and end trace events', async () => {
      // |------------- RunTask -------------||-- RunTask --|
      //  |-- RunMicrotasks --||-- Layout --|
      //   |- FunctionCall -|
      const traceEvents = [
        ...defaultTraceEvents, makeBeginEvent('RunTask', 0, '*', pid, tid),  // 0..10
        makeBeginEvent('RunMicrotasks', 1, '*', pid, tid),                   // 1..4
        makeBeginEvent('FunctionCall', 2, '*', pid, tid),                    // 2..3
        makeEndEvent('FunctionCall', 3, '*', pid, tid),                      // 2..3
        makeEndEvent('RunMicrotasks', 4, '*', pid, tid),                     // 1..4
        makeBeginEvent('Layout', 5, '*', pid, tid),                          // 5..8
        makeEndEvent('Layout', 8, '*', pid, tid),                            // 5..8
        makeEndEvent('RunTask', 10, '*', pid, tid),                          // 0..10
        makeBeginEvent('RunTask', 11, '*', pid, tid),                        // 11..14
        makeEndEvent('RunTask', 14, '*', pid, tid),                          // 11..14
      ];

      const data = await handleEvents(traceEvents);

      assert.lengthOf(data.allTraceEntries, 7);
      assert.strictEqual(data.processes.size, 1);
      const [process] = data.processes.values();
      assert.strictEqual(process.threads.size, 1);
      const [thread] = process.threads.values();
      assert.strictEqual(thread.tree?.roots.size, 2);
      if (!thread.tree?.roots) {
        // This shouldn't happen, since the tree.roots.size is 2, but add this if check to pass ts check.
        return;
      }
      const allNodes = getAllNodes(thread.tree?.roots);
      assert.lengthOf(allNodes, 5);
      if (!thread.tree) {
        return;
      }
      assert.strictEqual(prettyPrint(thread.tree), `
-RunTask [0.01ms]
  -RunMicrotasks [0.003ms]
    -FunctionCall [0.001ms]
  -Layout [0.003ms]
-RunTask [0.003ms]`);
    });
    it('builds a hierarchy using complete, begin and end trace events', async () => {
      // |------------- RunTask -------------|
      //  |-- RunMicrotasks --||-- Layout --|
      //   |- FunctionCall -|

      const traceEvents = [
        ...defaultTraceEvents, makeBeginEvent('RunTask', 0, '*', pid, tid),  // 0..10
        makeBeginEvent('RunMicrotasks', 1, '*', pid, tid),                   // 1..4
        makeCompleteEvent('FunctionCall', 2, 1, '*', pid, tid),              // 2..3
        makeEndEvent('RunMicrotasks', 4, '*', pid, tid),                     // 1..4
        makeBeginEvent('Layout', 5, '*', pid, tid),                          // 5..8
        makeEndEvent('Layout', 8, '*', pid, tid),                            // 5..8
        makeEndEvent('RunTask', 10, '*', pid, tid),                          // 0..10
      ];

      const data = await handleEvents(traceEvents);

      assert.lengthOf(data.allTraceEntries, 6);
      assert.strictEqual(data.processes.size, 1);
      const [process] = data.processes.values();
      assert.strictEqual(process.threads.size, 1);
      const [thread] = process.threads.values();
      assert.strictEqual(thread.tree?.roots.size, 1);
      if (!thread.tree?.roots) {
        // This shouldn't happen, since the tree.roots.size is 1, but add this if check to pass ts check.
        return;
      }
      const allNodes = getAllNodes(thread.tree?.roots);
      assert.lengthOf(allNodes, 4);
      if (!thread.tree) {
        return;
      }
      assert.strictEqual(prettyPrint(thread.tree), `
-RunTask [0.01ms]
  -RunMicrotasks [0.003ms]
    -FunctionCall [0.001ms]
  -Layout [0.003ms]`);
    });

    it('keeps a FunctionCall that has the end event missing', async () => {
      const traceEvents = [
        ...defaultTraceEvents, makeBeginEvent('RunMicrotasks', 1, '*', pid, tid),  // 1..4
        makeBeginEvent('FunctionCall', 2, '*', pid, tid),                          // 2..3
      ];

      const data = await handleEvents(traceEvents);
      assert.strictEqual(data.processes.size, 1);
      const [process] = data.processes.values();
      assert.strictEqual(process.threads.size, 1);
      const [thread] = process.threads.values();
      if (!thread.tree) {
        throw new Error('thread should have a tree');
      }
      // Ensure that the FunctionCall event has been kept despite not having an END event.
      assert.deepEqual(thread.entries.map(e => e.name), ['RunMicrotasks', 'FunctionCall']);
    });
  });

  describe('building hierarchies trace events and profile calls', () => {
    it('build a hierarchy using data from real world trace file', async () => {
      const {Renderer} = await handleEventsFromTraceFile(this, 'recursive-counting-js.json.gz');
      const threadId = Trace.Types.Events.ThreadID(259);
      const firstProcessId = Trace.Types.Events.ProcessID(23239);
      const thread = Renderer.processes.get(firstProcessId)?.threads.get(threadId);
      if (!thread || !thread.tree) {
        throw new Error('Tree not found');
      }
      const onlyLongTasksPredicate =
          (_node: Trace.Helpers.TreeHelpers.TraceEntryNode, event: Trace.Types.Events.Event) =>
              Boolean(event.dur && event.dur > 1000) &&
          Boolean(Utils.EntryStyles.getEventStyle(event.name as Trace.Types.Events.Name));
      assert.strictEqual(prettyPrint(thread.tree, onlyLongTasksPredicate), `
.............
-RunTask [17.269ms]
.............................
-RunTask [1065.663ms]
  -ParseHTML [1065.609ms]
.........
-RunTask [1.12ms]
  -ParseHTML [1.082ms]
.........................................................
-RunTask [1058.811ms]
  -TimerFire [1058.77ms]
    -FunctionCall [1058.693ms]
.
      -ProfileCall (anonymous) [1058.589ms]
        -ProfileCall (foo) [1058.589ms]
          -ProfileCall (foo) [1058.589ms]
            -ProfileCall (foo) [1058.589ms]
              -ProfileCall (foo) [1058.589ms]
..
                -ProfileCall (count) [1058.453ms]
........
-RunTask [1057.455ms]
  -TimerFire [1057.391ms]
    -FunctionCall [1057.27ms]
.
      -ProfileCall (anonymous) [1056.579ms]
        -ProfileCall (foo) [1056.579ms]
          -ProfileCall (foo) [1056.579ms]
            -ProfileCall (foo) [1056.579ms]
              -ProfileCall (foo) [1056.579ms]
                -ProfileCall (count) [1056.538ms]
........`);
    });
  });

  it('identifies and returns rasterizer threads', async () => {
    const {Renderer} = await handleEventsFromTraceFile(this, 'web-dev.json.gz');
    assert.deepEqual(Array.from(Renderer.compositorTileWorkers.entries()), [
      [
        Trace.Types.Events.ProcessID(68481),
        [
          Trace.Types.Events.ThreadID(81675),
        ],
      ],
      [
        Trace.Types.Events.ProcessID(73704),
        [
          Trace.Types.Events.ThreadID(23299),
          Trace.Types.Events.ThreadID(22275),
          Trace.Types.Events.ThreadID(41475),
          Trace.Types.Events.ThreadID(40451),
          Trace.Types.Events.ThreadID(22531),
        ],
      ],
    ]);
  });

  it('keeps the processes associated with AuctionWorklets and assigns them URLs', async () => {
    const {Renderer, AuctionWorklets} = await handleEventsFromTraceFile(this, 'fenced-frame-fledge.json.gz');
    assert.strictEqual(AuctionWorklets.worklets.size, 3);
    for (const [pid] of AuctionWorklets.worklets) {
      const process = Renderer.processes.get(pid);
      assert.exists(process);
      // Ensure that the URL was set properly based on the AuctionWorklets metadata event.
      assert.isTrue(process?.url?.includes('fledge-demo.glitch.me'));
    }
  });
  describe('ThirdParty', () => {
    it('correctly creates entities (simple)', async function() {
      const {Renderer} = await handleEventsFromTraceFile(this, 'load-simple.json.gz');
      const entities = Array.from(Renderer.entityMappings.eventsByEntity.keys()).map(entity => entity.name);
      const expectedEntities = ['localhost', 'Google Fonts'];
      assert.deepEqual(entities, expectedEntities);
    });
    it('correctly creates entities', async function() {
      const {Renderer} = await handleEventsFromTraceFile(this, 'lantern/paul/trace.json.gz');
      const entityNames = [...Renderer.entityMappings.eventsByEntity.keys()].map(entity => entity.name);
      assert.deepEqual([...new Set(entityNames)], [
        'paulirish.com',
        'Google Tag Manager',
        'Google Fonts',
        'Disqus',
        'Google Analytics',
      ]);
    });
  });
});
