core(lantern): move metrics to computed artifacts (#4766)
This commit is contained in:
Родитель
65dbd14fcc
Коммит
25913c7b2d
|
@ -90,7 +90,7 @@ function writeFile(filePath, output, outputMode) {
|
|||
* @param {!LH.Results} results
|
||||
* @param {string} mode
|
||||
* @param {string} path
|
||||
* @return {!Promise<!LH.Results>}
|
||||
* @return {Promise<LH.Results>}
|
||||
*/
|
||||
function write(results, mode, path) {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
|
|
@ -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<!LH.LaunchedChrome>}
|
||||
* @return {Promise<LH.LaunchedChrome>}
|
||||
*/
|
||||
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<void>}
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
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<!LH.Results|void>}
|
||||
* @return {Promise<LH.Results|void>}
|
||||
*/
|
||||
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) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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<string>}
|
||||
*/
|
||||
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<!Node, {startTime, endTime}>} 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 fcp = await artifacts.requestLanternFirstContentfulPaint({trace, devtoolsLog});
|
||||
const fmp = await artifacts.requestLanternFirstMeaningfulPaint({trace, devtoolsLog});
|
||||
const ttci = await artifacts.requestLanternConsistentlyInteractive({trace, devtoolsLog});
|
||||
|
||||
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
|
||||
);
|
||||
const values = {
|
||||
roughEstimateOfFCP: fcp.timing,
|
||||
optimisticFCP: fcp.optimisticEstimate.timeInMs,
|
||||
pessimisticFCP: fcp.pessimisticEstimate.timeInMs,
|
||||
|
||||
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;
|
||||
}
|
||||
});
|
||||
roughEstimateOfFMP: fmp.timing,
|
||||
optimisticFMP: fmp.optimisticEstimate.timeInMs,
|
||||
pessimisticFMP: fmp.pessimisticEstimate.timeInMs,
|
||||
|
||||
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;
|
||||
roughEstimateOfTTCI: ttci.timing,
|
||||
optimisticTTCI: ttci.optimisticEstimate.timeInMs,
|
||||
pessimisticTTCI: ttci.pessimisticEstimate.timeInMs,
|
||||
};
|
||||
|
||||
// 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(
|
||||
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},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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<LH.Gatherer.Artifact.LanternMetric>}
|
||||
*/
|
||||
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<Node, {startTime?: number, endTime?: number}>} 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;
|
|
@ -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;
|
|
@ -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<LH.Gatherer.Artifact.LanternMetric>}
|
||||
*/
|
||||
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;
|
|
@ -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<string>}
|
||||
*/
|
||||
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<LH.Gatherer.Artifact.LanternMetric>}
|
||||
*/
|
||||
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<LH.Gatherer.Artifact.LanternMetric>}
|
||||
*/
|
||||
compute_(data, artifacts) {
|
||||
return this.computeMetricWithGraphs(data, artifacts);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = LanternMetricArtifact;
|
|
@ -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;
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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<Node, NodeTimingData>}}
|
||||
* @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]
|
||||
*/
|
||||
|
||||
|
|
|
@ -315,18 +315,30 @@ class Runner {
|
|||
return fileList.filter(f => /\.js$/.test(f) && f !== 'gatherer.js').sort();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns list of computed gatherer names for external querying.
|
||||
* @return {!Array<string>}
|
||||
*/
|
||||
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}`),
|
||||
];
|
||||
|
||||
return fileList.filter(f => /\.js$/.test(f) && !filenamesToSkip.includes(f)).sort();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!ComputedArtifacts}
|
||||
*/
|
||||
static instantiateComputedArtifacts() {
|
||||
const computedArtifacts = {};
|
||||
const filenamesToSkip = [
|
||||
'computed-artifact.js', // the base class which other artifacts inherit
|
||||
];
|
||||
|
||||
require('fs').readdirSync(__dirname + '/gather/computed').forEach(function(filename) {
|
||||
if (filenamesToSkip.includes(filename)) return;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -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', () => {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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<void>;
|
||||
devtoolsLog: Array<void>;
|
||||
trace: {traceEvents: Array<TraceEvent>}
|
||||
trace: {traceEvents: Array<TraceEvent>};
|
||||
}
|
||||
|
||||
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<TraceEvent>;
|
||||
mainThreadEvents: Array<TraceEvent>;
|
||||
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<typeof _Node>;
|
||||
export type GraphNetworkNode = InstanceType<typeof _NetworkNode>;
|
||||
export type GraphCPUNode = InstanceType<typeof _CPUNode>;
|
||||
|
||||
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<string, number>;
|
||||
serverResponseTimeByOrigin?: Map<string, number>;
|
||||
}
|
||||
|
||||
export interface NodeTiming {
|
||||
startTime?: number;
|
||||
endTime?: number;
|
||||
queuedTime?: number;
|
||||
estimatedTimeElapsed?: number;
|
||||
timeElapsed?: number;
|
||||
timeElapsedOvershoot?: number;
|
||||
bytesDownloaded?: number;
|
||||
}
|
||||
|
||||
export interface Result {
|
||||
timeInMs: number;
|
||||
nodeTiming: Map<GraphNode, NodeTiming>;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// empty export to keep file a module
|
||||
export {}
|
||||
export {};
|
||||
|
|
Загрузка…
Ссылка в новой задаче