chrome-devtools-frontend
Version:
Chrome DevTools UI
1,021 lines (951 loc) • 40.3 kB
text/typescript
// 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, 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 = new Map<Trace.Types.Events.ProcessID, Trace.Handlers.ModelHandlers.Renderer.RendererProcess>();
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 = new Map<Trace.Types.Events.ProcessID, Trace.Handlers.ModelHandlers.Renderer.RendererProcess>();
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 = new Map<Trace.Types.Events.ProcessID, Trace.Handlers.ModelHandlers.Renderer.RendererProcess>();
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?.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',
'Google Analytics',
'Disqus',
// Firebase has no self time, but entity was created in NetworkHandler because transferSize
'Firebase',
]);
});
});
});