UNPKG

chrome-devtools-frontend

Version:
520 lines (440 loc) 21 kB
// Copyright 2024 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 Root from '../../../core/root/root.js'; import * as Trace from '../../../models/trace/trace.js'; import {describeWithEnvironment} from '../../../testing/EnvironmentHelpers.js'; import {TraceLoader} from '../../../testing/TraceLoader.js'; import * as Utils from './utils.js'; describeWithEnvironment('AICallTree', () => { beforeEach(() => { Root.Runtime.experiments.disableForTest('timeline-show-all-events'); }); it('will not build a tree from non-main-thread events', async function() { const {parsedTrace} = await TraceLoader.traceEngine(this, 'cls-single-frame.json.gz'); // A random RasterizerTask. Although this does technically run on the // main _frame_, it is not on the thread we identify as the main thread. const rasterTask = parsedTrace.Renderer.allTraceEntries.find(e => { return e.name === Trace.Types.Events.Name.RASTER_TASK && e.pid === 4274 && e.tid === 23555; }); assert.isOk(rasterTask); assert.isNull(Utils.AICallTree.AICallTree.fromEvent(rasterTask, parsedTrace)); }); it('does not build a tree from events the renderer is not aware of', async function() { const {parsedTrace} = await TraceLoader.traceEngine(this, 'cls-single-frame.json.gz'); // A SyntheticLayoutShift: the RendererHandler does not know about this. const shift = parsedTrace.LayoutShifts.clusters.at(0)?.events.at(0); assert.isOk(shift); assert.isTrue(Trace.Types.Events.isSyntheticLayoutShift(shift)); assert.isNull(Utils.AICallTree.AICallTree.fromEvent(shift, parsedTrace)); }); it('does not build a call tree from a performance.mark', async function() { const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev-with-timings.json.gz'); const mark = parsedTrace.UserTimings.performanceMarks.at(0); assert.isOk(mark); assert.isNull(Utils.AICallTree.AICallTree.fromEvent(mark, parsedTrace)); }); it('does not build a call tree from a performance.measure', async function() { const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev-with-timings.json.gz'); const measure = parsedTrace.UserTimings.performanceMeasures.at(0); assert.isOk(measure); assert.isNull(Utils.AICallTree.AICallTree.fromEvent(measure, parsedTrace)); }); it('supports NodeJS traces that do not have a "main thread"', async function() { // Bit of extra setup required: we need to mimic what the panel does where // it takes the CDP Profile and wraps it in fake trace events, before then // passing that through to the new engine. const rawEvents = await TraceLoader.rawCPUProfile(this, 'basic.cpuprofile.gz'); const events = Trace.Helpers.SamplesIntegrator.SamplesIntegrator.createFakeTraceFromCpuProfile( rawEvents, Trace.Types.Events.ThreadID(1), ); const {parsedTrace} = await TraceLoader.executeTraceEngineOnFileContents(events); // Find a random function call in the trace. const funcCall = parsedTrace.Samples.entryToNode.keys().find(event => { return Trace.Types.Events.isProfileCall(event) && event.callFrame.functionName === 'callAndPauseOnStart'; }); assert.isOk(funcCall); const callTree = Utils.AICallTree.AICallTree.fromEvent(funcCall, parsedTrace); assert.isOk(callTree); const expectedData = '\n' + ` # All URL #s: * 0: node:internal/main/run_main_module * 1: node:internal/modules/run_main * 2: node:internal/modules/cjs/loader * 3: file:///Users/andoli/Desktop/mocks/fixnodeinspector/app.js # Call tree: Node: 1 – (anonymous) dur: 2370 URL #: 0 Children: * 2 – executeUserEntryPoint Node: 2 – executeUserEntryPoint dur: 2370 URL #: 1 Children: * 3 – Module._load Node: 3 – Module._load dur: 2370 URL #: 2 Children: * 4 – Module.load Node: 4 – Module.load dur: 2370 URL #: 2 Children: * 5 – Module._extensions..js Node: 5 – Module._extensions..js dur: 2370 URL #: 2 Children: * 6 – Module._compile Node: 6 – Module._compile dur: 2370 URL #: 2 Children: * 7 – callAndPauseOnStart Node: 7 – callAndPauseOnStart Selected: true dur: 2370 Children: * 8 – (anonymous) Node: 8 – (anonymous) dur: 2370 self: 2370 URL #: 3 `.trim(); assert.strictEqual(callTree?.serialize(), expectedData); }); it('serializes a simple tree', async function() { const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev-outermost-frames.json.gz'); const mainEvents = parsedTrace.Renderer.allTraceEntries; // A function '_ds.q.ns'. Has a very small tree by default. const selectedEvent = mainEvents.find(event => event.ts === 465457308823); if (!selectedEvent) { throw new Error('Could not find expected event.'); } const callTree = Utils.AICallTree.AICallTree.fromEvent(selectedEvent, parsedTrace); const expectedData = '\n' + ` # All URL #s: * 0: https://www.gstatic.com/devrel-devsite/prod/vafe2e13ca17bb026e70df42a2ead1c8192750e86a12923a88eda839025dabf95/js/devsite_app_module.js # Call tree: Node: 1 – Task dur: 0.2 Children: * 2 – Timer fired Node: 2 – Timer fired dur: 0.2 Children: * 3 – Function call Node: 3 – Function call dur: 0.2 URL #: 0 Children: * 4 – _ds.q.ns Node: 4 – _ds.q.ns Selected: true dur: 0.2 URL #: 0 Children: * 5 – clearTimeout Node: 5 – clearTimeout dur: 0.2 self: 0 Children: * 6 – Recalculate style Node: 6 – Recalculate style dur: 0.2 self: 0.2 `.trim(); assert.strictEqual(callTree?.serialize(), expectedData); }); it('correctly serializes selected node with multiple children', async function() { const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev.json.gz'); const mainEvents = parsedTrace.Renderer.allTraceEntries; const selectedEvent = mainEvents.find(event => event.ts === 1020034984106); if (!selectedEvent) { throw new Error('Could not find expected event.'); } const callTree = Utils.AICallTree.AICallTree.fromEvent(selectedEvent, parsedTrace); let stringifiedNode = ''; if (callTree?.selectedNode) { stringifiedNode = callTree?.stringifyNodeCompressed(callTree.selectedNode, 2, parsedTrace, callTree.selectedNode, [''], 2); } // Entry Format: `id;name;duration;selfTime;urlIndex;childRange;[S] assert.deepEqual(stringifiedNode, '2;define;3.5;0.5;;2-6;S'); }); // Since the childIds are serialized while the node is visited by BFS, // it is important to test that the final parent-child IDs are assigned correctly. it('correctly numbers child node IDs sequentially', async function() { const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev.json.gz'); const mainEvents = parsedTrace.Renderer.allTraceEntries; // The selected event is structured like this: // // || Task || // || recalculate style|| || layout || || update || || paint || // // Here, the only node with children is task and the index of Task node children starts at 2 (Task itself is 1) // If this information is provided correctly, it is enough to serialize that node. const selectedEvent = mainEvents.find(event => event.ts === 1020034919877); if (!selectedEvent) { throw new Error('Could not find expected event.'); } const callTree = Utils.AICallTree.AICallTree.fromEvent(selectedEvent, parsedTrace); const visited: Array<{name: string, nodeIndex: number, childStartingIndex?: number}> = []; const callback = (node: Trace.Extras.TraceTree.Node, nodeIndex: number, childStartingIndex?: number): void => { visited.push({name: Utils.EntryName.nameForEntry(node.event, parsedTrace), nodeIndex, childStartingIndex}); }; callTree?.breadthFirstWalk(callTree.rootNode.children().values(), callback); const expectedVisited = [ {name: 'Task', nodeIndex: 1, childStartingIndex: 2}, {name: 'Recalculate style', nodeIndex: 2, childStartingIndex: undefined}, {name: 'Layout', nodeIndex: 3, childStartingIndex: undefined}, {name: 'Update layer tree', nodeIndex: 4, childStartingIndex: undefined}, {name: 'Paint', nodeIndex: 5, childStartingIndex: undefined}, ]; assert.deepEqual(visited, expectedVisited, 'Callback arguments were incorrect'); }); it('correctly numbers child nodes IDs for larger trees', async function() { const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev.json.gz'); const mainEvents = parsedTrace.Renderer.allTraceEntries; // The selected event is structured like this: // // || Task (1) || // || Timer fired (2) || // || Function Call (3) || // || Anonymous (4) || // || Anonymous (5) || // || Ot.getEntriesByType (6) || || le.createOobTrace (7) || // || getEntriesByType (8) || || le (9) || || ie (10) || // ||reqApisA= (11)|| ||oe (12) || // ||setTimeout (13)|| // // Here, the only node with children is task and the index of Task node children starts at 2 (Task itself is 1) // If this information is provided correctly, it is enough to serialize that node. const selectedEvent = mainEvents.find(event => event.ts === 1020035169460); if (!selectedEvent) { throw new Error('Could not find expected event.'); } const callTree = Utils.AICallTree.AICallTree.fromEvent(selectedEvent, parsedTrace); const visited: Array<{name: string, nodeIndex: number, childStartingIndex?: number}> = []; const callback = (node: Trace.Extras.TraceTree.Node, nodeIndex: number, childStartingIndex?: number): void => { visited.push({name: Utils.EntryName.nameForEntry(node.event, parsedTrace), nodeIndex, childStartingIndex}); }; callTree?.breadthFirstWalk(callTree.rootNode.children().values(), callback); const expectedVisited = [ {name: 'Task', nodeIndex: 1, childStartingIndex: 2}, {name: 'Timer fired', nodeIndex: 2, childStartingIndex: 3}, {name: 'Function call', nodeIndex: 3, childStartingIndex: 4}, {name: '(anonymous)', nodeIndex: 4, childStartingIndex: 5}, {name: '(anonymous)', nodeIndex: 5, childStartingIndex: 6}, {name: 'Ot.getEntriesByType', nodeIndex: 6, childStartingIndex: 8}, {name: 'le.createOobTrace', nodeIndex: 7, childStartingIndex: 9}, {name: 'getEntriesByType', nodeIndex: 8, childStartingIndex: undefined}, {name: 'le', nodeIndex: 9, childStartingIndex: undefined}, {name: 'ie', nodeIndex: 10, childStartingIndex: 11}, {name: 'Ot.requiredApisAvailable', nodeIndex: 11, childStartingIndex: undefined}, {name: 'oe', nodeIndex: 12, childStartingIndex: 13}, {name: 'setTimeout', nodeIndex: 13, childStartingIndex: undefined}, ]; assert.deepEqual(visited, expectedVisited, 'Callback arguments were incorrect'); }); it('serializes a simple tree in a concise format', async function() { const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev-outermost-frames.json.gz'); const mainEvents = parsedTrace.Renderer.allTraceEntries; // A function '_ds.q.ns'. Has a very small tree by default. const selectedEvent = mainEvents.find(event => event.ts === 465457308823); if (!selectedEvent) { throw new Error('Could not find expected event.'); } const callTree = Utils.AICallTree.AICallTree.fromEvent(selectedEvent, parsedTrace); // Entry Format: `id;name;duration;selfTime;urlIndex;childRange;[S] const expectedData = ` # All URL #s: * 0: https://www.gstatic.com/devrel-devsite/prod/vafe2e13ca17bb026e70df42a2ead1c8192750e86a12923a88eda839025dabf95/js/devsite_app_module.js # Call tree: 1;Task;0.2;;;2 2;Timer fired;0.2;;;3 3;Function call;0.2;;0;4 4;_ds.q.ns;0.2;;0;5;S 5;clearTimeout;0.2;0;;6 6;Recalculate style;0.2;0.2;;`; assert.strictEqual(callTree?.serializeIntoCompressedFormat(), expectedData); }); it('serializes a tree in a concise format', async function() { const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev.json.gz'); const mainEvents = parsedTrace.Renderer.allTraceEntries; const selectedEvent = mainEvents.find(event => event.ts === 1020035169460); if (!selectedEvent) { throw new Error('Could not find expected event.'); } const callTree = Utils.AICallTree.AICallTree.fromEvent(selectedEvent, parsedTrace); // Entry Format: `id;name;duration;selfTime;urlIndex;childRange;[S] const expectedData = ` # All URL #s: * 0: https://www.gstatic.com/firebasejs/6.6.1/firebase-performance.js # Call tree: 1;Task;0.9;0;;2;S 2;Timer fired;0.9;0;;3 3;Function call;0.9;0.1;0;4 4;(anonymous);0.8;;0;5 5;(anonymous);0.8;;0;6-8 6;Ot.getEntriesByType;0.1;;0;8 7;le.createOobTrace;0.6;0.2;0;9-11 8;getEntriesByType;0.1;0.1;; 9;le;0.1;0.1;0; 10;ie;0.2;;0;11-13 11;Ot.requiredApisAvailable;0.2;0.2;0; 12;oe;0;;0;13 13;setTimeout;0;0;;`; assert.strictEqual(callTree?.serializeIntoCompressedFormat(), expectedData); }); it('can serialize a tree from an event that is not shown unless "show all events" is enabled', async function() { Root.Runtime.experiments.enableForTest('timeline-show-all-events'); const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev-with-commit.json.gz'); // find a "v8.run" function that would not normally be shown const event = parsedTrace.Renderer.allTraceEntries.find(entry => { return entry.name === 'v8.run' && entry.ts === 122411196071; }); assert.exists(event); const callTree = Utils.AICallTree.AICallTree.fromEvent(event, parsedTrace); assert.isNotNull(callTree); const treeStr = callTree.serialize(); assert.include(treeStr, 'v8.run'); // make sure the event is in the tree }); it('serializes a tree with lots of recursion', async function() { const {parsedTrace} = await TraceLoader.traceEngine(this, 'one-second-interaction.json.gz'); const mainEvents = parsedTrace.Renderer.allTraceEntries; const selectedEvent = mainEvents.find(event => event.ts === 141251951589); if (!selectedEvent) { throw new Error('Could not find expected event.'); } const callTree = Utils.AICallTree.AICallTree.fromEvent(selectedEvent, parsedTrace); assert.isOk(callTree); // We don't need to validate the whole tree, just that it has recursion const treeStr = callTree.serialize(); const lines = treeStr.split('\n'); const fibCallCount = lines.filter(line => line.includes('fibonacci')).length; assert.isTrue(fibCallCount > 10); }); it('AITreeFilter includes the right items in the tree', async function() { const {parsedTrace} = await TraceLoader.traceEngine(this, 'two-workers.json.gz'); const mainEvents = parsedTrace.Renderer.allTraceEntries; // A very small 'get storage' event. It's 6µs long const tinyEvent = mainEvents.find(event => event.ts === 107350149168); if (!tinyEvent) { throw new Error('Could not find expected event.'); } const tinyStr = Utils.AICallTree.AICallTree.fromEvent(tinyEvent, parsedTrace)?.serialize(); assert.strictEqual(tinyStr?.split('\n').filter(l => l.startsWith('Node:')).join('\n'), ` Node: 1 – Task Node: 2 – Parse HTML Node: 3 – Evaluate script Node: 4 – (anonymous) Node: 5 – get storage`.trim()); assert.include(tinyStr, 'get storage'); // An evaluateScript that has 3 'Compile code' children const evaluateEvent = mainEvents.find(event => event.ts === 107350147808); if (!evaluateEvent) { throw new Error('Could not find expected event.'); } const treeStr = Utils.AICallTree.AICallTree.fromEvent(evaluateEvent, parsedTrace)?.serialize(); assert.strictEqual(treeStr?.split('\n').filter(l => l.startsWith('Node:')).join('\n'), ` Node: 1 – Task Node: 2 – Parse HTML Node: 3 – Evaluate script Node: 4 – Compile script Node: 5 – (anonymous) Node: 6 – H.la`.trim()); assert.notInclude(treeStr, 'Compile code'); // An Compile code event within the evaluateEvent call tree const compileEvent = mainEvents.find(event => event.ts === 107350148218); if (!compileEvent) { throw new Error('Could not find expected event.'); } const compileStr = Utils.AICallTree.AICallTree.fromEvent(compileEvent, parsedTrace)?.serialize(); assert.strictEqual(compileStr?.split('\n').filter(l => l.startsWith('Node:')).join('\n'), ` Node: 1 – Task Node: 2 – Parse HTML Node: 3 – Evaluate script Node: 4 – (anonymous) Node: 5 – Compile code`.trim()); assert.include(compileStr, 'Compile code'); }); it('can construct a tree from a period of time', async function() { const {parsedTrace} = await TraceLoader.traceEngine(this, 'nested-interactions.json.gz'); // Picked this interaction event because it spans multiple icicles in the main thread. // Note: if you are debugging this test, it is useful to load up this trace // in RPP and look for the first "keydown" event. const interaction = parsedTrace.UserInteractions.interactionEventsWithNoNesting.find(e => { return Trace.Types.Events.isEventTimingStart(e.rawSourceEvent) && e.rawSourceEvent.args.data.interactionId === 3572; }); assert.isOk(interaction); const timings = Trace.Helpers.Timing.eventTimingsMicroSeconds(interaction); const bounds = Trace.Helpers.Timing.traceWindowFromMicroSeconds(timings.startTime, timings.endTime); const tree = Utils.AICallTree.AICallTree.fromTimeOnThread({ thread: {pid: interaction.pid, tid: interaction.tid}, parsedTrace, bounds, }); assert.isOk(tree); const output = tree.serialize(); const totalNodes = output.split('\n').filter(l => l.startsWith('Node:')).length; assert.strictEqual(totalNodes, 242); // Check the min duration filter is working. // Check there are 3 keydown events. This confirms that the call tree is taking events from the right timespan. const keyDownEvents = output.split('\n').filter(line => { return line.startsWith('Node:') && line.includes('Event: keydown'); }); assert.lengthOf(keyDownEvents, 3); }); }); const makeEvent = (name: string, ts: number, dur: number): Trace.Types.Events.Event => ({ name, cat: 'disabled-by-default-devtools.timeline', ph: Trace.Types.Events.Phase.COMPLETE, ts: Trace.Types.Timing.Micro(ts), dur: Trace.Types.Timing.Micro(dur), pid: Trace.Types.Events.ProcessID(1), tid: Trace.Types.Events.ThreadID(4), args: {}, }); describe('AITreeFilter', () => { it('always includes the selected event', () => { const selectedEvent = makeEvent('selected', 0, 100); const filter = new Utils.AICallTree.SelectedEventDurationFilter(selectedEvent); assert.isTrue(filter.accept(selectedEvent)); }); it('includes events that are long enough', () => { const selectedEvent = makeEvent('selected', 0, 100); const filter = new Utils.AICallTree.SelectedEventDurationFilter(selectedEvent); assert.isTrue(filter.accept(makeEvent('short', 0, 1))); assert.isTrue(filter.accept(makeEvent('short', 0, 0.6))); assert.isTrue(filter.accept(makeEvent('long', 0, 101))); assert.isTrue(filter.accept(makeEvent('long', 0, 200))); assert.isTrue(filter.accept(makeEvent('long', 0, 1000))); }); it('excludes events that are too short', () => { const selectedEvent = makeEvent('selected', 0, 100); const filter = new Utils.AICallTree.SelectedEventDurationFilter(selectedEvent); assert.isFalse(filter.accept(makeEvent('short', 0, 0))); assert.isFalse(filter.accept(makeEvent('short', 0, 0.1))); assert.isFalse(filter.accept(makeEvent('short', 0, 0.4))); }); }); describe('CompileCode filter', () => { it('excludes COMPILE_CODE nodes if non-selected', () => { const selectedEvent = makeEvent('selected', 0, 100); const compileCodeEvent = makeEvent(Trace.Types.Events.Name.COMPILE_CODE, 0, 100); const filter = new Utils.AICallTree.ExcludeCompileCodeFilter(selectedEvent); assert.isFalse(filter.accept(compileCodeEvent)); }); it('includes COMPILE_CODE nodes if selected', () => { const selectedEvent = makeEvent(Trace.Types.Events.Name.COMPILE_CODE, 0, 100); const filter = new Utils.AICallTree.ExcludeCompileCodeFilter(selectedEvent); assert.isTrue(filter.accept(selectedEvent)); }); });