From 11085753f6eaf943c3919862a5783e97396a63c2 Mon Sep 17 00:00:00 2001 From: Ward Peeters Date: Fri, 12 May 2017 06:54:32 +0200 Subject: [PATCH] Change critical request gatherer --- .../closure/typedefs/ComputedArtifacts.js | 3 + .../computed/critical-request-chains.js | 138 ++---- .../gather/computed/critical-requests.js | 111 +++++ .../computed/critical-request-chains-test.js | 454 +++++++----------- .../gather/computed/critical-requests-test.js | 210 ++++++++ 5 files changed, 532 insertions(+), 384 deletions(-) create mode 100644 lighthouse-core/gather/computed/critical-requests.js create mode 100644 lighthouse-core/test/gather/computed/critical-requests-test.js diff --git a/lighthouse-core/closure/typedefs/ComputedArtifacts.js b/lighthouse-core/closure/typedefs/ComputedArtifacts.js index 7ac4661c7f1e..30bcf361f0b3 100644 --- a/lighthouse-core/closure/typedefs/ComputedArtifacts.js +++ b/lighthouse-core/closure/typedefs/ComputedArtifacts.js @@ -37,6 +37,9 @@ let TraceOfTabArtifact; */ function ComputedArtifacts() {} +/** @type {function(!Array): !Promise} */ +ComputedArtifacts.prototype.requestCriticalRequests; + /** @type {function(!Array): !Promise} */ ComputedArtifacts.prototype.requestCriticalRequestChains; diff --git a/lighthouse-core/gather/computed/critical-request-chains.js b/lighthouse-core/gather/computed/critical-request-chains.js index 56799216afaf..00d3d634ee67 100644 --- a/lighthouse-core/gather/computed/critical-request-chains.js +++ b/lighthouse-core/gather/computed/critical-request-chains.js @@ -18,7 +18,6 @@ 'use strict'; const ComputedArtifact = require('./computed-artifact'); -const WebInspector = require('../../lib/web-inspector'); class CriticalRequestChains extends ComputedArtifact { @@ -26,115 +25,50 @@ class CriticalRequestChains extends ComputedArtifact { return 'CriticalRequestChains'; } - /** - * For now, we use network priorities as a proxy for "render-blocking"/critical-ness. - * It's imperfect, but there is not a higher-fidelity signal available yet. - * @see https://docs.google.com/document/d/1bCDuq9H1ih9iNjgzyAL0gpwNFiEP4TZS-YLRp_RuMlc - * @param {any} request - */ - isCritical(request) { - const resourceTypeCategory = request._resourceType && request._resourceType._category; - - // XHRs are fetched at High priority, but we exclude them, as they are unlikely to be critical - // Images are also non-critical. - // Treat any images missed by category, primarily favicons, as non-critical resources - const nonCriticalResourceTypes = [ - WebInspector.resourceTypes.Image._category, - WebInspector.resourceTypes.XHR._category - ]; - if (nonCriticalResourceTypes.includes(resourceTypeCategory) || - request.mimeType && request.mimeType.startsWith('image/')) { - return false; - } - - return ['VeryHigh', 'High', 'Medium'].includes(request.priority()); - } - - compute_(networkRecords) { - networkRecords = networkRecords.filter(req => req.finished); - - // Build a map of requestID -> Node. - const requestIdToRequests = new Map(); - for (const request of networkRecords) { - requestIdToRequests.set(request.requestId, request); - } - - // Get all the critical requests. - /** @type {!Array} */ - const criticalRequests = networkRecords.filter(req => this.isCritical(req)); - - const flattenRequest = request => { - return { + generateChain(request) { + return { + request: { + id: request.id, url: request._url, startTime: request.startTime, endTime: request.endTime, responseReceivedTime: request.responseReceivedTime, - transferSize: request.transferSize - }; + transferSize: request.transferSize, + }, + children: {}, }; + } - // Create a tree of critical requests. - const criticalRequestChains = {}; - for (const request of criticalRequests) { - // Work back from this request up to the root. If by some weird quirk we are giving request D - // here, which has ancestors C, B and A (where A is the root), we will build array [C, B, A] - // during this phase. - const ancestors = []; - let ancestorRequest = request.initiatorRequest(); - let node = criticalRequestChains; - while (ancestorRequest) { - const ancestorIsCritical = this.isCritical(ancestorRequest); - - // If the parent request isn't a high priority request it won't be in the - // requestIdToRequests map, and so we can break the chain here. We should also - // break it if we've seen this request before because this is some kind of circular - // reference, and that's bad. - if (!ancestorIsCritical || ancestors.includes(ancestorRequest.requestId)) { - // Set the ancestors to an empty array and unset node so that we don't add - // the request in to the tree. - ancestors.length = 0; - node = undefined; - break; - } - ancestors.push(ancestorRequest.requestId); - ancestorRequest = ancestorRequest.initiatorRequest(); - } - - // With the above array we can work from back to front, i.e. A, B, C, and during this process - // we can build out the tree for any nodes that have yet to be created. - let ancestor = ancestors.pop(); - while (ancestor) { - const parentRequest = requestIdToRequests.get(ancestor); - const parentRequestId = parentRequest.requestId; - if (!node[parentRequestId]) { - node[parentRequestId] = { - request: flattenRequest(parentRequest), - children: {} - }; + compute_(networkRecords, artifacts) { + return artifacts.requestCriticalRequests(networkRecords) + .then(criticalRequests => { + // Create a tree of critical requests. + const criticalRequestChains = {}; + const mappedRequests = {}; + + let request = criticalRequests.shift(); + while(request) { + if (!mappedRequests[request.id]) { + mappedRequests[request.id] = this.generateChain(request); + } + + const node = mappedRequests[request.id]; + const parent = request.parent; + if (parent) { + if (!mappedRequests[parent.id]) { + mappedRequests[parent.id] = this.generateChain(parent); + } + + mappedRequests[parent.id].children[request.id] = node; + } else { + criticalRequestChains[request.id] = node; + } + + request = criticalRequests.shift(); } - // Step to the next iteration. - ancestor = ancestors.pop(); - node = node[parentRequestId].children; - } - - if (!node) { - continue; - } - - // If the node already exists, bail. - if (node[request.requestId]) { - continue; - } - - // node should now point to the immediate parent for this request. - node[request.requestId] = { - request: flattenRequest(request), - children: {} - }; - } - - return criticalRequestChains; + return criticalRequestChains; + }); } } diff --git a/lighthouse-core/gather/computed/critical-requests.js b/lighthouse-core/gather/computed/critical-requests.js new file mode 100644 index 000000000000..005a198a5662 --- /dev/null +++ b/lighthouse-core/gather/computed/critical-requests.js @@ -0,0 +1,111 @@ +/** + * @license + * Copyright 2016 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +const ComputedArtifact = require('./computed-artifact'); +const WebInspector = require('../../lib/web-inspector'); + +class CriticalRequests extends ComputedArtifact { + + get name() { + return 'CriticalRequests'; + } + + /** + * For now, we use network priorities as a proxy for "render-blocking"/critical-ness. + * It's imperfect, but there is not a higher-fidelity signal available yet. + * @see https://docs.google.com/document/d/1bCDuq9H1ih9iNjgzyAL0gpwNFiEP4TZS-YLRp_RuMlc + * @param {any} request + */ + isCritical(request) { + const resourceTypeCategory = request._resourceType && request._resourceType._category; + + // XHRs are fetched at High priority, but we exclude them, as they are unlikely to be critical + // Images are also non-critical. + // Treat any images missed by category, primarily favicons, as non-critical resources + const nonCriticalResourceTypes = [ + WebInspector.resourceTypes.Image._category, + WebInspector.resourceTypes.XHR._category + ]; + if (nonCriticalResourceTypes.includes(resourceTypeCategory) || + request.mimeType && request.mimeType.startsWith('image/')) { + return false; + } + + return ['VeryHigh', 'High', 'Medium'].includes(request.priority()); + } + + flattenRequest(record) { + const ancestor = record.initiatorRequest(); + + return { + id: record._requestId, + url: record._url, + startTime: record.startTime, + endTime: record.endTime, + responseReceivedTime: record.responseReceivedTime, + transferSize: record.transferSize, + parent: ancestor ? this.flattenRequest(ancestor) : null, + }; + } + + compute_(networkRecords) { + // Get all the critical requests. + /** @type {!Array} */ + const criticalRequests = networkRecords.filter(req => this.isCritical(req)); + const requestIds = []; + + // Create a tree of critical requests. + const flattenedRequests = []; + for (const request of criticalRequests) { + // Work back from this request up to the root. If by some weird quirk we are giving request D + // here, which has ancestors C, B and A (where A is the root), we will build array [C, B, A] + // during this phase. + const ancestors = []; + let ancestorRequest = request.initiatorRequest(); + while (ancestorRequest) { + const ancestorIsCritical = this.isCritical(ancestorRequest); + + // If the parent request isn't a high priority request it won't be in the + // requestIdToRequests map, and so we can break the chain here. We should also + // break it if we've seen this request before because this is some kind of circular + // reference, and that's bad. + if (!ancestorIsCritical || ancestors.includes(ancestorRequest._requestId)) { + // Set the ancestors to an empty array and unset node so that we don't add + // the request in to the tree. + ancestors.length = 0; + break; + } + + ancestors.push(ancestorRequest._requestId); + ancestorRequest = ancestorRequest.initiatorRequest(); + } + + const isAlreadyLogged = requestIds.indexOf(request._requestId) === -1; + const isHighPriorityChain = !request.initiatorRequest() || ancestors.length; + if (isAlreadyLogged && isHighPriorityChain) { + flattenedRequests.push(this.flattenRequest(request)); + requestIds.push(request._requestId); + } + } + + return flattenedRequests; + } +} + +module.exports = CriticalRequests; diff --git a/lighthouse-core/test/gather/computed/critical-request-chains-test.js b/lighthouse-core/test/gather/computed/critical-request-chains-test.js index c4f95898b2e3..242cf9bdcc96 100644 --- a/lighthouse-core/test/gather/computed/critical-request-chains-test.js +++ b/lighthouse-core/test/gather/computed/critical-request-chains-test.js @@ -19,323 +19,213 @@ const GathererClass = require('../../../gather/computed/critical-request-chains'); const assert = require('assert'); -const Gatherer = new GathererClass(); -const HIGH = 'High'; -const VERY_HIGH = 'VeryHigh'; -const MEDIUM = 'Medium'; -const LOW = 'Low'; -const VERY_LOW = 'VeryLow'; - -function mockTracingData(prioritiesList, edges) { - const networkRecords = prioritiesList.map((priority, index) => - ({requestId: index.toString(), - _resourceType: { - _category: 'fake' - }, - finished: true, - priority: () => priority, - initiatorRequest: () => null - })); - - // add mock initiator information - edges.forEach(edge => { - const initiator = networkRecords[edge[0]]; - networkRecords[edge[1]].initiatorRequest = () => initiator; - }); - - return networkRecords; -} +function testGetCriticalRequestsChains(criticalRequests, expected) { + const artifacts = { + requestCriticalRequests: () => Promise.resolve(criticalRequests), + }; -function testGetCriticalChain(data) { - const networkRecords = mockTracingData(data.priorityList, data.edges); - return Gatherer.request(networkRecords).then(criticalChains => { - assert.deepEqual(criticalChains, data.expected); + const Gatherer = new GathererClass(artifacts); + return Gatherer.request([]).then(criticalRequests => { + assert.deepEqual(criticalRequests, expected); }); } -function constructEmptyRequest() { - return { +let requests; +function constructEmptyRequest(id = null) { + const request = { + id, endTime: undefined, responseReceivedTime: undefined, startTime: undefined, url: undefined, transferSize: undefined, }; -} - -describe('CriticalRequestChain gatherer: getCriticalChain function', () => { - it('returns correct data for chain of four critical requests', () => - testGetCriticalChain({ - priorityList: [HIGH, MEDIUM, VERY_HIGH, HIGH], - edges: [[0, 1], [1, 2], [2, 3]], - expected: { - 0: { - request: constructEmptyRequest(), - children: { - 1: { - request: constructEmptyRequest(), - children: { - 2: { - request: constructEmptyRequest(), - children: { - 3: { - request: constructEmptyRequest(), - children: {} - } - } - } - } - } - } - } - } - })); - it('returns correct data for chain interleaved with non-critical requests', - () => testGetCriticalChain({ - priorityList: [MEDIUM, HIGH, LOW, MEDIUM, HIGH, VERY_LOW], - edges: [[0, 1], [1, 2], [2, 3], [3, 4]], - expected: { - 0: { - request: constructEmptyRequest(), - children: { - 1: { - request: constructEmptyRequest(), - children: {} - } - } - } - } - })); + requests[id] = request; - it('returns correct data for two parallel chains', () => - testGetCriticalChain({ - priorityList: [HIGH, HIGH, HIGH, HIGH], - edges: [[0, 2], [1, 3]], - expected: { - 0: { - request: constructEmptyRequest(), - children: { - 2: { - request: constructEmptyRequest(), - children: {} - } - } - }, - 1: { - request: constructEmptyRequest(), - children: { - 3: { - request: constructEmptyRequest(), - children: {} - } - } - } - } - })); + return request; +} - it('returns correct data for fork at root', () => - testGetCriticalChain({ - priorityList: [HIGH, HIGH, HIGH], - edges: [[0, 1], [0, 2]], - expected: { - 0: { - request: constructEmptyRequest(), - children: { - 1: { - request: constructEmptyRequest(), - children: {} - }, - 2: { - request: constructEmptyRequest(), - children: {} - } - } - } - } - })); +describe('CriticalRequest gatherer: getCriticalRequests function', () => { + beforeEach(() => { + requests = {}; + }); - it('returns correct data for fork at non root', () => - testGetCriticalChain({ - priorityList: [HIGH, HIGH, HIGH, HIGH], - edges: [[0, 1], [1, 2], [1, 3]], - expected: { - 0: { - request: constructEmptyRequest(), - children: { - 1: { - request: constructEmptyRequest(), - children: { - 2: { - request: constructEmptyRequest(), - children: {} - }, - 3: { - request: constructEmptyRequest(), - children: {} + it('returns correct data for four critical requests', () => { + const criticalRequests = [ + constructEmptyRequest('0'), + constructEmptyRequest('1'), + constructEmptyRequest('2'), + constructEmptyRequest('3'), + ]; + + const expected = { + '0': { + request: Object.assign({}, criticalRequests[0]), + children: { + '1': { + request: Object.assign({}, criticalRequests[1]), + children: { + '2': { + request: Object.assign({}, criticalRequests[2]), + children: { + '3': { + request: Object.assign({}, criticalRequests[3]), + children: {}, + } } } } } } } - })); + }; - it('returns empty chain list when no critical request', () => - testGetCriticalChain({ - priorityList: [LOW, LOW], - edges: [[0, 1]], - expected: {} - })); + criticalRequests[0].parent = null; + criticalRequests[1].parent = criticalRequests[0]; + criticalRequests[2].parent = criticalRequests[1]; + criticalRequests[3].parent = criticalRequests[2]; - it('returns empty chain list when no request whatsoever', () => - testGetCriticalChain({ - priorityList: [], - edges: [], - expected: {} - })); + return testGetCriticalRequestsChains(criticalRequests, expected); + }); - it('returns two single node chains for two independent requests', () => - testGetCriticalChain({ - priorityList: [HIGH, HIGH], - edges: [], - expected: { - 0: { - request: constructEmptyRequest(), - children: {} + it('returns correct data for two parallel chains', () => { + const criticalRequests = [ + constructEmptyRequest('0'), + constructEmptyRequest('1'), + constructEmptyRequest('2'), + constructEmptyRequest('3'), + ]; + + const expected = { + '0': { + request: Object.assign({}, criticalRequests[0]), + children: { + '2': { + request: Object.assign({}, criticalRequests[2]), + children: {}, + }, }, - 1: { - request: constructEmptyRequest(), - children: {} - } - } - })); + }, + '1': { + request: Object.assign({}, criticalRequests[1]), + children: { + '3': { + request: Object.assign({}, criticalRequests[3]), + children: {}, + }, + }, + }, + }; + + criticalRequests[0].parent = null; + criticalRequests[1].parent = null; + criticalRequests[2].parent = criticalRequests[0]; + criticalRequests[3].parent = criticalRequests[1]; + + return testGetCriticalRequestsChains(criticalRequests, expected); + }); - it('returns correct data on a random big graph', () => - testGetCriticalChain({ - priorityList: Array(9).fill(HIGH), - edges: [[0, 1], [1, 2], [1, 3], [4, 5], [5, 7], [7, 8], [5, 6]], - expected: { - 0: { - request: constructEmptyRequest(), - children: { - 1: { - request: constructEmptyRequest(), - children: { - 2: { - request: constructEmptyRequest(), - children: {} + it('returns empty list when no request whatsoever', () => + testGetCriticalRequestsChains([], {}) + ); + + + it('returns correct data on a random big graph', () => { + const criticalRequests = [ + constructEmptyRequest('0'), + constructEmptyRequest('1'), + constructEmptyRequest('2'), + constructEmptyRequest('3'), + constructEmptyRequest('4'), + constructEmptyRequest('5'), + constructEmptyRequest('6'), + constructEmptyRequest('7'), + ]; + + const expected = { + '0': { + request: Object.assign({}, criticalRequests[0]), + children: { + '1': { + request: Object.assign({}, criticalRequests[1]), + children: { + '2': { + request: Object.assign({}, criticalRequests[2]), + children: { + '3': { + request: Object.assign({}, criticalRequests[3]), + children: {}, + }, }, - 3: { - request: constructEmptyRequest(), - children: {} - } - } - } - } + }, + }, + }, }, - 4: { - request: constructEmptyRequest(), - children: { - 5: { - request: constructEmptyRequest(), - children: { - 7: { - request: constructEmptyRequest(), - children: { - 8: { - request: constructEmptyRequest(), - children: {} - } - } + }, + '4': { + request: Object.assign({}, criticalRequests[4]), + children: { + '5': { + request: Object.assign({}, criticalRequests[5]), + children: { + '6': { + request: Object.assign({}, criticalRequests[6]), + children: { + '7': { + request: Object.assign({}, criticalRequests[7]), + children: {}, + }, }, - 6: { - request: constructEmptyRequest(), - children: {} - } - } - } - } - } - } - })); + }, + }, + }, + }, + }, + }; - it('handles redirects', () => { - const networkRecords = mockTracingData([HIGH, HIGH, HIGH], [[0, 1], [1, 2]]); + criticalRequests[0].parent = null; + criticalRequests[1].parent = criticalRequests[0]; + criticalRequests[2].parent = criticalRequests[1]; + criticalRequests[3].parent = criticalRequests[2]; + criticalRequests[4].parent = null; + criticalRequests[5].parent = criticalRequests[4]; + criticalRequests[6].parent = criticalRequests[5]; + criticalRequests[7].parent = criticalRequests[6]; - // Make a fake redirect - networkRecords[1].requestId = '1:redirected.0'; - networkRecords[2].requestId = '1'; - return Gatherer.request(networkRecords).then(criticalChains => { - assert.deepEqual(criticalChains, { - 0: { - request: constructEmptyRequest(), - children: { - '1:redirected.0': { - request: constructEmptyRequest(), - children: { - 1: { - request: constructEmptyRequest(), - children: {} - } - } - } - } - } - }); - }); + return testGetCriticalRequestsChains(criticalRequests, expected); }); - it('discards favicons as non-critical', () => { - const networkRecords = mockTracingData([HIGH, HIGH, HIGH, HIGH], [[0, 1], [0, 2], [0, 3]]); - - // 2nd record is a favicon - networkRecords[1].url = 'https://example.com/favicon.ico'; - networkRecords[1].mimeType = 'image/x-icon'; - networkRecords[1].parsedURL = { - lastPathComponent: 'favicon.ico' - }; - // 3rd record is a favicon - networkRecords[2].url = 'https://example.com/favicon-32x32.png'; - networkRecords[2].mimeType = 'image/png'; - networkRecords[2].parsedURL = { - lastPathComponent: 'favicon-32x32.png' - }; - // 4th record is a favicon - networkRecords[3].url = 'https://example.com/android-chrome-192x192.png'; - networkRecords[3].mimeType = 'image/png'; - networkRecords[3].parsedURL = { - lastPathComponent: 'android-chrome-192x192.png' + it('handles redirects', () => { + const criticalRequests = [ + constructEmptyRequest('0'), + constructEmptyRequest('1:redirected.0'), + constructEmptyRequest('1'), + ]; + + const expected = { + '0': { + request: Object.assign({}, criticalRequests[0]), + children: { + '1:redirected.0': { + request: Object.assign({}, criticalRequests[1]), + children: { + '1': { + request: Object.assign({}, criticalRequests[2]), + children: { + }, + }, + }, + }, + }, + }, }; - return Gatherer.request(networkRecords).then(criticalChains => { - assert.deepEqual(criticalChains, { - 0: { - request: constructEmptyRequest(), - children: {} - } - }); - }); - }); + criticalRequests[0].parent = null; + criticalRequests[1].parent = criticalRequests[0]; + criticalRequests[2].parent = criticalRequests[1]; - it('handles non-existent nodes when building the tree', () => { - const networkRecords = mockTracingData([HIGH, HIGH], [[0, 1]]); - - // Reverse the records so we force nodes to be made early. - networkRecords.reverse(); - return Gatherer.request(networkRecords).then(criticalChains => { - assert.deepEqual(criticalChains, { - 0: { - request: constructEmptyRequest(), - children: { - 1: { - request: constructEmptyRequest(), - children: {} - } - } - } - }); - }); + return testGetCriticalRequestsChains(criticalRequests, expected); }); }); diff --git a/lighthouse-core/test/gather/computed/critical-requests-test.js b/lighthouse-core/test/gather/computed/critical-requests-test.js new file mode 100644 index 000000000000..c085bb233ffc --- /dev/null +++ b/lighthouse-core/test/gather/computed/critical-requests-test.js @@ -0,0 +1,210 @@ +/** + * Copyright 2016 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +'use strict'; + +/* eslint-env mocha */ + +const GathererClass = require('../../../gather/computed/critical-requests'); +const assert = require('assert'); +const Gatherer = new GathererClass(); + +const HIGH = 'High'; +const VERY_HIGH = 'VeryHigh'; +const MEDIUM = 'Medium'; +const LOW = 'Low'; +const VERY_LOW = 'VeryLow'; + +function mockTracingData(prioritiesList, edges) { + const networkRecords = prioritiesList.map((priority, index) => + ({_requestId: index.toString(), + _resourceType: { + _category: 'fake' + }, + priority: () => priority, + initiatorRequest: () => null + })); + + // add mock initiator information + edges.forEach(edge => { + const initiator = networkRecords[edge[0]]; + networkRecords[edge[1]].initiatorRequest = () => initiator; + }); + + return networkRecords; +} + +function testGetCriticalRequests(data) { + const networkRecords = mockTracingData(data.priorityList, data.edges); + return Gatherer.request(networkRecords).then(criticalRequests => { + assert.deepEqual(criticalRequests, data.expected); + }); +} + +let requests; +function constructEmptyRequest(parent = null, id = null) { + const request = { + id, + parent: requests[parent] || null, + endTime: undefined, + responseReceivedTime: undefined, + startTime: undefined, + url: undefined, + transferSize: undefined + }; + + requests[id] = request; + + return request; +} + +describe('CriticalRequest gatherer: getCriticalRequests function', () => { + beforeEach(() => { + requests = {}; + }); + + it('returns correct data for four critical requests', () => + testGetCriticalRequests({ + priorityList: [HIGH, MEDIUM, VERY_HIGH, HIGH], + edges: [[0, 1], [1, 2], [2, 3]], + expected: [ + constructEmptyRequest(null, '0'), + constructEmptyRequest('0', '1'), + constructEmptyRequest('1', '2'), + constructEmptyRequest('2', '3'), + ] + })); + + it('returns correct data for chain interleaved with non-critical requests', + () => testGetCriticalRequests({ + priorityList: [MEDIUM, HIGH, LOW, MEDIUM, HIGH, VERY_LOW], + edges: [[0, 1], [1, 2], [2, 3], [3, 4]], + expected: [ + constructEmptyRequest(null, '0'), + constructEmptyRequest('0', '1'), + ] + })); + + it('returns correct data for two parallel chains', () => + testGetCriticalRequests({ + priorityList: [HIGH, HIGH, HIGH, HIGH], + edges: [[0, 2], [1, 3]], + expected: [ + constructEmptyRequest(null, '0'), + constructEmptyRequest(null, '1'), + constructEmptyRequest('0', '2'), + constructEmptyRequest('1', '3'), + ] + })); + + it('returns correct data for fork at root', () => + testGetCriticalRequests({ + priorityList: [HIGH, HIGH, HIGH], + edges: [[0, 1], [0, 2]], + expected: [ + constructEmptyRequest(null, '0'), + constructEmptyRequest('0', '1'), + constructEmptyRequest('0', '2'), + ] + })); + + it('returns correct data for fork at non root', () => + testGetCriticalRequests({ + priorityList: [HIGH, HIGH, HIGH, HIGH], + edges: [[0, 1], [1, 2], [1, 3]], + expected: [ + constructEmptyRequest(null, '0'), + constructEmptyRequest('0', '1'), + constructEmptyRequest('1', '2'), + constructEmptyRequest('1', '3'), + ] + })); + + it('returns empty list when no critical request', () => + testGetCriticalRequests({ + priorityList: [LOW, LOW], + edges: [[0, 1]], + expected: {} + })); + + it('returns empty list when no request whatsoever', () => + testGetCriticalRequests({ + priorityList: [], + edges: [], + expected: {} + })); + + it('returns correct data on a random big graph', () => + testGetCriticalRequests({ + priorityList: Array(9).fill(HIGH), + edges: [[0, 1], [1, 2], [1, 3], [4, 5], [5, 7], [7, 8], [5, 6]], + expected: [ + constructEmptyRequest(null, '0'), + constructEmptyRequest('0', '1'), + constructEmptyRequest('1', '2'), + constructEmptyRequest('1', '3'), + constructEmptyRequest(null, '4'), + constructEmptyRequest('4', '5'), + constructEmptyRequest('5', '6'), + constructEmptyRequest('5', '7'), + constructEmptyRequest('7', '8'), + ] + })); + + it('handles redirects', () => { + const networkRecords = mockTracingData([HIGH, HIGH, HIGH], [[0, 1], [1, 2]]); + + // Make a fake redirect + networkRecords[1].requestId = '1:redirected.0'; + networkRecords[2].requestId = '1'; + return Gatherer.request(networkRecords).then(criticalRequests => { + assert.deepEqual(criticalRequests, [ + constructEmptyRequest(null, '0'), + constructEmptyRequest('0', '1'), + constructEmptyRequest('1', '2'), + ]); + }); + }); + + + it('discards favicons as non-critical', () => { + const networkRecords = mockTracingData([HIGH, HIGH, HIGH, HIGH], [[0, 1], [0, 2], [0, 3]]); + + // 2nd record is a favicon + networkRecords[1].url = 'https://example.com/favicon.ico'; + networkRecords[1].mimeType = 'image/x-icon'; + networkRecords[1].parsedURL = { + lastPathComponent: 'favicon.ico' + }; + // 3rd record is a favicon + networkRecords[2].url = 'https://example.com/favicon-32x32.png'; + networkRecords[2].mimeType = 'image/png'; + networkRecords[2].parsedURL = { + lastPathComponent: 'favicon-32x32.png' + }; + // 4th record is a favicon + networkRecords[3].url = 'https://example.com/android-chrome-192x192.png'; + networkRecords[3].mimeType = 'image/png'; + networkRecords[3].parsedURL = { + lastPathComponent: 'android-chrome-192x192.png' + }; + + return Gatherer.request(networkRecords).then(criticalChains => { + assert.deepEqual(criticalChains, [ + constructEmptyRequest(null, '0') + ]); + }); + }); +});