diff --git a/lighthouse-core/audits/predictive-perf.js b/lighthouse-core/audits/predictive-perf.js index 8098050720ac..9cfbd4a6bc69 100644 --- a/lighthouse-core/audits/predictive-perf.js +++ b/lighthouse-core/audits/predictive-perf.js @@ -19,6 +19,24 @@ const SCORING_MEDIAN = 10000; // Any CPU task of 20 ms or more will end up being a critical long task on mobile const CRITICAL_LONG_TASK_THRESHOLD = 20; +const COEFFICIENTS = { + FCP: { + intercept: 1440, + optimistic: -1.75, + pessimistic: 2.73, + }, + FMP: { + intercept: 1532, + optimistic: -.30, + pessimistic: 1.33, + }, + TTCI: { + intercept: 1582, + optimistic: .97, + pessimistic: .49, + }, +}; + class PredictivePerf extends Audit { /** * @return {!AuditMeta} @@ -27,13 +45,74 @@ class PredictivePerf extends Audit { return { name: 'predictive-perf', description: 'Predicted Performance (beta)', - helpText: 'Predicted performance evaluates how your site will perform under ' + - 'a 3G connection on a mobile device.', + helpText: + 'Predicted performance evaluates how your site will perform under ' + + 'a 3G connection on a mobile device.', scoringMode: Audit.SCORING_MODES.NUMERIC, requiredArtifacts: ['traces', 'devtoolsLogs'], }; } + /** + * @param {!Node} dependencyGraph + * @param {function()=} condition + * @return {!Set} + */ + static getScriptUrls(dependencyGraph, condition) { + const scriptUrls = new Set(); + + dependencyGraph.traverse(node => { + if (node.type === Node.TYPES.CPU) return; + if (node.record._resourceType !== WebInspector.resourceTypes.Script) return; + if (condition && !condition(node)) return; + scriptUrls.add(node.record.url); + }); + + return scriptUrls; + } + + /** + * @param {!Node} dependencyGraph + * @param {!TraceOfTabArtifact} traceOfTab + * @return {!Node} + */ + static getOptimisticFCPGraph(dependencyGraph, traceOfTab) { + const fcp = traceOfTab.timestamps.firstContentfulPaint; + const blockingScriptUrls = PredictivePerf.getScriptUrls(dependencyGraph, node => { + return ( + node.endTime <= fcp && node.hasRenderBlockingPriority() && node.initiatorType !== 'script' + ); + }); + + return dependencyGraph.cloneWithRelationships(node => { + if (node.endTime > fcp) return false; + // Include EvaluateScript tasks for blocking scripts + if (node.type === Node.TYPES.CPU) return node.isEvaluateScriptFor(blockingScriptUrls); + // Include non-script-initiated network requests with a render-blocking priority + return node.hasRenderBlockingPriority() && node.initiatorType !== 'script'; + }); + } + + /** + * @param {!Node} dependencyGraph + * @param {!TraceOfTabArtifact} traceOfTab + * @return {!Node} + */ + static getPessimisticFCPGraph(dependencyGraph, traceOfTab) { + const fcp = traceOfTab.timestamps.firstContentfulPaint; + const blockingScriptUrls = PredictivePerf.getScriptUrls(dependencyGraph, node => { + return node.endTime <= fcp && node.hasRenderBlockingPriority(); + }); + + return dependencyGraph.cloneWithRelationships(node => { + if (node.endTime > fcp) return false; + // Include EvaluateScript tasks for blocking scripts + if (node.type === Node.TYPES.CPU) return node.isEvaluateScriptFor(blockingScriptUrls); + // Include all network requests that had render-blocking priority (even script-initiated) + return node.hasRenderBlockingPriority(); + }); + } + /** * @param {!Node} dependencyGraph * @param {!TraceOfTabArtifact} traceOfTab @@ -41,8 +120,16 @@ class PredictivePerf extends Audit { */ static getOptimisticFMPGraph(dependencyGraph, traceOfTab) { const fmp = traceOfTab.timestamps.firstMeaningfulPaint; + const requiredScriptUrls = PredictivePerf.getScriptUrls(dependencyGraph, node => { + return ( + node.endTime <= fmp && node.hasRenderBlockingPriority() && node.initiatorType !== 'script' + ); + }); + return dependencyGraph.cloneWithRelationships(node => { - if (node.endTime > fmp || node.type === Node.TYPES.CPU) return false; + if (node.endTime > fmp) return false; + // Include EvaluateScript tasks for blocking scripts + if (node.type === Node.TYPES.CPU) return node.isEvaluateScriptFor(requiredScriptUrls); // Include non-script-initiated network requests with a render-blocking priority return node.hasRenderBlockingPriority() && node.initiatorType !== 'script'; }); @@ -55,10 +142,18 @@ class PredictivePerf extends Audit { */ static getPessimisticFMPGraph(dependencyGraph, traceOfTab) { const fmp = traceOfTab.timestamps.firstMeaningfulPaint; + const requiredScriptUrls = PredictivePerf.getScriptUrls(dependencyGraph, node => { + return node.endTime <= fmp && node.hasRenderBlockingPriority(); + }); + return dependencyGraph.cloneWithRelationships(node => { if (node.endTime > fmp) return false; - // Include CPU tasks that performed a layout - if (node.type === Node.TYPES.CPU) return node.didPerformLayout(); + + // Include CPU tasks that performed a layout or were evaluations of required scripts + if (node.type === Node.TYPES.CPU) { + return node.didPerformLayout() || node.isEvaluateScriptFor(requiredScriptUrls); + } + // Include all network requests that had render-blocking priority (even script-initiated) return node.hasRenderBlockingPriority(); }); @@ -116,19 +211,22 @@ class PredictivePerf extends Audit { artifacts.requestTraceOfTab(trace), ]).then(([graph, traceOfTab]) => { const graphs = { + optimisticFCP: PredictivePerf.getOptimisticFCPGraph(graph, traceOfTab), + pessimisticFCP: PredictivePerf.getPessimisticFCPGraph(graph, traceOfTab), optimisticFMP: PredictivePerf.getOptimisticFMPGraph(graph, traceOfTab), pessimisticFMP: PredictivePerf.getPessimisticFMPGraph(graph, traceOfTab), optimisticTTCI: PredictivePerf.getOptimisticTTCIGraph(graph, traceOfTab), pessimisticTTCI: PredictivePerf.getPessimisticTTCIGraph(graph, traceOfTab), }; - let sum = 0; const values = {}; Object.keys(graphs).forEach(key => { const estimate = new LoadSimulator(graphs[key]).simulate(); const lastLongTaskEnd = PredictivePerf.getLastLongTaskEndTime(estimate.nodeTiming); switch (key) { + case 'optimisticFCP': + case 'pessimisticFCP': case 'optimisticFMP': case 'pessimisticFMP': values[key] = estimate.timeInMs; @@ -140,21 +238,33 @@ class PredictivePerf extends Audit { values[key] = Math.max(values.pessimisticFMP, lastLongTaskEnd); break; } - - sum += values[key]; }); - const meanDuration = sum / Object.keys(values).length; + values.roughEstimateOfFCP = COEFFICIENTS.FCP.intercept + + COEFFICIENTS.FCP.optimistic * values.optimisticFCP + + COEFFICIENTS.FCP.pessimistic * values.pessimisticFCP; + values.roughEstimateOfFMP = COEFFICIENTS.FMP.intercept + + COEFFICIENTS.FMP.optimistic * values.optimisticFMP + + COEFFICIENTS.FMP.pessimistic * values.pessimisticFMP; + values.roughEstimateOfTTCI = COEFFICIENTS.TTCI.intercept + + COEFFICIENTS.TTCI.optimistic * values.optimisticTTCI + + COEFFICIENTS.TTCI.pessimistic * values.pessimisticTTCI; + + // While the raw values will never be lower than following metric, the weights make this + // theoretically possible, so take the maximum if this happens. + values.roughEstimateOfFMP = Math.max(values.roughEstimateOfFCP, values.roughEstimateOfFMP); + values.roughEstimateOfTTCI = Math.max(values.roughEstimateOfFMP, values.roughEstimateOfTTCI); + const score = Audit.computeLogNormalScore( - meanDuration, + values.roughEstimateOfTTCI, SCORING_POINT_OF_DIMINISHING_RETURNS, SCORING_MEDIAN ); return { score, - rawValue: meanDuration, - displayValue: Util.formatMilliseconds(meanDuration), + rawValue: values.roughEstimateOfTTCI, + displayValue: Util.formatMilliseconds(values.roughEstimateOfTTCI), extendedInfo: {value: values}, }; }); diff --git a/lighthouse-core/gather/computed/page-dependency-graph.js b/lighthouse-core/gather/computed/page-dependency-graph.js index 43f7288ff6bc..0f140ceab8ea 100644 --- a/lighthouse-core/gather/computed/page-dependency-graph.js +++ b/lighthouse-core/gather/computed/page-dependency-graph.js @@ -121,6 +121,15 @@ class PageDependencyGraphArtifact extends ComputedArtifact { } else if (node !== rootNode) { rootNode.addDependent(node); } + + const redirects = Array.from(node.record.redirects || []); + redirects.push(node.record); + + for (let i = 1; i < redirects.length; i++) { + const redirectNode = networkNodeOutput.idToNodeMap.get(redirects[i - 1].requestId); + const actualNode = networkNodeOutput.idToNodeMap.get(redirects[i].requestId); + actualNode.addDependency(redirectNode); + } }); } diff --git a/lighthouse-core/lib/dependency-graph/cpu-node.js b/lighthouse-core/lib/dependency-graph/cpu-node.js index 26595524cbc1..e89a38d94d50 100644 --- a/lighthouse-core/lib/dependency-graph/cpu-node.js +++ b/lighthouse-core/lib/dependency-graph/cpu-node.js @@ -56,12 +56,26 @@ class CPUNode extends Node { } /** + * Returns true if this node contains a Layout task. * @return {boolean} */ didPerformLayout() { return this._childEvents.some(evt => evt.name === 'Layout'); } + /** + * Returns true if this node contains the EvaluateScript task for a URL in the given set. + * @param {!Set} urls + * @return {boolean} + */ + isEvaluateScriptFor(urls) { + return this._childEvents.some(evt => { + return evt.name === 'EvaluateScript' && + evt.args.data && + urls.has(evt.args.data.url); + }); + } + /** * @return {!CPUNode} */ diff --git a/lighthouse-core/test/audits/predictive-perf-test.js b/lighthouse-core/test/audits/predictive-perf-test.js index 52d3baf6123b..2ace238d3d95 100644 --- a/lighthouse-core/test/audits/predictive-perf-test.js +++ b/lighthouse-core/test/audits/predictive-perf-test.js @@ -25,13 +25,18 @@ describe('Performance: predictive performance audit', () => { }, Runner.instantiateComputedArtifacts()); return PredictivePerf.audit(artifacts).then(output => { - assert.equal(output.score, 99); - assert.equal(Math.round(output.rawValue), 1696); - assert.equal(output.displayValue, '1,700\xa0ms'); + assert.equal(output.score, 80); + assert.equal(Math.round(output.rawValue), 5123); + assert.equal(output.displayValue, '5,120\xa0ms'); const valueOf = name => Math.round(output.extendedInfo.value[name]); - assert.equal(valueOf('optimisticFMP'), 754); + assert.equal(valueOf('roughEstimateOfFCP'), 2035); + assert.equal(valueOf('optimisticFCP'), 607); + assert.equal(valueOf('pessimisticFCP'), 607); + assert.equal(valueOf('roughEstimateOfFMP'), 2845); + assert.equal(valueOf('optimisticFMP'), 904); assert.equal(valueOf('pessimisticFMP'), 1191); + assert.equal(valueOf('roughEstimateOfTTCI'), 5123); assert.equal(valueOf('optimisticTTCI'), 2438); assert.equal(valueOf('pessimisticTTCI'), 2399); });