UNPKG

chrome-devtools-frontend

Version:
1,021 lines (951 loc) • 40.3 kB
// 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', ]); }); }); });