chrome-devtools-frontend
Version:
Chrome DevTools UI
671 lines (569 loc) • 29.3 kB
text/typescript
// 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(), []);
});
});
});