diff --git a/lighthouse-cli/printer.js b/lighthouse-cli/printer.js index 66474515e4e7..fbe07abf3459 100644 --- a/lighthouse-cli/printer.js +++ b/lighthouse-cli/printer.js @@ -90,7 +90,7 @@ function writeFile(filePath, output, outputMode) { * @param {!LH.Results} results * @param {string} mode * @param {string} path - * @return {!Promise} + * @return {Promise} */ function write(results, mode, path) { return new Promise((resolve, reject) => { diff --git a/lighthouse-cli/run.js b/lighthouse-cli/run.js index abd70966138c..55212daeed06 100644 --- a/lighthouse-cli/run.js +++ b/lighthouse-cli/run.js @@ -50,7 +50,7 @@ function parseChromeFlags(flags = '') { * Attempts to connect to an instance of Chrome with an open remote-debugging * port. If none is found, launches a debuggable instance. * @param {!LH.Flags} flags - * @return {!Promise} + * @return {Promise} */ function getDebuggableChrome(flags) { return ChromeLauncher.launch({ @@ -98,7 +98,7 @@ function handleError(err) { * @param {!LH.Results} results * @param {!Object} artifacts * @param {!LH.Flags} flags - * @return {!Promise} + * @return {Promise} */ function saveResults(results, artifacts, flags) { const cwd = process.cwd(); @@ -149,7 +149,7 @@ function saveResults(results, artifacts, flags) { * @param {string} url * @param {!LH.Flags} flags * @param {!LH.Config|undefined} config - * @return {!Promise} + * @return {Promise} */ function runLighthouse(url, flags, config) { /** @type {!LH.LaunchedChrome} */ @@ -183,7 +183,7 @@ function runLighthouse(url, flags, config) { }); /** - * @return {!Promise<{}>} + * @return {Promise<{}>} */ function potentiallyKillChrome() { if (launchedChrome !== undefined) { diff --git a/lighthouse-core/audits/byte-efficiency/byte-efficiency-audit.js b/lighthouse-core/audits/byte-efficiency/byte-efficiency-audit.js index 1a044cb0c615..7c60be5dae4e 100644 --- a/lighthouse-core/audits/byte-efficiency/byte-efficiency-audit.js +++ b/lighthouse-core/audits/byte-efficiency/byte-efficiency-audit.js @@ -6,7 +6,8 @@ 'use strict'; const Audit = require('../audit'); -const PredictivePerf = require('../predictive-perf'); +const ConsistentlyInteractive = require('../../gather/computed/metrics/lantern-consistently-interactive'); // eslint-disable-line max-len +const NetworkAnalysis = require('../../gather/computed/network-analysis'); const LoadSimulator = require('../../lib/dependency-graph/simulator/simulator.js'); const KB_IN_BYTES = 1024; @@ -120,8 +121,8 @@ class UnusedBytes extends Audit { }); const savingsOnTTI = Math.max( - PredictivePerf.getLastLongTaskEndTime(simulationBeforeChanges.nodeTiming) - - PredictivePerf.getLastLongTaskEndTime(simulationAfterChanges.nodeTiming), + ConsistentlyInteractive.getLastLongTaskEndTime(simulationBeforeChanges.nodeTiming) - + ConsistentlyInteractive.getLastLongTaskEndTime(simulationAfterChanges.nodeTiming), 0 ); @@ -135,7 +136,11 @@ class UnusedBytes extends Audit { * @return {!AuditResult} */ static createAuditResult(result, graph) { - const simulatorOptions = PredictivePerf.computeRTTAndServerResponseTime(graph); + const records = []; + graph.traverse(node => node.record && records.push(node.record)); + const simulatorOptions = NetworkAnalysis.computeRTTAndServerResponseTime(records); + // TODO: use rtt/throughput from config.settings instead of defaults + delete simulatorOptions.rtt; // TODO: calibrate multipliers, see https://github.com/GoogleChrome/lighthouse/issues/820 Object.assign(simulatorOptions, {cpuSlowdownMultiplier: 1, layoutTaskMultiplier: 1}); const simulator = new LoadSimulator(graph, simulatorOptions); diff --git a/lighthouse-core/audits/predictive-perf.js b/lighthouse-core/audits/predictive-perf.js index 698ad465728c..5c7a203edc7d 100644 --- a/lighthouse-core/audits/predictive-perf.js +++ b/lighthouse-core/audits/predictive-perf.js @@ -7,37 +7,12 @@ const Audit = require('./audit'); const Util = require('../report/v2/renderer/util'); -const LoadSimulator = require('../lib/dependency-graph/simulator/simulator'); -const NetworkAnalyzer = require('../lib/dependency-graph/simulator/network-analyzer'); -const Node = require('../lib/dependency-graph/node'); -const WebInspector = require('../lib/web-inspector'); // Parameters (in ms) for log-normal CDF scoring. To see the curve: // https://www.desmos.com/calculator/rjp0lbit8y const SCORING_POINT_OF_DIMINISHING_RETURNS = 1700; 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: -0.3, - pessimistic: 1.33, - }, - TTCI: { - intercept: 1582, - optimistic: 0.97, - pessimistic: 0.49, - }, -}; - class PredictivePerf extends Audit { /** * @return {!AuditMeta} @@ -54,266 +29,43 @@ class PredictivePerf extends Audit { }; } - /** - * @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 - * @return {!Object} - */ - static computeRTTAndServerResponseTime(dependencyGraph) { - const records = []; - dependencyGraph.traverse(node => { - if (node.type === Node.TYPES.NETWORK) records.push(node.record); - }); - - // First pass compute the estimated observed RTT to each origin's servers. - const rttByOrigin = new Map(); - for (const [origin, summary] of NetworkAnalyzer.estimateRTTByOrigin(records).entries()) { - rttByOrigin.set(origin, summary.min); - } - - // We'll use the minimum RTT as the assumed connection latency since we care about how much addt'l - // latency each origin introduces as Lantern will be simulating with its own connection latency. - const minimumRtt = Math.min(...Array.from(rttByOrigin.values())); - // We'll use the observed RTT information to help estimate the server response time - const responseTimeSummaries = NetworkAnalyzer.estimateServerResponseTimeByOrigin(records, { - rttByOrigin, - }); - - const additionalRttByOrigin = new Map(); - const serverResponseTimeByOrigin = new Map(); - for (const [origin, summary] of responseTimeSummaries.entries()) { - additionalRttByOrigin.set(origin, rttByOrigin.get(origin) - minimumRtt); - serverResponseTimeByOrigin.set(origin, summary.median); - } - - return {additionalRttByOrigin, serverResponseTimeByOrigin}; - } - - /** - * @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 - * @return {!Node} - */ - 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) 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'; - }); - } - - /** - * @param {!Node} dependencyGraph - * @param {!TraceOfTabArtifact} traceOfTab - * @return {!Node} - */ - 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 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(); - }); - } - - /** - * @param {!Node} dependencyGraph - * @return {!Node} - */ - static getOptimisticTTCIGraph(dependencyGraph) { - // Adjust the critical long task threshold for microseconds - const minimumCpuTaskDuration = CRITICAL_LONG_TASK_THRESHOLD * 1000; - - return dependencyGraph.cloneWithRelationships(node => { - // Include everything that might be a long task - if (node.type === Node.TYPES.CPU) return node.event.dur > minimumCpuTaskDuration; - // Include all scripts and high priority requests, exclude all images - const isImage = node.record._resourceType === WebInspector.resourceTypes.Image; - const isScript = node.record._resourceType === WebInspector.resourceTypes.Script; - return ( - !isImage && - (isScript || node.record.priority() === 'High' || node.record.priority() === 'VeryHigh') - ); - }); - } - - /** - * @param {!Node} dependencyGraph - * @return {!Node} - */ - static getPessimisticTTCIGraph(dependencyGraph) { - return dependencyGraph; - } - - /** - * @param {!Map} nodeTiming - * @return {number} - */ - static getLastLongTaskEndTime(nodeTiming, duration = 50) { - return Array.from(nodeTiming.entries()) - .filter( - ([node, timing]) => - node.type === Node.TYPES.CPU && timing.endTime - timing.startTime > duration - ) - .map(([_, timing]) => timing.endTime) - .reduce((max, x) => Math.max(max, x), 0); - } - /** * @param {!Artifacts} artifacts * @return {!AuditResult} */ - static audit(artifacts) { + static async audit(artifacts) { const trace = artifacts.traces[Audit.DEFAULT_PASS]; const devtoolsLog = artifacts.devtoolsLogs[Audit.DEFAULT_PASS]; - return Promise.all([ - artifacts.requestPageDependencyGraph({trace, devtoolsLog}), - 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), - }; - - const values = {}; - const options = PredictivePerf.computeRTTAndServerResponseTime(graph); - Object.keys(graphs).forEach(key => { - const estimate = new LoadSimulator(graphs[key], options).simulate(); - const longTaskThreshold = key.startsWith('optimistic') ? 100 : 50; - const lastLongTaskEnd = PredictivePerf.getLastLongTaskEndTime( - estimate.nodeTiming, - longTaskThreshold - ); - - switch (key) { - case 'optimisticFCP': - case 'pessimisticFCP': - case 'optimisticFMP': - case 'pessimisticFMP': - values[key] = estimate.timeInMs; - break; - case 'optimisticTTCI': - values[key] = Math.max(values.optimisticFMP, lastLongTaskEnd); - break; - case 'pessimisticTTCI': - values[key] = Math.max(values.pessimisticFMP, lastLongTaskEnd); - break; - } - }); - - 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 fcp = await artifacts.requestLanternFirstContentfulPaint({trace, devtoolsLog}); + const fmp = await artifacts.requestLanternFirstMeaningfulPaint({trace, devtoolsLog}); + const ttci = await artifacts.requestLanternConsistentlyInteractive({trace, devtoolsLog}); + + const values = { + roughEstimateOfFCP: fcp.timing, + optimisticFCP: fcp.optimisticEstimate.timeInMs, + pessimisticFCP: fcp.pessimisticEstimate.timeInMs, + + roughEstimateOfFMP: fmp.timing, + optimisticFMP: fmp.optimisticEstimate.timeInMs, + pessimisticFMP: fmp.pessimisticEstimate.timeInMs, + + roughEstimateOfTTCI: ttci.timing, + optimisticTTCI: ttci.optimisticEstimate.timeInMs, + pessimisticTTCI: ttci.pessimisticEstimate.timeInMs, + }; - const score = Audit.computeLogNormalScore( - values.roughEstimateOfTTCI, - SCORING_POINT_OF_DIMINISHING_RETURNS, - SCORING_MEDIAN - ); + const score = Audit.computeLogNormalScore( + values.roughEstimateOfTTCI, + SCORING_POINT_OF_DIMINISHING_RETURNS, + SCORING_MEDIAN + ); - return { - score, - rawValue: values.roughEstimateOfTTCI, - displayValue: Util.formatMilliseconds(values.roughEstimateOfTTCI), - extendedInfo: {value: values}, - }; - }); + return { + score, + rawValue: values.roughEstimateOfTTCI, + displayValue: Util.formatMilliseconds(values.roughEstimateOfTTCI), + extendedInfo: {value: values}, + }; } } diff --git a/lighthouse-core/gather/computed/computed-artifact.js b/lighthouse-core/gather/computed/computed-artifact.js index bd010dd97738..0edf323b85cc 100644 --- a/lighthouse-core/gather/computed/computed-artifact.js +++ b/lighthouse-core/gather/computed/computed-artifact.js @@ -3,6 +3,7 @@ * 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. */ +// @ts-nocheck 'use strict'; const ArbitraryEqualityMap = require('../../lib/arbitrary-equality-map'); diff --git a/lighthouse-core/gather/computed/metrics/lantern-consistently-interactive.js b/lighthouse-core/gather/computed/metrics/lantern-consistently-interactive.js new file mode 100644 index 000000000000..e837a580d712 --- /dev/null +++ b/lighthouse-core/gather/computed/metrics/lantern-consistently-interactive.js @@ -0,0 +1,113 @@ +/** + * @license Copyright 2018 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 MetricArtifact = require('./lantern-metric'); +const Node = require('../../../lib/dependency-graph/node'); +const CPUNode = require('../../../lib/dependency-graph/cpu-node'); // eslint-disable-line no-unused-vars +const NetworkNode = require('../../../lib/dependency-graph/network-node'); // eslint-disable-line no-unused-vars +const WebInspector = require('../../../lib/web-inspector'); + +// Any CPU task of 20 ms or more will end up being a critical long task on mobile +const CRITICAL_LONG_TASK_THRESHOLD = 20; + +class ConsistentlyInteractive extends MetricArtifact { + get name() { + return 'LanternConsistentlyInteractive'; + } + + /** + * @return {LH.Gatherer.Simulation.MetricCoefficients} + */ + get COEFFICIENTS() { + return { + intercept: 1582, + optimistic: 0.97, + pessimistic: 0.49, + }; + } + + /** + * @param {Node} dependencyGraph + * @return {Node} + */ + getOptimisticGraph(dependencyGraph) { + // Adjust the critical long task threshold for microseconds + const minimumCpuTaskDuration = CRITICAL_LONG_TASK_THRESHOLD * 1000; + + return dependencyGraph.cloneWithRelationships(node => { + // Include everything that might be a long task + if (node.type === Node.TYPES.CPU) { + return /** @type {CPUNode} */ (node).event.dur > minimumCpuTaskDuration; + } + + const asNetworkNode = /** @type {NetworkNode} */ (node); + // Include all scripts and high priority requests, exclude all images + const isImage = asNetworkNode.record._resourceType === WebInspector.resourceTypes.Image; + const isScript = asNetworkNode.record._resourceType === WebInspector.resourceTypes.Script; + return ( + !isImage && + (isScript || + asNetworkNode.record.priority() === 'High' || + asNetworkNode.record.priority() === 'VeryHigh') + ); + }); + } + + /** + * @param {Node} dependencyGraph + * @return {Node} + */ + getPessimisticGraph(dependencyGraph) { + return dependencyGraph; + } + + /** + * @param {LH.Gatherer.Simulation.Result} simulationResult + * @param {Object} extras + * @return {LH.Gatherer.Simulation.Result} + */ + getEstimateFromSimulation(simulationResult, extras) { + const lastTaskAt = ConsistentlyInteractive.getLastLongTaskEndTime(simulationResult.nodeTiming); + const minimumTime = extras.optimistic + ? extras.fmpResult.optimisticEstimate.timeInMs + : extras.fmpResult.pessimisticEstimate.timeInMs; + return { + timeInMs: Math.max(minimumTime, lastTaskAt), + nodeTiming: simulationResult.nodeTiming, + }; + } + + /** + * @param {{trace: Object, devtoolsLog: Object}} data + * @param {Object} artifacts + * @return {Promise} + */ + async compute_(data, artifacts) { + const fmpResult = await artifacts.requestLanternFirstMeaningfulPaint(data, artifacts); + const metricResult = await this.computeMetricWithGraphs(data, artifacts, {fmpResult}); + metricResult.timing = Math.max(metricResult.timing, fmpResult.timing); + return metricResult; + } + + /** + * @param {Map} nodeTiming + * @return {number} + */ + static getLastLongTaskEndTime(nodeTiming, duration = 50) { + // @ts-ignore TS can't infer how the object invariants change + return Array.from(nodeTiming.entries()) + .filter(([node, timing]) => { + if (node.type !== Node.TYPES.CPU) return false; + if (!timing.endTime || !timing.startTime) return false; + return timing.endTime - timing.startTime > duration; + }) + .map(([_, timing]) => timing.endTime) + .reduce((max, x) => Math.max(max || 0, x || 0), 0); + } +} + +module.exports = ConsistentlyInteractive; diff --git a/lighthouse-core/gather/computed/metrics/lantern-first-contentful-paint.js b/lighthouse-core/gather/computed/metrics/lantern-first-contentful-paint.js new file mode 100644 index 000000000000..3a40b61952fb --- /dev/null +++ b/lighthouse-core/gather/computed/metrics/lantern-first-contentful-paint.js @@ -0,0 +1,79 @@ +/** + * @license Copyright 2018 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 MetricArtifact = require('./lantern-metric'); +const Node = require('../../../lib/dependency-graph/node'); +const CPUNode = require('../../../lib/dependency-graph/cpu-node'); // eslint-disable-line no-unused-vars +const NetworkNode = require('../../../lib/dependency-graph/network-node'); // eslint-disable-line no-unused-vars + +class FirstContentfulPaint extends MetricArtifact { + get name() { + return 'LanternFirstContentfulPaint'; + } + + /** + * @return {LH.Gatherer.Simulation.MetricCoefficients} + */ + get COEFFICIENTS() { + return { + intercept: 1440, + optimistic: -1.75, + pessimistic: 2.73, + }; + } + + /** + * @param {!Node} dependencyGraph + * @param {LH.Gatherer.Artifact.TraceOfTab} traceOfTab + * @return {!Node} + */ + getOptimisticGraph(dependencyGraph, traceOfTab) { + const fcp = traceOfTab.timestamps.firstContentfulPaint; + const blockingScriptUrls = MetricArtifact.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 /** @type {CPUNode} */ (node).isEvaluateScriptFor(blockingScriptUrls); + } + + const asNetworkNode = /** @type {NetworkNode} */ (node); + // Include non-script-initiated network requests with a render-blocking priority + return asNetworkNode.hasRenderBlockingPriority() && asNetworkNode.initiatorType !== 'script'; + }); + } + + /** + * @param {!Node} dependencyGraph + * @param {LH.Gatherer.Artifact.TraceOfTab} traceOfTab + * @return {!Node} + */ + getPessimisticGraph(dependencyGraph, traceOfTab) { + const fcp = traceOfTab.timestamps.firstContentfulPaint; + const blockingScriptUrls = MetricArtifact.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 /** @type {CPUNode} */ (node).isEvaluateScriptFor(blockingScriptUrls); + } + + // Include non-script-initiated network requests with a render-blocking priority + return /** @type {NetworkNode} */ (node).hasRenderBlockingPriority(); + }); + } +} + +module.exports = FirstContentfulPaint; diff --git a/lighthouse-core/gather/computed/metrics/lantern-first-meaningful-paint.js b/lighthouse-core/gather/computed/metrics/lantern-first-meaningful-paint.js new file mode 100644 index 000000000000..b6c3192fe860 --- /dev/null +++ b/lighthouse-core/gather/computed/metrics/lantern-first-meaningful-paint.js @@ -0,0 +1,93 @@ +/** + * @license Copyright 2018 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 MetricArtifact = require('./lantern-metric'); +const Node = require('../../../lib/dependency-graph/node'); +const CPUNode = require('../../../lib/dependency-graph/cpu-node'); // eslint-disable-line no-unused-vars +const NetworkNode = require('../../../lib/dependency-graph/network-node'); // eslint-disable-line no-unused-vars + +class FirstMeaningfulPaint extends MetricArtifact { + get name() { + return 'LanternFirstMeaningfulPaint'; + } + + /** + * @return {LH.Gatherer.Simulation.MetricCoefficients} + */ + get COEFFICIENTS() { + return { + intercept: 1532, + optimistic: -0.3, + pessimistic: 1.33, + }; + } + + /** + * @param {!Node} dependencyGraph + * @param {LH.Gatherer.Artifact.TraceOfTab} traceOfTab + * @return {!Node} + */ + getOptimisticGraph(dependencyGraph, traceOfTab) { + const fmp = traceOfTab.timestamps.firstMeaningfulPaint; + const blockingScriptUrls = MetricArtifact.getScriptUrls(dependencyGraph, node => { + return ( + node.endTime <= fmp && node.hasRenderBlockingPriority() && node.initiatorType !== 'script' + ); + }); + + return dependencyGraph.cloneWithRelationships(node => { + if (node.endTime > fmp) return false; + // Include EvaluateScript tasks for blocking scripts + if (node.type === Node.TYPES.CPU) { + return /** @type {CPUNode} */ (node).isEvaluateScriptFor(blockingScriptUrls); + } + + const asNetworkNode = /** @type {NetworkNode} */ (node); + // Include non-script-initiated network requests with a render-blocking priority + return asNetworkNode.hasRenderBlockingPriority() && asNetworkNode.initiatorType !== 'script'; + }); + } + + /** + * @param {!Node} dependencyGraph + * @param {LH.Gatherer.Artifact.TraceOfTab} traceOfTab + * @return {!Node} + */ + getPessimisticGraph(dependencyGraph, traceOfTab) { + const fmp = traceOfTab.timestamps.firstMeaningfulPaint; + const requiredScriptUrls = MetricArtifact.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 or were evaluations of required scripts + if (node.type === Node.TYPES.CPU) { + const asCpuNode = /** @type {CPUNode} */ (node); + return asCpuNode.didPerformLayout() || asCpuNode.isEvaluateScriptFor(requiredScriptUrls); + } + + // Include all network requests that had render-blocking priority (even script-initiated) + return /** @type {NetworkNode} */ (node).hasRenderBlockingPriority(); + }); + } + + /** + * @param {{trace: Object, devtoolsLog: Object}} data + * @param {Object} artifacts + * @return {Promise} + */ + async compute_(data, artifacts) { + const fcpResult = await artifacts.requestLanternFirstContentfulPaint(data, artifacts); + const metricResult = await this.computeMetricWithGraphs(data, artifacts); + metricResult.timing = Math.max(metricResult.timing, fcpResult.timing); + return metricResult; + } +} + +module.exports = FirstMeaningfulPaint; diff --git a/lighthouse-core/gather/computed/metrics/lantern-metric.js b/lighthouse-core/gather/computed/metrics/lantern-metric.js new file mode 100644 index 000000000000..578fa0e8312a --- /dev/null +++ b/lighthouse-core/gather/computed/metrics/lantern-metric.js @@ -0,0 +1,126 @@ +/** + * @license Copyright 2018 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 Node = require('../../../lib/dependency-graph/node'); +const NetworkNode = require('../../../lib/dependency-graph/network-node'); // eslint-disable-line no-unused-vars +const Simulator = require('../../../lib/dependency-graph/simulator/simulator'); +const WebInspector = require('../../../lib/web-inspector'); + +class LanternMetricArtifact extends ComputedArtifact { + /** + * @param {!Node} dependencyGraph + * @param {function(NetworkNode):boolean=} condition + * @return {!Set} + */ + static getScriptUrls(dependencyGraph, condition) { + const scriptUrls = new Set(); + + dependencyGraph.traverse(node => { + if (node.type === Node.TYPES.CPU) return; + const asNetworkNode = /** @type {NetworkNode} */ (node); + if (asNetworkNode.record._resourceType !== WebInspector.resourceTypes.Script) return; + if (condition && !condition(asNetworkNode)) return; + scriptUrls.add(asNetworkNode.record.url); + }); + + return scriptUrls; + } + + /** + * @return {LH.Gatherer.Simulation.MetricCoefficients} + */ + get COEFFICIENTS() { + throw new Error('COEFFICIENTS unimplemented!'); + } + + /** + * @param {!Node} dependencyGraph + * @param {LH.Gatherer.Artifact.TraceOfTab} traceOfTab + * @return {!Node} + */ + getOptimisticGraph(dependencyGraph, traceOfTab) { // eslint-disable-line no-unused-vars + throw new Error('Optimistic graph unimplemented!'); + } + + /** + * @param {!Node} dependencyGraph + * @param {LH.Gatherer.Artifact.TraceOfTab} traceOfTab + * @return {!Node} + */ + getPessimisticGraph(dependencyGraph, traceOfTab) { // eslint-disable-line no-unused-vars + throw new Error('Pessmistic graph unimplemented!'); + } + + /** + * @param {LH.Gatherer.Simulation.Result} simulationResult + * @param {any=} extras + * @return {LH.Gatherer.Simulation.Result} + */ + getEstimateFromSimulation(simulationResult, extras) { // eslint-disable-line no-unused-vars + return simulationResult; + } + + /** + * @param {{trace: Object, devtoolsLog: Object}} data + * @param {Object} artifacts + * @param {any=} extras + * @return {Promise} + */ + async computeMetricWithGraphs(data, artifacts, extras) { + const {trace, devtoolsLog} = data; + const graph = await artifacts.requestPageDependencyGraph({trace, devtoolsLog}); + const traceOfTab = await artifacts.requestTraceOfTab(trace); + const networkAnalysis = await artifacts.requestNetworkAnalysis(devtoolsLog); + + const optimisticGraph = this.getOptimisticGraph(graph, traceOfTab); + const pessimisticGraph = this.getPessimisticGraph(graph, traceOfTab); + + // TODO(phulce): use rtt and throughput from config.settings instead of defaults + const options = { + additionalRttByOrigin: networkAnalysis.additionalRttByOrigin, + serverResponseTimeByOrigin: networkAnalysis.serverResponseTimeByOrigin, + }; + + const optimisticSimulation = new Simulator(optimisticGraph, options).simulate(); + const pessimisticSimulation = new Simulator(pessimisticGraph, options).simulate(); + + const optimisticEstimate = this.getEstimateFromSimulation( + optimisticSimulation, + Object.assign({}, extras, {optimistic: true}) + ); + + const pessimisticEstimate = this.getEstimateFromSimulation( + pessimisticSimulation, + Object.assign({}, extras, {optimistic: false}) + ); + + const timing = + this.COEFFICIENTS.intercept + + this.COEFFICIENTS.optimistic * optimisticEstimate.timeInMs + + this.COEFFICIENTS.pessimistic * pessimisticEstimate.timeInMs; + + return { + timing, + optimisticEstimate, + pessimisticEstimate, + optimisticGraph, + pessimisticGraph, + }; + } + + /** + * @param {{trace: Object, devtoolsLog: Object}} data + * @param {Object} artifacts + * @return {Promise} + */ + compute_(data, artifacts) { + return this.computeMetricWithGraphs(data, artifacts); + } +} + +module.exports = LanternMetricArtifact; diff --git a/lighthouse-core/gather/computed/network-analysis.js b/lighthouse-core/gather/computed/network-analysis.js new file mode 100644 index 000000000000..d16d683afd6b --- /dev/null +++ b/lighthouse-core/gather/computed/network-analysis.js @@ -0,0 +1,58 @@ +/** + * @license Copyright 2017 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 NetworkAnalyzer = require('../../lib/dependency-graph/simulator/network-analyzer'); + +class NetworkAnalysis extends ComputedArtifact { + get name() { + return 'NetworkAnalysis'; + } + + /** + * @param {!Array} records + * @return {!Object} + */ + static computeRTTAndServerResponseTime(records) { + // First pass compute the estimated observed RTT to each origin's servers. + const rttByOrigin = new Map(); + for (const [origin, summary] of NetworkAnalyzer.estimateRTTByOrigin(records).entries()) { + rttByOrigin.set(origin, summary.min); + } + + // We'll use the minimum RTT as the assumed connection latency since we care about how much addt'l + // latency each origin introduces as Lantern will be simulating with its own connection latency. + const minimumRtt = Math.min(...Array.from(rttByOrigin.values())); + // We'll use the observed RTT information to help estimate the server response time + const responseTimeSummaries = NetworkAnalyzer.estimateServerResponseTimeByOrigin(records, { + rttByOrigin, + }); + + const additionalRttByOrigin = new Map(); + const serverResponseTimeByOrigin = new Map(); + for (const [origin, summary] of responseTimeSummaries.entries()) { + additionalRttByOrigin.set(origin, rttByOrigin.get(origin) - minimumRtt); + serverResponseTimeByOrigin.set(origin, summary.median); + } + + return {rtt: minimumRtt, additionalRttByOrigin, serverResponseTimeByOrigin}; + } + + /** + * @param {Object} devtoolsLog + * @return {Object} + */ + async compute_(devtoolsLog, artifacts) { + const records = await artifacts.requestNetworkRecords(devtoolsLog); + const throughput = await artifacts.requestNetworkThroughput(devtoolsLog); + const rttAndServerResponseTime = NetworkAnalysis.computeRTTAndServerResponseTime(records); + rttAndServerResponseTime.throughput = throughput; + return rttAndServerResponseTime; + } +} + +module.exports = NetworkAnalysis; diff --git a/lighthouse-core/lib/arbitrary-equality-map.js b/lighthouse-core/lib/arbitrary-equality-map.js index cc2cc582099a..7332944ead6c 100644 --- a/lighthouse-core/lib/arbitrary-equality-map.js +++ b/lighthouse-core/lib/arbitrary-equality-map.js @@ -5,6 +5,7 @@ */ 'use strict'; +// @ts-ignore const isEqual = require('lodash.isequal'); /** @@ -14,32 +15,49 @@ const isEqual = require('lodash.isequal'); */ module.exports = class ArbitraryEqualityMap { constructor() { - this._equalsFn = (a, b) => a === b; + this._equalsFn = /** @type {function(any,any):boolean} */ ((a, b) => a === b); + /** @type {Array<{key: string, value: *}>} */ this._entries = []; } /** - * @param {function():boolean} equalsFn + * @param {function(*,*):boolean} equalsFn */ setEqualityFn(equalsFn) { this._equalsFn = equalsFn; } + /** + * @param {string} key + * @return {boolean} + */ has(key) { return this._findIndexOf(key) !== -1; } + /** + * @param {string} key + * @return {*} + */ get(key) { const entry = this._entries[this._findIndexOf(key)]; return entry && entry.value; } + /** + * @param {string} key + * @param {*} value + */ set(key, value) { let index = this._findIndexOf(key); if (index === -1) index = this._entries.length; this._entries[index] = {key, value}; } + /** + * @param {string} key + * @return {number} + */ _findIndexOf(key) { for (let i = 0; i < this._entries.length; i++) { if (this._equalsFn(key, this._entries[i].key)) return i; diff --git a/lighthouse-core/lib/dependency-graph/node.js b/lighthouse-core/lib/dependency-graph/node.js index 13e2d69fb797..877882e77cc4 100644 --- a/lighthouse-core/lib/dependency-graph/node.js +++ b/lighthouse-core/lib/dependency-graph/node.js @@ -121,10 +121,9 @@ class Node { /** * Clones the entire graph connected to this node filtered by the optional predicate. If a node is * included by the predicate, all nodes along the paths between the two will be included. If the - * node that was called clone is not included in the resulting filtered graph, the return will be - * undefined. + * node that was called clone is not included in the resulting filtered graph, the method will throw. * @param {function(Node):boolean=} predicate - * @return {Node|undefined} + * @return {Node} */ cloneWithRelationships(predicate) { const rootNode = this.getRootNode(); @@ -162,6 +161,7 @@ class Node { } }); + if (!idToNodeMap.has(this.id)) throw new Error(`Cloned graph missing node ${this.id}`); return idToNodeMap.get(this.id); } diff --git a/lighthouse-core/lib/dependency-graph/simulator/simulator.js b/lighthouse-core/lib/dependency-graph/simulator/simulator.js index 668d5ff2c5ea..38fd88c9af18 100644 --- a/lighthouse-core/lib/dependency-graph/simulator/simulator.js +++ b/lighthouse-core/lib/dependency-graph/simulator/simulator.js @@ -36,7 +36,7 @@ const NodeState = { class Simulator { /** * @param {Node} graph - * @param {SimulationOptions} [options] + * @param {LH.Gatherer.Simulation.Options} [options] */ constructor(graph, options) { this._graph = graph; @@ -75,7 +75,7 @@ class Simulator { const records = []; this._graph.getRootNode().traverse(node => { if (node.type === Node.TYPES.NETWORK) { - records.push((/** @type {NetworkNode} */ (node)).record); + records.push(/** @type {NetworkNode} */ (node).record); } }); @@ -105,7 +105,7 @@ class Simulator { /** * @param {Node} node - * @param {NodeTimingData} values + * @param {LH.Gatherer.Simulation.NodeTiming} values */ _setTimingData(node, values) { const timingData = this._nodeTiming.get(node) || {}; @@ -175,7 +175,7 @@ class Simulator { // Start a network request if we're not at max requests and a connection is available const numberOfActiveRequests = this._numberInProgress(node.type); if (numberOfActiveRequests >= this._maximumConcurrentRequests) return; - const connection = this._connectionPool.acquire((/** @type {NetworkNode} */ (node)).record); + const connection = this._connectionPool.acquire(/** @type {NetworkNode} */ (node).record); if (!connection) return; this._markNodeAsInProgress(node, totalElapsedTime); @@ -204,11 +204,11 @@ class Simulator { _estimateTimeRemaining(node) { if (node.type === Node.TYPES.CPU) { const timingData = this._nodeTiming.get(node); - const multiplier = (/** @type {CpuNode} */ (node)).didPerformLayout() + const multiplier = /** @type {CpuNode} */ (node).didPerformLayout() ? this._layoutTaskMultiplier : this._cpuSlowdownMultiplier; const totalDuration = Math.min( - Math.round((/** @type {CpuNode} */ (node)).event.dur / 1000 * multiplier), + Math.round(/** @type {CpuNode} */ (node).event.dur / 1000 * multiplier), DEFAULT_MAXIMUM_CPU_TASK_DURATION ); const estimatedTimeElapsed = totalDuration - timingData.timeElapsed; @@ -218,7 +218,7 @@ class Simulator { if (node.type !== Node.TYPES.NETWORK) throw new Error('Unsupported'); - const record = (/** @type {NetworkNode} */ (node)).record; + const record = /** @type {NetworkNode} */ (node).record; const timingData = this._nodeTiming.get(node); const connection = /** @type {TcpConnection} */ (this._connectionPool.acquire(record)); const calculation = connection.simulateDownloadUntil( @@ -262,7 +262,7 @@ class Simulator { if (node.type !== Node.TYPES.NETWORK) throw new Error('Unsupported'); - const record = (/** @type {NetworkNode} */ (node)).record; + const record = /** @type {NetworkNode} */ (node).record; const connection = /** @type {TcpConnection} */ (this._connectionPool.acquire(record)); const calculation = connection.simulateDownloadUntil( record.transferSize - timingData.bytesDownloaded, @@ -288,7 +288,7 @@ class Simulator { /** * Estimates the time taken to process all of the graph's nodes. - * @return {{timeInMs: number, nodeTiming: Map}} + * @return {LH.Gatherer.Simulation.Result} */ simulate() { // initialize the necessary data containers @@ -342,25 +342,3 @@ class Simulator { } module.exports = Simulator; - -/** - * @typedef NodeTimingData - * @property {number} [startTime] - * @property {number} [endTime] - * @property {number} [queuedTime] - * @property {number} [estimatedTimeElapsed] - * @property {number} [timeElapsed] - * @property {number} [timeElapsedOvershoot] - * @property {number} [bytesDownloaded] - */ - -/** - * @typedef SimulationOptions - * @property {number} [rtt] - * @property {number} [throughput] - * @property {number} [fallbackTTFB] - * @property {number} [maximumConcurrentRequests] - * @property {number} [cpuSlowdownMultiplier] - * @property {number} [layoutTaskMultiplier] - */ - diff --git a/lighthouse-core/runner.js b/lighthouse-core/runner.js index aa943633ef05..9c4fa6eb1b79 100644 --- a/lighthouse-core/runner.js +++ b/lighthouse-core/runner.js @@ -316,17 +316,29 @@ class Runner { } /** - * @return {!ComputedArtifacts} + * Returns list of computed gatherer names for external querying. + * @return {!Array} */ - static instantiateComputedArtifacts() { - const computedArtifacts = {}; + static getComputedGathererList() { const filenamesToSkip = [ 'computed-artifact.js', // the base class which other artifacts inherit + 'metrics', // the sub folder that contains metric names + ]; + + const fileList = [ + ...fs.readdirSync(path.join(__dirname, './gather/computed')), + ...fs.readdirSync(path.join(__dirname, './gather/computed/metrics')).map(f => `metrics/${f}`), ]; - require('fs').readdirSync(__dirname + '/gather/computed').forEach(function(filename) { - if (filenamesToSkip.includes(filename)) return; + return fileList.filter(f => /\.js$/.test(f) && !filenamesToSkip.includes(f)).sort(); + } + /** + * @return {!ComputedArtifacts} + */ + static instantiateComputedArtifacts() { + const computedArtifacts = {}; + Runner.getComputedGathererList().forEach(function(filename) { // Drop `.js` suffix to keep browserify import happy. filename = filename.replace(/\.js$/, ''); const ArtifactClass = require('./gather/computed/' + filename); @@ -334,6 +346,7 @@ class Runner { // define the request* function that will be exposed on `artifacts` computedArtifacts['request' + artifact.name] = artifact.request.bind(artifact); }); + return computedArtifacts; } diff --git a/lighthouse-core/test/gather/computed/metrics/lantern-consistently-interactive-test.js b/lighthouse-core/test/gather/computed/metrics/lantern-consistently-interactive-test.js new file mode 100644 index 000000000000..99251c0c3fa9 --- /dev/null +++ b/lighthouse-core/test/gather/computed/metrics/lantern-consistently-interactive-test.js @@ -0,0 +1,28 @@ +/** + * @license Copyright 2017 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 Runner = require('../../../../runner'); +const assert = require('assert'); + +const trace = require('../../../fixtures/traces/progressive-app-m60.json'); +const devtoolsLog = require('../../../fixtures/traces/progressive-app-m60.devtools.log.json'); + +/* eslint-env mocha */ +describe('Metrics: Lantern TTCI', () => { + it('should compute predicted value', async () => { + const artifacts = Runner.instantiateComputedArtifacts(); + const result = await artifacts.requestLanternConsistentlyInteractive({trace, devtoolsLog}); + + assert.equal(Math.round(result.timing), 5308); + assert.equal(Math.round(result.optimisticEstimate.timeInMs), 2451); + assert.equal(Math.round(result.pessimisticEstimate.timeInMs), 2752); + assert.equal(result.optimisticEstimate.nodeTiming.size, 19); + assert.equal(result.pessimisticEstimate.nodeTiming.size, 79); + assert.ok(result.optimisticGraph, 'should have created optimistic graph'); + assert.ok(result.pessimisticGraph, 'should have created pessimistic graph'); + }); +}); diff --git a/lighthouse-core/test/gather/computed/metrics/lantern-first-contentful-paint-test.js b/lighthouse-core/test/gather/computed/metrics/lantern-first-contentful-paint-test.js new file mode 100644 index 000000000000..036bb64dfa3d --- /dev/null +++ b/lighthouse-core/test/gather/computed/metrics/lantern-first-contentful-paint-test.js @@ -0,0 +1,28 @@ +/** + * @license Copyright 2017 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 Runner = require('../../../../runner'); +const assert = require('assert'); + +const trace = require('../../../fixtures/traces/progressive-app-m60.json'); +const devtoolsLog = require('../../../fixtures/traces/progressive-app-m60.devtools.log.json'); + +/* eslint-env mocha */ +describe('Metrics: Lantern FCP', () => { + it('should compute predicted value', async () => { + const artifacts = Runner.instantiateComputedArtifacts(); + const result = await artifacts.requestLanternFirstContentfulPaint({trace, devtoolsLog}); + + assert.equal(Math.round(result.timing), 2038); + assert.equal(Math.round(result.optimisticEstimate.timeInMs), 611); + assert.equal(Math.round(result.pessimisticEstimate.timeInMs), 611); + assert.equal(result.optimisticEstimate.nodeTiming.size, 2); + assert.equal(result.pessimisticEstimate.nodeTiming.size, 2); + assert.ok(result.optimisticGraph, 'should have created optimistic graph'); + assert.ok(result.pessimisticGraph, 'should have created pessimistic graph'); + }); +}); diff --git a/lighthouse-core/test/gather/computed/metrics/lantern-first-meaningful-paint-test.js b/lighthouse-core/test/gather/computed/metrics/lantern-first-meaningful-paint-test.js new file mode 100644 index 000000000000..0f3a99a16dbc --- /dev/null +++ b/lighthouse-core/test/gather/computed/metrics/lantern-first-meaningful-paint-test.js @@ -0,0 +1,28 @@ +/** + * @license Copyright 2017 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 Runner = require('../../../../runner'); +const assert = require('assert'); + +const trace = require('../../../fixtures/traces/progressive-app-m60.json'); +const devtoolsLog = require('../../../fixtures/traces/progressive-app-m60.devtools.log.json'); + +/* eslint-env mocha */ +describe('Metrics: Lantern FMP', () => { + it('should compute predicted value', async () => { + const artifacts = Runner.instantiateComputedArtifacts(); + const result = await artifacts.requestLanternFirstMeaningfulPaint({trace, devtoolsLog}); + + assert.equal(Math.round(result.timing), 2851); + assert.equal(Math.round(result.optimisticEstimate.timeInMs), 911); + assert.equal(Math.round(result.pessimisticEstimate.timeInMs), 1198); + assert.equal(result.optimisticEstimate.nodeTiming.size, 4); + assert.equal(result.pessimisticEstimate.nodeTiming.size, 7); + assert.ok(result.optimisticGraph, 'should have created optimistic graph'); + assert.ok(result.pessimisticGraph, 'should have created pessimistic graph'); + }); +}); diff --git a/lighthouse-extension/gulpfile.js b/lighthouse-extension/gulpfile.js index 5c00478b259a..419dfa9ebecd 100644 --- a/lighthouse-extension/gulpfile.js +++ b/lighthouse-extension/gulpfile.js @@ -1,8 +1,6 @@ // generated on 2016-03-19 using generator-chrome-extension 0.5.4 'use strict'; -const fs = require('fs'); -const path = require('path'); const del = require('del'); const gutil = require('gulp-util'); const runSequence = require('run-sequence'); @@ -35,9 +33,7 @@ const audits = LighthouseRunner.getAuditList() const gatherers = LighthouseRunner.getGathererList() .map(f => '../lighthouse-core/gather/gatherers/' + f.replace(/\.js$/, '')); -const computedArtifacts = fs.readdirSync( - path.join(__dirname, '../lighthouse-core/gather/computed/')) - .filter(f => /\.js$/.test(f)) +const computedArtifacts = LighthouseRunner.getComputedGathererList() .map(f => '../lighthouse-core/gather/computed/' + f.replace(/\.js$/, '')); gulp.task('extras', () => { diff --git a/lighthouse-extension/test/extension-test.js b/lighthouse-extension/test/extension-test.js index 67a61effcfe9..f5b959721042 100644 --- a/lighthouse-extension/test/extension-test.js +++ b/lighthouse-extension/test/extension-test.js @@ -76,6 +76,9 @@ describe('Lighthouse chrome extension', function() { }); if (lighthouseResult.exceptionDetails) { + // Log the full result if there was an error, since the relevant information may not be found + // in the error message alone. + console.error(lighthouseResult); // eslint-disable-line no-console if (lighthouseResult.exceptionDetails.exception) { throw new Error(lighthouseResult.exceptionDetails.exception.description); } diff --git a/tsconfig.json b/tsconfig.json index 41ba828bd03b..ac2e6e2ac239 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,6 +19,7 @@ "lighthouse-core/audits/audit.js", "lighthouse-core/lib/dependency-graph/**/*.js", "lighthouse-core/lib/emulation.js", + "lighthouse-core/gather/computed/metrics/*.js", "lighthouse-core/gather/connections/**/*.js", "lighthouse-core/gather/gatherers/gatherer.js", "lighthouse-core/scripts/*.js", diff --git a/typings/gatherer.d.ts b/typings/gatherer.d.ts index 06e17c547909..72f8c7af5c02 100644 --- a/typings/gatherer.d.ts +++ b/typings/gatherer.d.ts @@ -4,6 +4,10 @@ * 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. */ +import * as _Node from '../lighthouse-core/lib/dependency-graph/node'; +import * as _NetworkNode from '../lighthouse-core/lib/dependency-graph/network-node'; +import * as _CPUNode from '../lighthouse-core/lib/dependency-graph/cpu-node'; + declare global { module LH.Gatherer { export interface PassContext { @@ -16,10 +20,83 @@ declare global { export interface LoadData { networkRecords: Array; devtoolsLog: Array; - trace: {traceEvents: Array} + trace: {traceEvents: Array}; + } + + namespace Artifact { + export interface LanternMetric { + timing: number; + optimisticEstimate: Simulation.Result; + pessimisticEstimate: Simulation.Result; + optimisticGraph: Simulation.GraphNode; + pessimisticGraph: Simulation.GraphNode; + } + + export interface TraceTimes { + navigationStart: number; + firstPaint: number; + firstContentfulPaint: number; + firstMeaningfulPaint: number; + traceEnd: number; + onLoad: number; + domContentLoaded: number; + } + + export interface TraceOfTab { + timings: TraceTimes; + timestamps: TraceTimes; + processEvents: Array; + mainThreadEvents: Array; + startedInPageEvt: TraceEvent; + navigationStartEvt: TraceEvent; + firstPaintEvt: TraceEvent; + firstContentfulPaintEvt: TraceEvent; + firstMeaningfulPaintEvt: TraceEvent; + onLoadEvt: TraceEvent; + fmpFellBack: boolean; + } + } + + namespace Simulation { + // HACK: TS treats 'import * as Foo' as namespace instead of a type, use typeof and prototype + export type GraphNode = InstanceType; + export type GraphNetworkNode = InstanceType; + export type GraphCPUNode = InstanceType; + + export interface MetricCoefficients { + intercept: number; + optimistic: number; + pessimistic: number; + } + + export interface Options { + rtt?: number; + throughput?: number; + fallbackTTFB?: number; + maximumConcurrentRequests?: number; + cpuSlowdownMultiplier?: number; + layoutTaskMultiplier?: number; + additionalRttByOrigin?: Map; + serverResponseTimeByOrigin?: Map; + } + + export interface NodeTiming { + startTime?: number; + endTime?: number; + queuedTime?: number; + estimatedTimeElapsed?: number; + timeElapsed?: number; + timeElapsedOvershoot?: number; + bytesDownloaded?: number; + } + + export interface Result { + timeInMs: number; + nodeTiming: Map; + } } } } // empty export to keep file a module -export {} +export {};