Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

core(uses-preload): prevent infinite loop #5184

Merged
merged 3 commits into from
May 12, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 5 additions & 10 deletions lighthouse-core/audits/uses-rel-preload.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,18 +62,16 @@ class UsesRelPreloadAudit extends Audit {
* @param {Set<string>} urls The array of byte savings results per resource
* @param {LH.Gatherer.Simulation.GraphNode} graph
* @param {LH.Gatherer.Simulation.Simulator} simulator
* @param {LH.WebInspector.NetworkRequest} mainResource
* @return {{wastedMs: number, results: Array<{url: string, wastedMs: number}>}}
*/
static computeWasteWithGraph(urls, graph, simulator, mainResource) {
static computeWasteWithGraph(urls, graph, simulator) {
if (!urls.size) {
return {wastedMs: 0, results: []};
}

// Preload changes the ordering of requests, simulate the original graph with flexible ordering
// to have a reasonable baseline for comparison.
const simulationBeforeChanges = simulator.simulate(graph, {flexibleOrdering: true});

const modifiedGraph = graph.cloneWithRelationships();

/** @type {Array<LH.Gatherer.Simulation.GraphNetworkNode>} */
Expand All @@ -84,12 +82,10 @@ class UsesRelPreloadAudit extends Audit {
if (node.type !== 'network') return;

const networkNode = /** @type {LH.Gatherer.Simulation.GraphNetworkNode} */ (node);
if (networkNode.record && urls.has(networkNode.record.url)) {
nodesToPreload.push(networkNode);
}

if (networkNode.record && networkNode.record.url === mainResource.url) {
if (node.isMainDocument()) {
mainDocumentNode = networkNode;
} else if (networkNode.record && urls.has(networkNode.record.url)) {
nodesToPreload.push(networkNode);
}
});

Expand Down Expand Up @@ -167,8 +163,7 @@ class UsesRelPreloadAudit extends Audit {
}
}

const {results, wastedMs} = UsesRelPreloadAudit.computeWasteWithGraph(urls, graph, simulator,
mainResource);
const {results, wastedMs} = UsesRelPreloadAudit.computeWasteWithGraph(urls, graph, simulator);
// sort results by wastedTime DESC
results.sort((a, b) => b.wastedMs - a.wastedMs);

Expand Down
4 changes: 3 additions & 1 deletion lighthouse-core/lib/dependency-graph/network-node.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,9 @@ class NetworkNode extends Node {
* @return {NetworkNode}
*/
cloneWithoutRelationships() {
return new NetworkNode(this._record);
const node = new NetworkNode(this._record);
node.setIsMainDocument(this._isMainDocument);
return node;
}
}

Expand Down
23 changes: 17 additions & 6 deletions lighthouse-core/lib/dependency-graph/node.js
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,9 @@ class Node {
* @return {Node}
*/
cloneWithoutRelationships() {
return new Node(this.id);
const node = new Node(this.id);
node.setIsMainDocument(this._isMainDocument);
return node;
}

/**
Expand Down Expand Up @@ -256,9 +258,15 @@ class Node {
/**
* Returns whether the given node has a cycle in its dependent graph by performing a DFS.
* @param {Node} node
* @param {'dependents'|'dependencies'|'both'} [direction]
* @return {boolean}
*/
static hasCycle(node) {
static hasCycle(node, direction = 'both') {
// Checking 'both' is the default entrypoint to recursively check both directions
if (direction === 'both') {
return Node.hasCycle(node, 'dependents') || Node.hasCycle(node, 'dependencies');
}

const visited = new Set();
/** @type {Node[]} */
const currentPath = [];
Expand Down Expand Up @@ -286,10 +294,13 @@ class Node {
currentPath.push(currentNode);

// Add all of its dependents to our toVisit stack
for (const dependent of currentNode._dependents) {
if (toVisit.includes(dependent)) continue;
toVisit.push(dependent);
depthAdded.set(dependent, currentPath.length);
const nodesToExplore = direction === 'dependents' ?
currentNode._dependents :
currentNode._dependencies;
for (const nextNode of nodesToExplore) {
if (toVisit.includes(nextNode)) continue;
toVisit.push(nextNode);
depthAdded.set(nextNode, currentPath.length);
}
}

Expand Down
5 changes: 4 additions & 1 deletion lighthouse-core/lib/dependency-graph/simulator/simulator.js
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,10 @@ class Simulator {
* @return {LH.Gatherer.Simulation.Result}
*/
simulate(graph, options) {
if (Node.hasCycle(graph)) {
throw new Error('Cannot simulate graph with cycle');
}

options = Object.assign({flexibleOrdering: false}, options);
// initialize the necessary data containers
this._flexibleOrdering = options.flexibleOrdering;
Expand All @@ -327,7 +331,6 @@ class Simulator {

const rootNode = graph.getRootNode();
rootNode.traverse(node => nodesNotReadyToStart.add(node));

let totalElapsedTime = 0;
let iteration = 0;

Expand Down
3 changes: 2 additions & 1 deletion lighthouse-core/test/audits/uses-rel-preload-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ describe('Performance: uses-rel-preload audit', () => {
const scriptNode = buildNode(3, 'http://www.example.com/script.js');
const scriptAddedNode = buildNode(4, 'http://www.example.com/script-added.js');

mainDocumentNode.setIsMainDocument(true);
mainDocumentNode.addDependency(rootNode);
scriptNode.addDependency(mainDocumentNode);
scriptAddedNode.addDependency(scriptNode);
Expand Down Expand Up @@ -195,7 +196,7 @@ describe('Performance: uses-rel-preload audit', () => {
});
});

it('does no throw on a real trace/devtools log', async () => {
it('does not throw on a real trace/devtools log', async () => {
const artifacts = Object.assign({
URL: {finalUrl: 'https://pwa.rocks/'},
traces: {
Expand Down
34 changes: 34 additions & 0 deletions lighthouse-core/test/lib/dependency-graph/node-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
'use strict';

const Node = require('../../../lib/dependency-graph/node');
const NetworkNode = require('../../../lib/dependency-graph/network-node');

const assert = require('assert');

Expand Down Expand Up @@ -103,6 +104,16 @@ describe('DependencyGraph/Node', () => {
assert.notEqual(node, clone);
assert.equal(clone.getDependencies().length, 0);
});

it('should copy isMainDocument', () => {
const node = new Node(1);
node.setIsMainDocument(true);
const networkNode = new NetworkNode({});
networkNode.setIsMainDocument(true);

assert.ok(node.cloneWithoutRelationships().isMainDocument());
assert.ok(networkNode.cloneWithoutRelationships().isMainDocument());
});
});

describe('.cloneWithRelationships', () => {
Expand Down Expand Up @@ -241,6 +252,7 @@ describe('DependencyGraph/Node', () => {
});

it('should return true for basic cycles', () => {
// A - B - C - A!
const nodeA = new Node('A');
const nodeB = new Node('B');
const nodeC = new Node('C');
Expand All @@ -252,6 +264,21 @@ describe('DependencyGraph/Node', () => {
assert.equal(Node.hasCycle(nodeA), true);
});

it('should return true for children', () => {
// A!
// /
// A - B - C
const nodeA = new Node('A');
const nodeB = new Node('B');
const nodeC = new Node('C');

nodeA.addDependent(nodeB);
nodeB.addDependent(nodeC);
nodeB.addDependent(nodeA);

assert.equal(Node.hasCycle(nodeC), true);
});

it('should return true for complex cycles', () => {
// B - D - F - G - C!
// / /
Expand All @@ -276,6 +303,13 @@ describe('DependencyGraph/Node', () => {
nodeG.addDependent(nodeC);

assert.equal(Node.hasCycle(nodeA), true);
assert.equal(Node.hasCycle(nodeB), true);
assert.equal(Node.hasCycle(nodeC), true);
assert.equal(Node.hasCycle(nodeD), true);
assert.equal(Node.hasCycle(nodeE), true);
assert.equal(Node.hasCycle(nodeF), true);
assert.equal(Node.hasCycle(nodeG), true);
assert.equal(Node.hasCycle(nodeH), true);
});

it('works for very large graphs', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -212,5 +212,15 @@ describe('DependencyGraph/Simulator', () => {
// should be 800ms for E and 800ms for F/G
assert.equal(resultB.timeInMs, 800 + 800);
});

it('should throw (not hang) on graphs with cycles', () => {
const rootNode = new NetworkNode(request({}));
const depNode = new NetworkNode(request({}));
rootNode.addDependency(depNode);
depNode.addDependency(rootNode);

const simulator = new Simulator({serverResponseTimeByOrigin});
assert.throws(() => simulator.simulate(rootNode), /cycle/);
});
});
});