From b1b21617bce6b04fc94361f31e4246856708b877 Mon Sep 17 00:00:00 2001 From: Ward Peeters Date: Thu, 11 May 2017 20:38:31 +0200 Subject: [PATCH] Fix critical chains & time to first byte errors --- lighthouse-core/audits/time-to-first-byte.js | 6 +- .../closure/typedefs/ComputedArtifacts.js | 3 + .../computed/critical-request-chains.js | 142 ++---- .../gather/computed/critical-requests.js | 74 ++- .../audits/critical-request-chains-test.js | 93 ++-- .../computed/critical-request-chains-test.js | 441 +++++++----------- .../gather/computed/critical-requests-test.js | 197 ++++++++ 7 files changed, 515 insertions(+), 441 deletions(-) create mode 100644 lighthouse-core/test/gather/computed/critical-requests-test.js diff --git a/lighthouse-core/audits/time-to-first-byte.js b/lighthouse-core/audits/time-to-first-byte.js index 9630c86599b6..6a03b9a6510c 100644 --- a/lighthouse-core/audits/time-to-first-byte.js +++ b/lighthouse-core/audits/time-to-first-byte.js @@ -43,6 +43,10 @@ class TTFBMetric extends Audit { static caclulateTTFB(record) { const timing = record._timing; + if (!timing) { + console.log(record.url, record._timing, record); + } + return timing.receiveHeadersEnd - timing.sendEnd; } @@ -66,7 +70,7 @@ class TTFBMetric extends Audit { criticalRequests.forEach(request => { const networkRecord = networkRecords.find(record => record._requestId === request.id); - if (networkRecord) { + if (networkRecord && networkRecord._timing) { const ttfb = TTFBMetric.caclulateTTFB(networkRecord); results.push({ url: URL.getDisplayName(networkRecord._url), 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 81b8441dd37a..139d728e2cdb 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,117 +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. - const nonCriticalResourceTypes = [ - WebInspector.resourceTypes.Image._category, - WebInspector.resourceTypes.XHR._category - ]; - if (nonCriticalResourceTypes.includes(resourceTypeCategory)) { - return false; - } - - // Treat favicons as non-critical resources - if (request.mimeType === 'image/x-icon' || - (request.parsedURL && request.parsedURL.lastPathComponent === 'favicon.ico')) { - return false; - } - - return ['VeryHigh', 'High', 'Medium'].includes(request.priority()); - } - - compute_(networkRecords) { - // 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 { - url: request._url, + 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 index f540ea76e894..6ac427003c17 100644 --- a/lighthouse-core/gather/computed/critical-requests.js +++ b/lighthouse-core/gather/computed/critical-requests.js @@ -32,10 +32,16 @@ class CriticalRequests extends ComputedArtifact { * @see https://docs.google.com/document/d/1bCDuq9H1ih9iNjgzyAL0gpwNFiEP4TZS-YLRp_RuMlc * @param {any} request */ - static isCritical(request) { - // XHRs are fetched at High priority, but we exclude them, as they are unlikely to be critical + isCritical(request) { const resourceTypeCategory = request._resourceType && request._resourceType._category; - if (resourceTypeCategory === WebInspector.resourceTypes.XHR._category) { + + // XHRs are fetched at High priority, but we exclude them, as they are unlikely to be critical + // Images are also non-critical. + const nonCriticalResourceTypes = [ + WebInspector.resourceTypes.Image._category, + WebInspector.resourceTypes.XHR._category + ]; + if (nonCriticalResourceTypes.includes(resourceTypeCategory)) { return false; } @@ -48,19 +54,61 @@ class CriticalRequests extends ComputedArtifact { 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 => CriticalRequests.isCritical(req)); - - return criticalRequests.map(req => ({ - id: req._requestId, - url: req._url, - startTime: req.startTime, - endTime: req.endTime, - responseReceivedTime: req.responseReceivedTime, - transferSize: req.transferSize, - })); + 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; } } diff --git a/lighthouse-core/test/audits/critical-request-chains-test.js b/lighthouse-core/test/audits/critical-request-chains-test.js index cb1a0267f210..4f608c59729d 100644 --- a/lighthouse-core/test/audits/critical-request-chains-test.js +++ b/lighthouse-core/test/audits/critical-request-chains-test.js @@ -18,62 +18,49 @@ const Audit = require('../../audits/critical-request-chains.js'); const assert = require('assert'); -const FAILING_REQUEST_CHAIN = { - 0: { - request: { - endTime: 1, - responseReceivedTime: 5, - startTime: 0, - url: 'https://example.com/' - }, - children: { - 1: { - request: { - endTime: 16, - responseReceivedTime: 14, - startTime: 11, - url: 'https://example.com/b.js' - }, - children: { - } - }, - 2: { - request: { - endTime: 17, - responseReceivedTime: 15, - startTime: 12, - url: 'https://example.com/c.js' - }, - children: {} - } - } - } +const rootElm = { + id: '0', + endTime: 1, + responseReceivedTime: 5, + startTime: 0, + url: 'https://example.com/', + parent: null, }; - -const PASSING_REQUEST_CHAIN = { - 0: { - request: { - endTime: 1, - responseReceivedTime: 5, - startTime: 0, - url: 'https://example.com/' - }, - children: {}, +const FAILING_REQUEST_CHAIN = [ + rootElm, + { + id: '1', + endTime: 16, + responseReceivedTime: 14, + startTime: 11, + url: 'https://example.com/b.js', + parent: rootElm, }, -}; + { + id: '2', + endTime: 17, + responseReceivedTime: 15, + startTime: 12, + url: 'https://example.com/c.js', + parent: rootElm, + }, +]; -const PASSING_REQUEST_CHAIN_2 = { - 13653.1: { - request: { - url: 'http://localhost:10503/offline-ready.html', - startTime: 33552.036878, - endTime: 33552.285438, - responseReceivedTime: 33552.275677, - transferSize: 1849 - }, - children: {} +const PASSING_REQUEST_CHAIN = [ + rootElm, +]; + +const PASSING_REQUEST_CHAIN_2 = [ + { + id: '13653.1', + url: 'http://localhost:10503/offline-ready.html', + startTime: 33552.036878, + endTime: 33552.285438, + responseReceivedTime: 33552.275677, + transferSize: 1849, + parent: null, } -}; +]; const EMPTY_REQUEST_CHAIN = {}; @@ -85,7 +72,7 @@ const mockArtifacts = (mockChain) => { requestNetworkRecords: () => { return Promise.resolve([]); }, - requestCriticalRequestChains: function() { + requestCriticalRequest: function() { return Promise.resolve(mockChain); } }; 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 161e4d2eb6e4..a2797a686ce3 100644 --- a/lighthouse-core/test/gather/computed/critical-request-chains-test.js +++ b/lighthouse-core/test/gather/computed/critical-request-chains-test.js @@ -19,310 +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' - }, - 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]; - 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: {} + return testGetCriticalRequestsChains(criticalRequests, expected); + }); + + 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], [[0, 1], [0, 2]]); - - // 2nd record is a favicon - networkRecords[1].url = 'https://example.com/favicon.ico'; - networkRecords[1].parsedURL = { - lastPathComponent: 'favicon.ico' + 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: { + }, + }, + }, + }, + }, + }, }; - // 3rd record is also a favicon - networkRecords[2].mimeType = 'image/x-icon'; - return Gatherer.request(networkRecords).then(criticalChains => { - assert.deepEqual(criticalChains, { - 0: { - request: constructEmptyRequest(), - children: {} - } - }); - }); - }); - it('handles non-existent nodes when building the tree', () => { - const networkRecords = mockTracingData([HIGH, HIGH], [[0, 1]]); + criticalRequests[0].parent = null; + criticalRequests[1].parent = criticalRequests[0]; + criticalRequests[2].parent = criticalRequests[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..d16fc92442f2 --- /dev/null +++ b/lighthouse-core/test/gather/computed/critical-requests-test.js @@ -0,0 +1,197 @@ +/** + * 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], [[0, 1], [0, 2]]); + + // 2nd record is a favicon + networkRecords[1].url = 'https://example.com/favicon.ico'; + networkRecords[1].parsedURL = { + lastPathComponent: 'favicon.ico' + }; + // 3rd record is also a favicon + networkRecords[2].mimeType = 'image/x-icon'; + return Gatherer.request(networkRecords).then(criticalRequests => { + assert.deepEqual(criticalRequests, [ + constructEmptyRequest(null, '0'), + ]); + }); + }); +});