From 94bfb871aa47af2997049412851d16a02d8a77fd Mon Sep 17 00:00:00 2001 From: Patrick Hulce Date: Thu, 2 Nov 2017 14:36:06 -0700 Subject: [PATCH 1/3] core(predictive-perf): predict FCP instead of FMP --- lighthouse-core/audits/predictive-perf.js | 63 ++++++++++++++----- .../gather/computed/page-dependency-graph.js | 9 +++ .../lib/dependency-graph/cpu-node.js | 14 +++++ .../test/audits/predictive-perf-test.js | 8 +-- 4 files changed, 74 insertions(+), 20 deletions(-) diff --git a/lighthouse-core/audits/predictive-perf.js b/lighthouse-core/audits/predictive-perf.js index 8098050720ac..0841b46b8b46 100644 --- a/lighthouse-core/audits/predictive-perf.js +++ b/lighthouse-core/audits/predictive-perf.js @@ -27,22 +27,49 @@ 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 getOptimisticFMPGraph(dependencyGraph, traceOfTab) { - const fmp = traceOfTab.timestamps.firstMeaningfulPaint; + 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 > fmp || node.type === Node.TYPES.CPU) return false; + 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'; }); @@ -53,12 +80,16 @@ class PredictivePerf extends Audit { * @param {!TraceOfTabArtifact} traceOfTab * @return {!Node} */ - static getPessimisticFMPGraph(dependencyGraph, traceOfTab) { - const fmp = traceOfTab.timestamps.firstMeaningfulPaint; + 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 > fmp) return false; - // Include CPU tasks that performed a layout - if (node.type === Node.TYPES.CPU) return node.didPerformLayout(); + 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(); }); @@ -116,8 +147,8 @@ class PredictivePerf extends Audit { artifacts.requestTraceOfTab(trace), ]).then(([graph, traceOfTab]) => { const graphs = { - optimisticFMP: PredictivePerf.getOptimisticFMPGraph(graph, traceOfTab), - pessimisticFMP: PredictivePerf.getPessimisticFMPGraph(graph, traceOfTab), + optimisticFCP: PredictivePerf.getOptimisticFCPGraph(graph, traceOfTab), + pessimisticFCP: PredictivePerf.getPessimisticFCPGraph(graph, traceOfTab), optimisticTTCI: PredictivePerf.getOptimisticTTCIGraph(graph, traceOfTab), pessimisticTTCI: PredictivePerf.getPessimisticTTCIGraph(graph, traceOfTab), }; @@ -129,15 +160,15 @@ class PredictivePerf extends Audit { const lastLongTaskEnd = PredictivePerf.getLastLongTaskEndTime(estimate.nodeTiming); switch (key) { - case 'optimisticFMP': - case 'pessimisticFMP': + case 'optimisticFCP': + case 'pessimisticFCP': values[key] = estimate.timeInMs; break; case 'optimisticTTCI': - values[key] = Math.max(values.optimisticFMP, lastLongTaskEnd); + values[key] = Math.max(values.optimisticFCP, lastLongTaskEnd); break; case 'pessimisticTTCI': - values[key] = Math.max(values.pessimisticFMP, lastLongTaskEnd); + values[key] = Math.max(values.pessimisticFCP, lastLongTaskEnd); break; } 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..d04d5b0607de 100644 --- a/lighthouse-core/test/audits/predictive-perf-test.js +++ b/lighthouse-core/test/audits/predictive-perf-test.js @@ -26,12 +26,12 @@ describe('Performance: predictive performance audit', () => { 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(Math.round(output.rawValue), 1513); + assert.equal(output.displayValue, '1,510\xa0ms'); const valueOf = name => Math.round(output.extendedInfo.value[name]); - assert.equal(valueOf('optimisticFMP'), 754); - assert.equal(valueOf('pessimisticFMP'), 1191); + assert.equal(valueOf('optimisticFCP'), 607); + assert.equal(valueOf('pessimisticFCP'), 607); assert.equal(valueOf('optimisticTTCI'), 2438); assert.equal(valueOf('pessimisticTTCI'), 2399); }); From 8b0f2b9ecefb25f470836aea5ed3d6d4d5bf334f Mon Sep 17 00:00:00 2001 From: Patrick Hulce Date: Mon, 6 Nov 2017 13:54:19 -0800 Subject: [PATCH 2/3] add back FMP --- lighthouse-core/audits/predictive-perf.js | 38 ++++++++++++++++++- .../test/audits/predictive-perf-test.js | 6 ++- 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/lighthouse-core/audits/predictive-perf.js b/lighthouse-core/audits/predictive-perf.js index 0841b46b8b46..3348b38baa51 100644 --- a/lighthouse-core/audits/predictive-perf.js +++ b/lighthouse-core/audits/predictive-perf.js @@ -95,6 +95,36 @@ class PredictivePerf extends Audit { }); } + /** + * @param {!Node} dependencyGraph + * @param {!TraceOfTabArtifact} traceOfTab + * @return {!Node} + */ + static getOptimisticFMPGraph(dependencyGraph, traceOfTab) { + const fmp = traceOfTab.timestamps.firstMeaningfulPaint; + return dependencyGraph.cloneWithRelationships(node => { + if (node.endTime > fmp || node.type === Node.TYPES.CPU) return false; + // 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 getPessimisticFMPGraph(dependencyGraph, traceOfTab) { + const fmp = traceOfTab.timestamps.firstMeaningfulPaint; + 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 all network requests that had render-blocking priority (even script-initiated) + return node.hasRenderBlockingPriority(); + }); + } + /** * @param {!Node} dependencyGraph * @return {!Node} @@ -149,6 +179,8 @@ class PredictivePerf extends Audit { 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), }; @@ -162,13 +194,15 @@ class PredictivePerf extends Audit { switch (key) { case 'optimisticFCP': case 'pessimisticFCP': + case 'optimisticFMP': + case 'pessimisticFMP': values[key] = estimate.timeInMs; break; case 'optimisticTTCI': - values[key] = Math.max(values.optimisticFCP, lastLongTaskEnd); + values[key] = Math.max(values.optimisticFMP, lastLongTaskEnd); break; case 'pessimisticTTCI': - values[key] = Math.max(values.pessimisticFCP, lastLongTaskEnd); + values[key] = Math.max(values.pessimisticFMP, lastLongTaskEnd); break; } diff --git a/lighthouse-core/test/audits/predictive-perf-test.js b/lighthouse-core/test/audits/predictive-perf-test.js index d04d5b0607de..f664c43844cf 100644 --- a/lighthouse-core/test/audits/predictive-perf-test.js +++ b/lighthouse-core/test/audits/predictive-perf-test.js @@ -26,12 +26,14 @@ describe('Performance: predictive performance audit', () => { return PredictivePerf.audit(artifacts).then(output => { assert.equal(output.score, 99); - assert.equal(Math.round(output.rawValue), 1513); - assert.equal(output.displayValue, '1,510\xa0ms'); + assert.equal(Math.round(output.rawValue), 1333); + assert.equal(output.displayValue, '1,330\xa0ms'); const valueOf = name => Math.round(output.extendedInfo.value[name]); assert.equal(valueOf('optimisticFCP'), 607); assert.equal(valueOf('pessimisticFCP'), 607); + assert.equal(valueOf('optimisticFMP'), 754); + assert.equal(valueOf('pessimisticFMP'), 1191); assert.equal(valueOf('optimisticTTCI'), 2438); assert.equal(valueOf('pessimisticTTCI'), 2399); }); From 7d1f809e6233135dc0d432dd9c0aebbc88fee1b1 Mon Sep 17 00:00:00 2001 From: Patrick Hulce Date: Wed, 8 Nov 2017 18:20:38 -0800 Subject: [PATCH 3/3] add rough estimates --- lighthouse-core/audits/predictive-perf.js | 65 ++++++++++++++++--- .../test/audits/predictive-perf-test.js | 11 ++-- 2 files changed, 62 insertions(+), 14 deletions(-) diff --git a/lighthouse-core/audits/predictive-perf.js b/lighthouse-core/audits/predictive-perf.js index 3348b38baa51..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} @@ -102,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'; }); @@ -116,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(); }); @@ -185,7 +219,6 @@ class PredictivePerf extends Audit { pessimisticTTCI: PredictivePerf.getPessimisticTTCIGraph(graph, traceOfTab), }; - let sum = 0; const values = {}; Object.keys(graphs).forEach(key => { const estimate = new LoadSimulator(graphs[key]).simulate(); @@ -205,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/test/audits/predictive-perf-test.js b/lighthouse-core/test/audits/predictive-perf-test.js index f664c43844cf..2ace238d3d95 100644 --- a/lighthouse-core/test/audits/predictive-perf-test.js +++ b/lighthouse-core/test/audits/predictive-perf-test.js @@ -25,15 +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), 1333); - assert.equal(output.displayValue, '1,330\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('roughEstimateOfFCP'), 2035); assert.equal(valueOf('optimisticFCP'), 607); assert.equal(valueOf('pessimisticFCP'), 607); - assert.equal(valueOf('optimisticFMP'), 754); + 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); });