UNPKG

chrome-devtools-frontend

Version:
671 lines (569 loc) • 29.3 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 Lantern from '../lantern.js'; const {NetworkRequestTypes} = Lantern.Types; const {PageDependencyGraph} = Lantern.Graph; function createRequest( requestId: number, url: string, rendererStartTime = 0, initiator: null|Partial<Lantern.Types.NetworkRequest['initiator']> = null, resourceType: keyof typeof NetworkRequestTypes = NetworkRequestTypes.Document, fromWorker = false, ): Lantern.Types.NetworkRequest { const networkEndTime = rendererStartTime + 50; return { requestId: String(requestId), url, rendererStartTime, networkEndTime, initiator, resourceType, fromWorker, } as unknown as Lantern.Types.NetworkRequest; } const getDependencyIds = (node: Lantern.Graph.Node): string[] => node.getDependencies().map(node => node.id); const TOPLEVEL_TASK_NAME = 'TaskQueueManager::ProcessTaskFromWorkQueue'; describe('PageDependencyGraph', () => { let traceEvents: Lantern.Types.TraceEvent[] = []; let url: Lantern.Types.Simulation.URL; function addTaskEvents(startTs: number, duration: number, evts: Array<{ name: string, ts?: number, data?: unknown, }>) { const mainEvent = { name: TOPLEVEL_TASK_NAME, tid: 1, ts: startTs * 1000, dur: duration * 1000, args: {}, }; traceEvents.push(mainEvent as Lantern.Types.TraceEvent); let i = 0; for (const evt of evts) { i++; traceEvents.push({ name: evt.name, ts: typeof evt.ts === 'number' ? (evt.ts * 1000) : NaN || (startTs * 1000 + i), args: {data: evt.data}, } as Lantern.Types.TraceEvent); } } beforeEach(() => { traceEvents = []; url = {requestedUrl: 'https://example.com/', mainDocumentUrl: 'https://example.com/'}; }); describe('#getNetworkNodeOutput', () => { const request1 = createRequest(1, 'https://example.com/'); const request2 = createRequest(2, 'https://example.com/page'); const request3 = createRequest(3, 'https://example.com/page'); const networkRequests = [request1, request2, request3]; it('should create network nodes', () => { const networkNodeOutput = PageDependencyGraph.getNetworkNodeOutput(networkRequests); for (let i = 0; i < networkRequests.length; i++) { const node = networkNodeOutput.nodes[i]; assert.isOk(node, `did not create node at index ${i}`); assert.strictEqual(node.id, String(i + 1)); assert.strictEqual(node.type, 'network'); assert.strictEqual(node.request, networkRequests[i]); } }); it('should ignore worker requests', () => { const workerRequest = createRequest(4, 'https://example.com/worker.js', 0, null, 'Script', true); const recordsWithWorker = [ ...networkRequests, workerRequest, ]; const networkNodeOutput = PageDependencyGraph.getNetworkNodeOutput(recordsWithWorker); expect(networkNodeOutput.nodes).to.have.lengthOf(3); expect(networkNodeOutput.nodes.map(node => node.request)).not.contain(workerRequest); }); it('should index nodes by ID', () => { const networkNodeOutput = PageDependencyGraph.getNetworkNodeOutput(networkRequests); const indexedById = networkNodeOutput.idToNodeMap; for (const request of networkRequests) { assert.strictEqual(indexedById.get(request.requestId)?.request, request); } }); it('should index nodes by URL', () => { const networkNodeOutput = PageDependencyGraph.getNetworkNodeOutput(networkRequests); const nodes = networkNodeOutput.nodes; const indexedByUrl = networkNodeOutput.urlToNodeMap; assert.deepEqual(indexedByUrl.get('https://example.com/'), [nodes[0]]); assert.deepEqual(indexedByUrl.get('https://example.com/page'), [nodes[1], nodes[2]]); }); it('should index nodes by frame', () => { const networkNodeOutput = PageDependencyGraph.getNetworkNodeOutput([ {...createRequest(1, 'https://example.com/'), documentURL: 'https://example.com/', frameId: 'A'}, {...createRequest(2, 'https://example.com/page'), documentURL: 'https://example.com/', frameId: 'A'}, { ...createRequest(3, 'https://example.com/page2'), documentURL: 'https://example.com/page2', frameId: 'C', resourceType: NetworkRequestTypes.XHR, }, {...createRequest(4, 'https://example.com/page3'), documentURL: 'https://example.com/page3', frameId: 'D'}, { ...createRequest(4, 'https://example.com/page4'), documentURL: 'https://example.com/page4', frameId: undefined, }, { ...createRequest(4, 'https://example.com/page5'), documentURL: 'https://example.com/page5', frameId: 'collision', }, { ...createRequest(4, 'https://example.com/page6'), documentURL: 'https://example.com/page6', frameId: 'collision', }, ]); const nodes = networkNodeOutput.nodes; const indexedByFrame = networkNodeOutput.frameIdToNodeMap; expect([...indexedByFrame.entries()]).deep.equals([ ['A', nodes[0]], ['D', nodes[3]], ['collision', null], ]); }); }); describe('#getCPUNodes', () => { it('should create CPU nodes', () => { addTaskEvents(0, 100, [ {name: 'MyCustomEvent'}, {name: 'OtherEvent'}, {name: 'OutsideTheWindow', ts: 200}, {name: 'OrphanedEvent'}, // should be ignored since we stopped at OutsideTheWindow ]); addTaskEvents(250, 50, [ {name: 'LaterEvent'}, ]); assert.lengthOf(traceEvents, 7); const nodes = PageDependencyGraph.getCPUNodes(traceEvents); assert.lengthOf(nodes, 2); const node1 = nodes[0]; assert.strictEqual(node1.id, '1.0'); assert.strictEqual(node1.type, 'cpu'); assert.strictEqual(node1.event, traceEvents[0]); assert.lengthOf(node1.childEvents, 2); assert.strictEqual(node1.childEvents[1].name, 'OtherEvent'); const node2 = nodes[1]; assert.strictEqual(node2.id, '1.250000'); assert.strictEqual(node2.type, 'cpu'); assert.strictEqual(node2.event, traceEvents[5]); assert.lengthOf(node2.childEvents, 1); assert.strictEqual(node2.childEvents[0].name, 'LaterEvent'); }); it('should correct overlapping tasks', () => { addTaskEvents(0, 500, [ {name: 'MyCustomEvent'}, {name: 'OtherEvent'}, ]); addTaskEvents(400, 50, [ {name: 'OverlappingEvent'}, ]); assert.lengthOf(traceEvents, 5); const nodes = PageDependencyGraph.getCPUNodes(traceEvents); assert.lengthOf(nodes, 2); const node1 = nodes[0]; assert.strictEqual(node1.id, '1.0'); assert.strictEqual(node1.type, 'cpu'); assert.strictEqual(node1.event, traceEvents[0]); assert.lengthOf(node1.childEvents, 2); assert.strictEqual(node1.childEvents[0].name, 'MyCustomEvent'); assert.strictEqual(node1.childEvents[1].name, 'OtherEvent'); const node2 = nodes[1]; assert.strictEqual(node2.id, '1.400000'); assert.strictEqual(node2.type, 'cpu'); assert.strictEqual(node2.event, traceEvents[3]); assert.lengthOf(node2.childEvents, 1); assert.strictEqual(node2.childEvents[0].name, 'OverlappingEvent'); }); }); describe('#createGraph', () => { it('should compute a simple network graph', () => { const request1 = createRequest(1, 'https://example.com/', 0); const request2 = createRequest(2, 'https://example.com/page', 5); const request3 = createRequest(3, 'https://example.com/page2', 5); const request4 = createRequest(4, 'https://example.com/page3', 10, {url: 'https://example.com/page'}); const networkRequests = [request1, request2, request3, request4]; addTaskEvents(0, 0, []); const graph = PageDependencyGraph.createGraph(traceEvents, networkRequests, url); const nodes: Lantern.Graph.Node[] = []; graph.traverse(node => nodes.push(node)); assert.lengthOf(nodes, 4); assert.deepEqual(nodes.map(node => node.id), [1, 2, 3, 4].map(String)); assert.deepEqual(nodes[0].getDependencies(), []); assert.deepEqual(nodes[1].getDependencies(), [nodes[0]]); assert.deepEqual(nodes[2].getDependencies(), [nodes[0]]); assert.deepEqual(nodes[3].getDependencies(), [nodes[1]]); }); it('should compute a simple network and CPU graph', () => { const request1 = createRequest(1, 'https://example.com/', 0); const request2 = createRequest(2, 'https://example.com/page', 50); const request3 = createRequest(3, 'https://example.com/page2', 50); const request4 = createRequest(4, 'https://example.com/page3', 300, null, NetworkRequestTypes.XHR); const networkRequests = [request1, request2, request3, request4]; addTaskEvents(200, 200, [ {name: 'EvaluateScript', data: {url: 'https://example.com/page'}}, {name: 'ResourceSendRequest', data: {requestId: '4'}}, ]); addTaskEvents(700, 50, [ {name: 'InvalidateLayout', data: {stackTrace: [{url: 'https://example.com/page2'}]}}, {name: 'XHRReadyStateChange', data: {readyState: 4, url: 'https://example.com/page3'}}, ]); const graph = PageDependencyGraph.createGraph(traceEvents, networkRequests, url); const nodes: Lantern.Graph.Node[] = []; graph.traverse(node => nodes.push(node)); const getIds = (nodes: Lantern.Graph.Node[]): string[] => nodes.map(node => node.id); assert.lengthOf(nodes, 6); assert.deepEqual(getIds(nodes), ['1', '2', '3', '4', '1.200000', '1.700000']); assert.deepEqual(getDependencyIds(nodes[0]), []); assert.deepEqual(getDependencyIds(nodes[1]), ['1']); assert.deepEqual(getDependencyIds(nodes[2]), ['1']); assert.deepEqual(getDependencyIds(nodes[3]), ['1', '1.200000']); assert.deepEqual(getDependencyIds(nodes[4]), ['2']); assert.deepEqual(getDependencyIds(nodes[5]), ['3', '4']); }); it('should compute a network graph with duplicate URLs', () => { const request1 = createRequest(1, 'https://example.com/', 0); const request2 = createRequest(2, 'https://example.com/page', 5); const request3 = createRequest(3, 'https://example.com/page', 5); // duplicate URL const request4 = createRequest(4, 'https://example.com/page3', 10, {url: 'https://example.com/page'}); const networkRequests = [request1, request2, request3, request4]; addTaskEvents(0, 0, []); const graph = PageDependencyGraph.createGraph(traceEvents, networkRequests, url); const nodes: Lantern.Graph.Node[] = []; graph.traverse(node => nodes.push(node)); assert.lengthOf(nodes, 4); assert.deepEqual(nodes.map(node => node.id), [1, 2, 3, 4].map(String)); assert.deepEqual(nodes[0].getDependencies(), []); assert.deepEqual(nodes[1].getDependencies(), [nodes[0]]); assert.deepEqual(nodes[2].getDependencies(), [nodes[0]]); assert.deepEqual(nodes[3].getDependencies(), [nodes[0]]); // should depend on rootNode instead }); it('should be forgiving without cyclic dependencies', () => { const request1 = createRequest(1, 'https://example.com/', 0); const request2 = createRequest(2, 'https://example.com/page', 250, null, NetworkRequestTypes.XHR); const request3 = createRequest(3, 'https://example.com/page2', 210); const request4 = createRequest(4, 'https://example.com/page3', 590); const request5 = createRequest(5, 'https://example.com/page4', 595, null, NetworkRequestTypes.XHR); const networkRequests = [request1, request2, request3, request4, request5]; addTaskEvents(200, 200, [ // CPU 1.2 should depend on Network 1 {name: 'EvaluateScript', data: {url: 'https://example.com/'}}, // Network 2 should depend on CPU 1.2, but 1.2 should not depend on Network 1 {name: 'ResourceSendRequest', data: {requestId: '2'}}, {name: 'XHRReadyStateChange', data: {readyState: 4, url: 'https://example.com/page'}}, // CPU 1.2 should not depend on Network 3 because it starts after CPU 1.2 {name: 'EvaluateScript', data: {url: 'https://example.com/page2'}}, ]); addTaskEvents(600, 150, [ // CPU 1.6 should depend on Network 4 even though it ends at 410ms {name: 'InvalidateLayout', data: {stackTrace: [{url: 'https://example.com/page3'}]}}, // Network 5 should not depend on CPU 1.6 because it started before CPU 1.6 {name: 'ResourceSendRequest', data: {requestId: '5'}}, ]); const graph = PageDependencyGraph.createGraph(traceEvents, networkRequests, url); const nodes: Lantern.Graph.Node[] = []; graph.traverse(node => nodes.push(node)); assert.lengthOf(nodes, 7); assert.deepEqual(getDependencyIds(nodes[0]), []); assert.deepEqual(getDependencyIds(nodes[1]), ['1', '1.200000']); assert.deepEqual(getDependencyIds(nodes[2]), ['1']); assert.deepEqual(getDependencyIds(nodes[3]), ['1']); assert.deepEqual(getDependencyIds(nodes[4]), ['1']); assert.deepEqual(getDependencyIds(nodes[5]), ['1']); assert.deepEqual(getDependencyIds(nodes[6]), ['4']); }); it('should not install timer dependency on itself', () => { const request1 = createRequest(1, 'https://example.com/', 0); const networkRequests = [request1]; addTaskEvents(200, 200, [ // CPU 1.2 should depend on Network 1 {name: 'EvaluateScript', data: {url: 'https://example.com/'}}, // CPU 1.2 will install and fire it's own timer, but should not depend on itself {name: 'TimerInstall', data: {timerId: 'timer1'}}, {name: 'TimerFire', data: {timerId: 'timer1'}}, ]); const graph = PageDependencyGraph.createGraph(traceEvents, networkRequests, url); const nodes: Lantern.Graph.Node[] = []; graph.traverse(node => nodes.push(node)); assert.lengthOf(nodes, 2); assert.deepEqual(getDependencyIds(nodes[0]), []); assert.deepEqual(getDependencyIds(nodes[1]), ['1']); }); it('should prune short tasks', () => { const request0 = createRequest(0, 'https://example.com/page0', 0); const request1 = createRequest(1, 'https://example.com/', 100, null, NetworkRequestTypes.Script); const request2 = createRequest(2, 'https://example.com/page', 200, null, NetworkRequestTypes.XHR); const request3 = createRequest(3, 'https://example.com/page2', 300, null, NetworkRequestTypes.Script); const request4 = createRequest(4, 'https://example.com/page3', 400, null, NetworkRequestTypes.XHR); const networkRequests = [request0, request1, request2, request3, request4]; url = {requestedUrl: 'https://example.com/page0', mainDocumentUrl: 'https://example.com/page0'}; // Long task, should be kept in the output. addTaskEvents(120, 50, [ {name: 'EvaluateScript', data: {url: 'https://example.com/'}}, {name: 'ResourceSendRequest', data: {requestId: '2'}}, {name: 'XHRReadyStateChange', data: {readyState: 4, url: 'https://example.com/page'}}, ]); // Short task, should be pruned, but the 3->4 relationship should be retained addTaskEvents(350, 5, [ {name: 'EvaluateScript', data: {url: 'https://example.com/page2'}}, {name: 'ResourceSendRequest', data: {requestId: '4'}}, {name: 'XHRReadyStateChange', data: {readyState: 4, url: 'https://example.com/page3'}}, ]); const graph = PageDependencyGraph.createGraph(traceEvents, networkRequests, url); const nodes: Lantern.Graph.Node[] = []; graph.traverse(node => nodes.push(node)); assert.lengthOf(nodes, 6); assert.deepEqual(getDependencyIds(nodes[0]), []); assert.deepEqual(getDependencyIds(nodes[1]), ['0']); assert.deepEqual(getDependencyIds(nodes[2]), ['0', '1.120000']); assert.deepEqual(getDependencyIds(nodes[3]), ['0']); assert.deepEqual(getDependencyIds(nodes[4]), ['0', '3']); assert.strictEqual('1.120000', nodes[5].id); assert.deepEqual(getDependencyIds(nodes[5]), ['1']); }); it('should not prune highly-connected short tasks', () => { const request0 = createRequest(0, 'https://example.com/page0', 0); const request1 = { ...createRequest(1, 'https://example.com/', 100, null, NetworkRequestTypes.Document), documentURL: 'https://example.com/', frameId: 'frame1', }; const request2 = { ...createRequest(2, 'https://example.com/page', 200, null, NetworkRequestTypes.Script), documentURL: 'https://example.com/', frameId: 'frame1', }; const request3 = createRequest(3, 'https://example.com/page2', 300, null, NetworkRequestTypes.XHR); const request4 = createRequest(4, 'https://example.com/page3', 400, null, NetworkRequestTypes.XHR); const networkRequests = [request0, request1, request2, request3, request4]; url = {requestedUrl: 'https://example.com/page0', mainDocumentUrl: 'https://example.com/page0'}; // Short task, evaluates script (2) and sends two XHRs. addTaskEvents(220, 5, [ {name: 'EvaluateScript', data: {url: 'https://example.com/page', frame: 'frame1'}}, {name: 'ResourceSendRequest', data: {requestId: '3'}}, {name: 'XHRReadyStateChange', data: {readyState: 4, url: 'https://example.com/page2'}}, {name: 'ResourceSendRequest', data: {requestId: '4'}}, {name: 'XHRReadyStateChange', data: {readyState: 4, url: 'https://example.com/page3'}}, ]); const graph = PageDependencyGraph.createGraph(traceEvents, networkRequests, url); const nodes: Lantern.Graph.Node[] = []; graph.traverse(node => nodes.push(node)); assert.lengthOf(nodes, 6); assert.deepEqual(getDependencyIds(nodes[0]), []); assert.deepEqual(getDependencyIds(nodes[1]), ['0']); assert.deepEqual(getDependencyIds(nodes[2]), ['0']); assert.deepEqual(getDependencyIds(nodes[3]), ['0', '1.220000']); assert.deepEqual(getDependencyIds(nodes[4]), ['0', '1.220000']); assert.strictEqual('1.220000', nodes[5].id); assert.deepEqual(getDependencyIds(nodes[5]), ['1', '2']); }); it('should not prune short, first tasks of critical events', () => { const request0 = createRequest(0, 'https://example.com/page0', 0); const networkRequests = [request0]; url = {requestedUrl: 'https://example.com/page0', mainDocumentUrl: 'https://example.com/page0'}; const makeShortEvent = (firstEventName: string) => { const startTs = traceEvents.length * 100; addTaskEvents(startTs, 5, [ {name: firstEventName, data: {url: 'https://example.com/page0'}}, ]); }; const criticalEventNames = [ 'Paint', 'Layout', 'ParseHTML', ]; for (const eventName of criticalEventNames) { makeShortEvent(eventName); makeShortEvent(eventName); } const graph = PageDependencyGraph.createGraph(traceEvents, networkRequests, url); const cpuNodes: Lantern.Graph.CPUNode[] = []; graph.traverse(node => node.type === 'cpu' && cpuNodes.push(node)); expect(cpuNodes.map(node => { return { id: node.id, name: node.childEvents[0].name, }; })) .deep.equals([ { id: '1.0', name: 'Paint', }, { // ID jumps by 4 between each because each node has 2 CPU tasks and we skip the 2nd of each event type id: '1.400000', name: 'Layout', }, { id: '1.800000', name: 'ParseHTML', }, ]); }); it('should set isMainDocument on request with mainDocumentUrl', () => { const request1 = createRequest(1, 'https://example.com/', 0, null, NetworkRequestTypes.Other); const request2 = createRequest(2, 'https://example.com/page', 5, null, NetworkRequestTypes.Document); // Add in another unrelated + early request to make sure we pick the correct chain const request3 = createRequest(3, 'https://example.com/page2', 0, null, NetworkRequestTypes.Other); request2.redirects = [request1]; const networkRequests = [request1, request2, request3]; url = {requestedUrl: 'https://example.com/', mainDocumentUrl: 'https://example.com/page'}; addTaskEvents(0, 0, []); const graph = PageDependencyGraph.createGraph(traceEvents, networkRequests, url); const nodes: Lantern.Graph.Node[] = []; graph.traverse(node => nodes.push(node)); assert.lengthOf(nodes, 3); assert.strictEqual(nodes[0].id, '1'); assert.isFalse(nodes[0].isMainDocument()); assert.isTrue(nodes[1].isMainDocument()); assert.isFalse(nodes[2].isMainDocument()); }); it('should link up script initiators', () => { const request1 = createRequest(1, 'https://example.com/', 0); const request2 = createRequest(2, 'https://example.com/page', 5); const request3 = createRequest(3, 'https://example.com/page2', 5); const request4 = createRequest(4, 'https://example.com/page3', 20); // Set multiple initiator requests through script stack. request4.initiator = { type: 'script', stack: { callFrames: [{url: 'https://example.com/page'}], parent: {parent: {callFrames: [{url: 'https://example.com/page2'}]}}, } as unknown as Lantern.Types.CallStack, }; // Also set the initiatorRequest that Lighthouse's network-recorder.js creates. // This should be ignored and only used as a fallback. request4.initiatorRequest = request1; const networkRequests = [request1, request2, request3, request4]; addTaskEvents(0, 0, []); const graph = PageDependencyGraph.createGraph(traceEvents, networkRequests, url); const nodes: Lantern.Graph.Node[] = []; graph.traverse(node => nodes.push(node)); assert.lengthOf(nodes, 4); assert.deepEqual(nodes.map(node => node.id), [1, 2, 3, 4].map(String)); assert.deepEqual(nodes[0].getDependencies(), []); assert.deepEqual(nodes[1].getDependencies(), [nodes[0]]); assert.deepEqual(nodes[2].getDependencies(), [nodes[0]]); assert.deepEqual(nodes[3].getDependencies(), [nodes[1], nodes[2]]); }); it('should link up script initiators only when timing is valid', () => { const request1 = createRequest(1, 'https://example.com/', 0); const request2 = createRequest(2, 'https://example.com/page', 500); const request3 = createRequest(3, 'https://example.com/page2', 500); const request4 = createRequest(4, 'https://example.com/page3', 20); request4.initiator = { type: 'script', stack: { callFrames: [{url: 'https://example.com/page'}], parent: {parent: {callFrames: [{url: 'https://example.com/page2'}]}}, } as unknown as Lantern.Types.CallStack, }; const networkRequests = [request1, request2, request3, request4]; addTaskEvents(0, 0, []); const graph = PageDependencyGraph.createGraph(traceEvents, networkRequests, url); const nodes: Lantern.Graph.Node[] = []; graph.traverse(node => nodes.push(node)); assert.lengthOf(nodes, 4); assert.deepEqual(nodes.map(node => node.id), [1, 2, 3, 4].map(String)); assert.deepEqual(nodes[0].getDependencies(), []); assert.deepEqual(nodes[1].getDependencies(), [nodes[0]]); assert.deepEqual(nodes[2].getDependencies(), [nodes[0]]); assert.deepEqual(nodes[3].getDependencies(), [nodes[0]]); }); it('should link up script initiators with prefetch requests', () => { const request1 = createRequest(1, 'https://a.com/1', 0); const request2Prefetch = createRequest(2, 'https://a.com/js', 5); const request2Fetch = createRequest(3, 'https://a.com/js', 10); const request3 = createRequest(4, 'https://a.com/4', 20); // Set the initiator to an ambiguous URL (there are 2 requests for https://a.com/js) request3.initiator = { type: 'script', stack: {callFrames: [{url: 'https://a.com/js'}], parent: {parent: {callFrames: [{url: 'js'}]}}} as unknown as Lantern.Types.CallStack, }; // Set the initiatorRequest that it should fallback to. request3.initiatorRequest = request2Fetch; const networkRequests = [request1, request2Prefetch, request2Fetch, request3]; url = {requestedUrl: 'https://a.com/1', mainDocumentUrl: 'https://a.com/1'}; addTaskEvents(0, 0, []); const graph = PageDependencyGraph.createGraph(traceEvents, networkRequests, url); const nodes: Lantern.Graph.Node[] = []; graph.traverse(node => nodes.push(node)); assert.lengthOf(nodes, 4); assert.deepEqual(nodes.map(node => node.id), [1, 2, 3, 4].map(String)); assert.deepEqual(nodes[0].getDependencies(), []); assert.deepEqual(nodes[1].getDependencies(), [nodes[0]]); assert.deepEqual(nodes[2].getDependencies(), [nodes[0]]); assert.deepEqual(nodes[3].getDependencies(), [nodes[2]]); }); it('should not link up initiators with circular dependencies', () => { const rootRequest = createRequest(1, 'https://a.com', 0); // jsRequest1 initiated by jsRequest2 // *AND* // jsRequest2 initiated by jsRequest1 const jsRequest1 = createRequest(2, 'https://a.com/js1', 1, {url: 'https://a.com/js2'}); const jsRequest2 = createRequest(3, 'https://a.com/js2', 1, {url: 'https://a.com/js1'}); const networkRequests = [rootRequest, jsRequest1, jsRequest2]; url = {requestedUrl: 'https://a.com', mainDocumentUrl: 'https://a.com'}; addTaskEvents(0, 0, []); const graph = PageDependencyGraph.createGraph(traceEvents, networkRequests, url); const nodes: Lantern.Graph.Node[] = []; graph.traverse(node => nodes.push(node)); nodes.sort((a, b) => parseInt(a.id, 10) - parseInt(b.id, 10)); assert.lengthOf(nodes, 3); assert.deepEqual(nodes.map(node => node.id), [1, 2, 3].map(String)); assert.deepEqual(nodes[0].getDependencies(), []); // We don't know which of the initiators to trust in a cycle, so for now we // trust the earliest one (mostly because it's simplest). // In the wild so far we've only seen this for self-referential relationships. // If the evidence changes, then feel free to change these expectations :) assert.deepEqual(nodes[1].getDependencies(), [nodes[2]]); assert.deepEqual(nodes[2].getDependencies(), [nodes[0]]); }); it('should not link up initiatorRequests with circular dependencies', () => { const rootRequest = createRequest(1, 'https://a.com', 0); // jsRequest1 initiated by jsRequest2 // *AND* // jsRequest2 initiated by jsRequest1 const jsRequest1 = createRequest(2, 'https://a.com/js1', 1); const jsRequest2 = createRequest(3, 'https://a.com/js2', 1); jsRequest1.initiatorRequest = jsRequest2; jsRequest2.initiatorRequest = jsRequest1; const networkRequests = [rootRequest, jsRequest1, jsRequest2]; url = {requestedUrl: 'https://a.com', mainDocumentUrl: 'https://a.com'}; addTaskEvents(0, 0, []); const graph = PageDependencyGraph.createGraph(traceEvents, networkRequests, url); const nodes: Lantern.Graph.Node[] = []; graph.traverse(node => nodes.push(node)); nodes.sort((a, b) => parseInt(a.id, 10) - parseInt(b.id, 10)); assert.lengthOf(nodes, 3); assert.deepEqual(nodes.map(node => node.id), [1, 2, 3].map(String)); assert.deepEqual(nodes[0].getDependencies(), []); assert.deepEqual(nodes[1].getDependencies(), [nodes[2]]); assert.deepEqual(nodes[2].getDependencies(), [nodes[0]]); }); it('should find root if it is not the first node', () => { const request1 = createRequest(1, 'https://example.com/', 0, null, NetworkRequestTypes.Other); const request2 = createRequest(2, 'https://example.com/page', 5, null, NetworkRequestTypes.Document); const networkRequests = [request1, request2]; url = {requestedUrl: 'https://example.com/page', mainDocumentUrl: 'https://example.com/page'}; // Evaluated before root request. addTaskEvents(0.1, 50, [ {name: 'EvaluateScript'}, ]); const graph = PageDependencyGraph.createGraph(traceEvents, networkRequests, url); const nodes: Lantern.Graph.Node[] = []; graph.traverse(node => nodes.push(node)); assert.lengthOf(nodes, 1); assert.deepEqual(nodes.map(node => node.id), ['2']); assert.deepEqual(nodes[0].getDependencies(), []); assert.deepEqual(nodes[0].getDependents(), []); }); }); });