core(lantern): move metrics to computed artifacts (#4766)

This commit is contained in:
Patrick Hulce 2018-03-30 15:16:01 -07:00 коммит произвёл GitHub
Родитель 65dbd14fcc
Коммит 25913c7b2d
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
21 изменённых файлов: 730 добавлений и 333 удалений

Просмотреть файл

@ -90,7 +90,7 @@ function writeFile(filePath, output, outputMode) {
* @param {!LH.Results} results * @param {!LH.Results} results
* @param {string} mode * @param {string} mode
* @param {string} path * @param {string} path
* @return {!Promise<!LH.Results>} * @return {Promise<LH.Results>}
*/ */
function write(results, mode, path) { function write(results, mode, path) {
return new Promise((resolve, reject) => { 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 * Attempts to connect to an instance of Chrome with an open remote-debugging
* port. If none is found, launches a debuggable instance. * port. If none is found, launches a debuggable instance.
* @param {!LH.Flags} flags * @param {!LH.Flags} flags
* @return {!Promise<!LH.LaunchedChrome>} * @return {Promise<LH.LaunchedChrome>}
*/ */
function getDebuggableChrome(flags) { function getDebuggableChrome(flags) {
return ChromeLauncher.launch({ return ChromeLauncher.launch({
@ -98,7 +98,7 @@ function handleError(err) {
* @param {!LH.Results} results * @param {!LH.Results} results
* @param {!Object} artifacts * @param {!Object} artifacts
* @param {!LH.Flags} flags * @param {!LH.Flags} flags
* @return {!Promise<void>} * @return {Promise<void>}
*/ */
function saveResults(results, artifacts, flags) { function saveResults(results, artifacts, flags) {
const cwd = process.cwd(); const cwd = process.cwd();
@ -149,7 +149,7 @@ function saveResults(results, artifacts, flags) {
* @param {string} url * @param {string} url
* @param {!LH.Flags} flags * @param {!LH.Flags} flags
* @param {!LH.Config|undefined} config * @param {!LH.Config|undefined} config
* @return {!Promise<!LH.Results|void>} * @return {Promise<LH.Results|void>}
*/ */
function runLighthouse(url, flags, config) { function runLighthouse(url, flags, config) {
/** @type {!LH.LaunchedChrome} */ /** @type {!LH.LaunchedChrome} */
@ -183,7 +183,7 @@ function runLighthouse(url, flags, config) {
}); });
/** /**
* @return {!Promise<{}>} * @return {Promise<{}>}
*/ */
function potentiallyKillChrome() { function potentiallyKillChrome() {
if (launchedChrome !== undefined) { if (launchedChrome !== undefined) {

Просмотреть файл

@ -6,7 +6,8 @@
'use strict'; 'use strict';
const Audit = require('../audit'); 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 LoadSimulator = require('../../lib/dependency-graph/simulator/simulator.js');
const KB_IN_BYTES = 1024; const KB_IN_BYTES = 1024;
@ -120,8 +121,8 @@ class UnusedBytes extends Audit {
}); });
const savingsOnTTI = Math.max( const savingsOnTTI = Math.max(
PredictivePerf.getLastLongTaskEndTime(simulationBeforeChanges.nodeTiming) - ConsistentlyInteractive.getLastLongTaskEndTime(simulationBeforeChanges.nodeTiming) -
PredictivePerf.getLastLongTaskEndTime(simulationAfterChanges.nodeTiming), ConsistentlyInteractive.getLastLongTaskEndTime(simulationAfterChanges.nodeTiming),
0 0
); );
@ -135,7 +136,11 @@ class UnusedBytes extends Audit {
* @return {!AuditResult} * @return {!AuditResult}
*/ */
static createAuditResult(result, graph) { 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 // TODO: calibrate multipliers, see https://github.com/GoogleChrome/lighthouse/issues/820
Object.assign(simulatorOptions, {cpuSlowdownMultiplier: 1, layoutTaskMultiplier: 1}); Object.assign(simulatorOptions, {cpuSlowdownMultiplier: 1, layoutTaskMultiplier: 1});
const simulator = new LoadSimulator(graph, simulatorOptions); const simulator = new LoadSimulator(graph, simulatorOptions);

Просмотреть файл

@ -7,37 +7,12 @@
const Audit = require('./audit'); const Audit = require('./audit');
const Util = require('../report/v2/renderer/util'); 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: // Parameters (in ms) for log-normal CDF scoring. To see the curve:
// https://www.desmos.com/calculator/rjp0lbit8y // https://www.desmos.com/calculator/rjp0lbit8y
const SCORING_POINT_OF_DIMINISHING_RETURNS = 1700; const SCORING_POINT_OF_DIMINISHING_RETURNS = 1700;
const SCORING_MEDIAN = 10000; 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 { class PredictivePerf extends Audit {
/** /**
* @return {!AuditMeta} * @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 * @param {!Artifacts} artifacts
* @return {!AuditResult} * @return {!AuditResult}
*/ */
static audit(artifacts) { static async audit(artifacts) {
const trace = artifacts.traces[Audit.DEFAULT_PASS]; const trace = artifacts.traces[Audit.DEFAULT_PASS];
const devtoolsLog = artifacts.devtoolsLogs[Audit.DEFAULT_PASS]; const devtoolsLog = artifacts.devtoolsLogs[Audit.DEFAULT_PASS];
return Promise.all([ const fcp = await artifacts.requestLanternFirstContentfulPaint({trace, devtoolsLog});
artifacts.requestPageDependencyGraph({trace, devtoolsLog}), const fmp = await artifacts.requestLanternFirstMeaningfulPaint({trace, devtoolsLog});
artifacts.requestTraceOfTab(trace), const ttci = await artifacts.requestLanternConsistentlyInteractive({trace, devtoolsLog});
]).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 values = {
const options = PredictivePerf.computeRTTAndServerResponseTime(graph); roughEstimateOfFCP: fcp.timing,
Object.keys(graphs).forEach(key => { optimisticFCP: fcp.optimisticEstimate.timeInMs,
const estimate = new LoadSimulator(graphs[key], options).simulate(); pessimisticFCP: fcp.pessimisticEstimate.timeInMs,
const longTaskThreshold = key.startsWith('optimistic') ? 100 : 50;
const lastLongTaskEnd = PredictivePerf.getLastLongTaskEndTime(
estimate.nodeTiming,
longTaskThreshold
);
switch (key) { roughEstimateOfFMP: fmp.timing,
case 'optimisticFCP': optimisticFMP: fmp.optimisticEstimate.timeInMs,
case 'pessimisticFCP': pessimisticFMP: fmp.pessimisticEstimate.timeInMs,
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 = roughEstimateOfTTCI: ttci.timing,
COEFFICIENTS.FCP.intercept + optimisticTTCI: ttci.optimisticEstimate.timeInMs,
COEFFICIENTS.FCP.optimistic * values.optimisticFCP + pessimisticTTCI: ttci.pessimisticEstimate.timeInMs,
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 const score = Audit.computeLogNormalScore(
// theoretically possible, so take the maximum if this happens. values.roughEstimateOfTTCI,
values.roughEstimateOfFMP = Math.max(values.roughEstimateOfFCP, values.roughEstimateOfFMP); SCORING_POINT_OF_DIMINISHING_RETURNS,
values.roughEstimateOfTTCI = Math.max(values.roughEstimateOfFMP, values.roughEstimateOfTTCI); SCORING_MEDIAN
);
const score = Audit.computeLogNormalScore( return {
values.roughEstimateOfTTCI, score,
SCORING_POINT_OF_DIMINISHING_RETURNS, rawValue: values.roughEstimateOfTTCI,
SCORING_MEDIAN 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 * 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. * 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'; 'use strict';
const ArbitraryEqualityMap = require('../../lib/arbitrary-equality-map'); 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'; 'use strict';
// @ts-ignore
const isEqual = require('lodash.isequal'); const isEqual = require('lodash.isequal');
/** /**
@ -14,32 +15,49 @@ const isEqual = require('lodash.isequal');
*/ */
module.exports = class ArbitraryEqualityMap { module.exports = class ArbitraryEqualityMap {
constructor() { 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 = []; this._entries = [];
} }
/** /**
* @param {function():boolean} equalsFn * @param {function(*,*):boolean} equalsFn
*/ */
setEqualityFn(equalsFn) { setEqualityFn(equalsFn) {
this._equalsFn = equalsFn; this._equalsFn = equalsFn;
} }
/**
* @param {string} key
* @return {boolean}
*/
has(key) { has(key) {
return this._findIndexOf(key) !== -1; return this._findIndexOf(key) !== -1;
} }
/**
* @param {string} key
* @return {*}
*/
get(key) { get(key) {
const entry = this._entries[this._findIndexOf(key)]; const entry = this._entries[this._findIndexOf(key)];
return entry && entry.value; return entry && entry.value;
} }
/**
* @param {string} key
* @param {*} value
*/
set(key, value) { set(key, value) {
let index = this._findIndexOf(key); let index = this._findIndexOf(key);
if (index === -1) index = this._entries.length; if (index === -1) index = this._entries.length;
this._entries[index] = {key, value}; this._entries[index] = {key, value};
} }
/**
* @param {string} key
* @return {number}
*/
_findIndexOf(key) { _findIndexOf(key) {
for (let i = 0; i < this._entries.length; i++) { for (let i = 0; i < this._entries.length; i++) {
if (this._equalsFn(key, this._entries[i].key)) return 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 * 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 * 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 * node that was called clone is not included in the resulting filtered graph, the method will throw.
* undefined.
* @param {function(Node):boolean=} predicate * @param {function(Node):boolean=} predicate
* @return {Node|undefined} * @return {Node}
*/ */
cloneWithRelationships(predicate) { cloneWithRelationships(predicate) {
const rootNode = this.getRootNode(); 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); return idToNodeMap.get(this.id);
} }

Просмотреть файл

@ -36,7 +36,7 @@ const NodeState = {
class Simulator { class Simulator {
/** /**
* @param {Node} graph * @param {Node} graph
* @param {SimulationOptions} [options] * @param {LH.Gatherer.Simulation.Options} [options]
*/ */
constructor(graph, options) { constructor(graph, options) {
this._graph = graph; this._graph = graph;
@ -75,7 +75,7 @@ class Simulator {
const records = []; const records = [];
this._graph.getRootNode().traverse(node => { this._graph.getRootNode().traverse(node => {
if (node.type === Node.TYPES.NETWORK) { 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 {Node} node
* @param {NodeTimingData} values * @param {LH.Gatherer.Simulation.NodeTiming} values
*/ */
_setTimingData(node, values) { _setTimingData(node, values) {
const timingData = this._nodeTiming.get(node) || {}; 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 // Start a network request if we're not at max requests and a connection is available
const numberOfActiveRequests = this._numberInProgress(node.type); const numberOfActiveRequests = this._numberInProgress(node.type);
if (numberOfActiveRequests >= this._maximumConcurrentRequests) return; 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; if (!connection) return;
this._markNodeAsInProgress(node, totalElapsedTime); this._markNodeAsInProgress(node, totalElapsedTime);
@ -204,11 +204,11 @@ class Simulator {
_estimateTimeRemaining(node) { _estimateTimeRemaining(node) {
if (node.type === Node.TYPES.CPU) { if (node.type === Node.TYPES.CPU) {
const timingData = this._nodeTiming.get(node); const timingData = this._nodeTiming.get(node);
const multiplier = (/** @type {CpuNode} */ (node)).didPerformLayout() const multiplier = /** @type {CpuNode} */ (node).didPerformLayout()
? this._layoutTaskMultiplier ? this._layoutTaskMultiplier
: this._cpuSlowdownMultiplier; : this._cpuSlowdownMultiplier;
const totalDuration = Math.min( 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 DEFAULT_MAXIMUM_CPU_TASK_DURATION
); );
const estimatedTimeElapsed = totalDuration - timingData.timeElapsed; const estimatedTimeElapsed = totalDuration - timingData.timeElapsed;
@ -218,7 +218,7 @@ class Simulator {
if (node.type !== Node.TYPES.NETWORK) throw new Error('Unsupported'); 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 timingData = this._nodeTiming.get(node);
const connection = /** @type {TcpConnection} */ (this._connectionPool.acquire(record)); const connection = /** @type {TcpConnection} */ (this._connectionPool.acquire(record));
const calculation = connection.simulateDownloadUntil( const calculation = connection.simulateDownloadUntil(
@ -262,7 +262,7 @@ class Simulator {
if (node.type !== Node.TYPES.NETWORK) throw new Error('Unsupported'); 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 connection = /** @type {TcpConnection} */ (this._connectionPool.acquire(record));
const calculation = connection.simulateDownloadUntil( const calculation = connection.simulateDownloadUntil(
record.transferSize - timingData.bytesDownloaded, record.transferSize - timingData.bytesDownloaded,
@ -288,7 +288,7 @@ class Simulator {
/** /**
* Estimates the time taken to process all of the graph's nodes. * 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() { simulate() {
// initialize the necessary data containers // initialize the necessary data containers
@ -342,25 +342,3 @@ class Simulator {
} }
module.exports = 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(); 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} * @return {!ComputedArtifacts}
*/ */
static instantiateComputedArtifacts() { static instantiateComputedArtifacts() {
const computedArtifacts = {}; const computedArtifacts = {};
const filenamesToSkip = [ Runner.getComputedGathererList().forEach(function(filename) {
'computed-artifact.js', // the base class which other artifacts inherit
];
require('fs').readdirSync(__dirname + '/gather/computed').forEach(function(filename) {
if (filenamesToSkip.includes(filename)) return;
// Drop `.js` suffix to keep browserify import happy. // Drop `.js` suffix to keep browserify import happy.
filename = filename.replace(/\.js$/, ''); filename = filename.replace(/\.js$/, '');
const ArtifactClass = require('./gather/computed/' + filename); const ArtifactClass = require('./gather/computed/' + filename);
@ -334,6 +346,7 @@ class Runner {
// define the request* function that will be exposed on `artifacts` // define the request* function that will be exposed on `artifacts`
computedArtifacts['request' + artifact.name] = artifact.request.bind(artifact); computedArtifacts['request' + artifact.name] = artifact.request.bind(artifact);
}); });
return computedArtifacts; 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 // generated on 2016-03-19 using generator-chrome-extension 0.5.4
'use strict'; 'use strict';
const fs = require('fs');
const path = require('path');
const del = require('del'); const del = require('del');
const gutil = require('gulp-util'); const gutil = require('gulp-util');
const runSequence = require('run-sequence'); const runSequence = require('run-sequence');
@ -35,9 +33,7 @@ const audits = LighthouseRunner.getAuditList()
const gatherers = LighthouseRunner.getGathererList() const gatherers = LighthouseRunner.getGathererList()
.map(f => '../lighthouse-core/gather/gatherers/' + f.replace(/\.js$/, '')); .map(f => '../lighthouse-core/gather/gatherers/' + f.replace(/\.js$/, ''));
const computedArtifacts = fs.readdirSync( const computedArtifacts = LighthouseRunner.getComputedGathererList()
path.join(__dirname, '../lighthouse-core/gather/computed/'))
.filter(f => /\.js$/.test(f))
.map(f => '../lighthouse-core/gather/computed/' + f.replace(/\.js$/, '')); .map(f => '../lighthouse-core/gather/computed/' + f.replace(/\.js$/, ''));
gulp.task('extras', () => { gulp.task('extras', () => {

Просмотреть файл

@ -76,6 +76,9 @@ describe('Lighthouse chrome extension', function() {
}); });
if (lighthouseResult.exceptionDetails) { 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) { if (lighthouseResult.exceptionDetails.exception) {
throw new Error(lighthouseResult.exceptionDetails.exception.description); throw new Error(lighthouseResult.exceptionDetails.exception.description);
} }

Просмотреть файл

@ -19,6 +19,7 @@
"lighthouse-core/audits/audit.js", "lighthouse-core/audits/audit.js",
"lighthouse-core/lib/dependency-graph/**/*.js", "lighthouse-core/lib/dependency-graph/**/*.js",
"lighthouse-core/lib/emulation.js", "lighthouse-core/lib/emulation.js",
"lighthouse-core/gather/computed/metrics/*.js",
"lighthouse-core/gather/connections/**/*.js", "lighthouse-core/gather/connections/**/*.js",
"lighthouse-core/gather/gatherers/gatherer.js", "lighthouse-core/gather/gatherers/gatherer.js",
"lighthouse-core/scripts/*.js", "lighthouse-core/scripts/*.js",

81
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. * 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 { declare global {
module LH.Gatherer { module LH.Gatherer {
export interface PassContext { export interface PassContext {
@ -16,10 +20,83 @@ declare global {
export interface LoadData { export interface LoadData {
networkRecords: Array<void>; networkRecords: Array<void>;
devtoolsLog: 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 // empty export to keep file a module
export {} export {};