Skip to content

Commit

Permalink
core(tbt): clean up total blocking time metric (#9409)
Browse files Browse the repository at this point in the history
  • Loading branch information
deepanjanroy authored and patrickhulce committed Jul 23, 2019
1 parent 39b6336 commit 85429f9
Show file tree
Hide file tree
Showing 14 changed files with 281 additions and 228 deletions.
4 changes: 2 additions & 2 deletions lighthouse-cli/test/cli/__snapshots__/index-test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ Object {
"path": "metrics/estimated-input-latency",
},
Object {
"path": "metrics/cumulative-long-queuing-delay",
"path": "metrics/total-blocking-time",
},
Object {
"path": "metrics/max-potential-fid",
Expand Down Expand Up @@ -717,7 +717,7 @@ Object {
"weight": 0,
},
Object {
"id": "cumulative-long-queuing-delay",
"id": "total-blocking-time",
"weight": 0,
},
Object {
Expand Down
8 changes: 4 additions & 4 deletions lighthouse-core/audits/metrics.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const FirstCPUIdle = require('../computed/metrics/first-cpu-idle.js');
const Interactive = require('../computed/metrics/interactive.js');
const SpeedIndex = require('../computed/metrics/speed-index.js');
const EstimatedInputLatency = require('../computed/metrics/estimated-input-latency.js');
const CumulativeLongQueuingDelay = require('../computed/metrics/cumulative-long-queuing-delay.js');
const TotalBlockingTime = require('../computed/metrics/total-blocking-time.js');

class Metrics extends Audit {
/**
Expand Down Expand Up @@ -60,7 +60,7 @@ class Metrics extends Audit {
const interactive = await requestOrUndefined(Interactive, metricComputationData);
const speedIndex = await requestOrUndefined(SpeedIndex, metricComputationData);
const estimatedInputLatency = await EstimatedInputLatency.request(metricComputationData, context); // eslint-disable-line max-len
const cumulativeLongQueuingDelay = await CumulativeLongQueuingDelay.request(metricComputationData, context); // eslint-disable-line max-len
const totalBlockingTime = await TotalBlockingTime.request(metricComputationData, context); // eslint-disable-line max-len

/** @type {UberMetricsItem} */
const metrics = {
Expand All @@ -77,7 +77,7 @@ class Metrics extends Audit {
speedIndexTs: speedIndex && speedIndex.timestamp,
estimatedInputLatency: estimatedInputLatency.timing,
estimatedInputLatencyTs: estimatedInputLatency.timestamp,
cumulativeLongQueuingDelay: cumulativeLongQueuingDelay.timing,
totalBlockingTime: totalBlockingTime.timing,

// Include all timestamps of interest from trace of tab
observedNavigationStart: traceOfTab.timings.navigationStart,
Expand Down Expand Up @@ -140,7 +140,7 @@ class Metrics extends Audit {
* @property {number=} speedIndexTs
* @property {number} estimatedInputLatency
* @property {number=} estimatedInputLatencyTs
* @property {number} cumulativeLongQueuingDelay
* @property {number} totalBlockingTime
* @property {number} observedNavigationStart
* @property {number} observedNavigationStartTs
* @property {number=} observedFirstPaint
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,28 @@
'use strict';

const Audit = require('../audit.js');
const CumulativeLQD = require('../../computed/metrics/cumulative-long-queuing-delay.js');
const ComputedTBT = require('../../computed/metrics/total-blocking-time.js');
const i18n = require('../../lib/i18n/i18n.js');

// TODO(deepanjanroy): i18n strings once metric is final.
const UIStringsNotExported = {
title: 'Cumulative Long Queuing Delay',
description: '[Experimental metric] Total time period between FCP and Time to Interactive ' +
'during which queuing time for any input event would be higher than 50ms.',
const UIStrings = {
/** The name of a metric that calculates the total duration of blocking time for a web page. Blocking times are time periods when the page would be blocked (prevented) from responding to user input (clicks, taps, and keypresses will feel slow to respond). Shown to users as the label for the numeric metric value. Ideally fits within a ~40 character limit. */
title: 'Total Blocking Time',
/** Description of the Total Blocking Time (TBT) metric, which calculates the total duration of blocking time for a web page. Blocking times are time periods when the page would be blocked (prevented) from responding to user input (clicks, taps, and keypresses will feel slow to respond). This is displayed within a tooltip when the user hovers on the metric name to see more. No character length limits.*/
description: 'Sum of all time periods between FCP and Time to Interactive, ' +
'when task length exceeded 50ms, expressed in milliseconds.',
};

class CumulativeLongQueuingDelay extends Audit {
const str_ = i18n.createMessageInstanceIdFn(__filename, UIStrings);

class TotalBlockingTime extends Audit {
/**
* @return {LH.Audit.Meta}
*/
static get meta() {
return {
id: 'cumulative-long-queuing-delay',
title: UIStringsNotExported.title,
description: UIStringsNotExported.description,
id: 'total-blocking-time',
title: str_(UIStrings.title),
description: str_(UIStrings.description),
scoreDisplayMode: Audit.SCORING_MODES.NUMERIC,
requiredArtifacts: ['traces', 'devtoolsLogs'],
};
Expand All @@ -46,13 +50,12 @@ class CumulativeLongQueuingDelay extends Audit {
}

/**
* Audits the page to calculate Cumulative Long Queuing Delay.
* Audits the page to calculate Total Blocking Time.
*
* We define Long Queuing Delay Region as any time interval in the loading timeline where queuing
* time for an input event would be longer than 50ms. For example, if there is a 110ms main thread
* task, the first 60ms of it is Long Queuing Delay Region, because any input event occuring in
* that region has to wait more than 50ms. Cumulative Long Queuing Delay is the sum of all Long
* Queuing Delay Regions between First Contentful Paint and Interactive Time (TTI).
* We define Blocking Time as any time interval in the loading timeline where task length exceeds
* 50ms. For example, if there is a 110ms main thread task, the last 60ms of it is blocking time.
* Total Blocking Time is the sum of all Blocking Time between First Contentful Paint and
* Interactive Time (TTI).
*
* @param {LH.Artifacts} artifacts
* @param {LH.Audit.Context} context
Expand All @@ -62,7 +65,7 @@ class CumulativeLongQueuingDelay extends Audit {
const trace = artifacts.traces[Audit.DEFAULT_PASS];
const devtoolsLog = artifacts.devtoolsLogs[Audit.DEFAULT_PASS];
const metricComputationData = {trace, devtoolsLog, settings: context.settings};
const metricResult = await CumulativeLQD.request(metricComputationData, context);
const metricResult = await ComputedTBT.request(metricComputationData, context);

return {
score: Audit.computeLogNormalScore(
Expand All @@ -71,9 +74,10 @@ class CumulativeLongQueuingDelay extends Audit {
context.options.scoreMedian
),
numericValue: metricResult.timing,
displayValue: 10 * Math.round(metricResult.timing / 10) + '\xa0ms',
displayValue: str_(i18n.UIStrings.ms, {timeInMs: metricResult.timing}),
};
}
}

module.exports = CumulativeLongQueuingDelay;
module.exports = TotalBlockingTime;
module.exports.UIStrings = UIStrings;
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const LanternInteractive = require('./lantern-interactive.js');

/** @typedef {BaseNode.Node} Node */

class LanternCumulativeLongQueuingDelay extends LanternMetric {
class LanternTotalBlockingTime extends LanternMetric {
/**
* @return {LH.Gatherer.Simulation.MetricCoefficients}
*/
Expand Down Expand Up @@ -48,32 +48,32 @@ class LanternCumulativeLongQueuingDelay extends LanternMetric {
*/
static getEstimateFromSimulation(simulation, extras) {
// Intentionally use the opposite FCP estimate. A pessimistic FCP is higher than equal to an
// optimistic FCP, which means potentially more tasks are excluded from the
// CumulativeLongQueuingDelay computation. So a more pessimistic FCP gives a more optimistic
// CumulativeLongQueuingDelay for the same work.
// optimistic FCP, which means potentially more tasks are excluded from the Total Blocking Time
// computation. So a more pessimistic FCP gives a more optimistic Total Blocking Time for the
// same work.
const fcpTimeInMs = extras.optimistic
? extras.fcpResult.pessimisticEstimate.timeInMs
: extras.fcpResult.optimisticEstimate.timeInMs;

// Similarly, we always have pessimistic TTI >= optimistic TTI. Therefore, picking optimistic
// TTI means our window of interest is smaller and thus potentially more tasks are excluded from
// CumulativeLongQueuingDelay computation, yielding a lower (more optimistic)
// CumulativeLongQueuingDelay value for the same work.
// Total Blocking Time computation, yielding a lower (more optimistic) Total Blocking Time value
// for the same work.
const interactiveTimeMs = extras.optimistic
? extras.interactiveResult.optimisticEstimate.timeInMs
: extras.interactiveResult.pessimisticEstimate.timeInMs;

// Require here to resolve circular dependency.
const CumulativeLongQueuingDelay = require('./cumulative-long-queuing-delay.js');
const minDurationMs = CumulativeLongQueuingDelay.LONG_QUEUING_DELAY_THRESHOLD;
const TotalBlockingTime = require('./total-blocking-time.js');
const minDurationMs = TotalBlockingTime.BLOCKING_TIME_THRESHOLD;

const events = LanternCumulativeLongQueuingDelay.getTopLevelEvents(
const events = LanternTotalBlockingTime.getTopLevelEvents(
simulation.nodeTimings,
minDurationMs
);

return {
timeInMs: CumulativeLongQueuingDelay.calculateSumOfLongQueuingDelay(
timeInMs: TotalBlockingTime.calculateSumOfBlockingTime(
events,
fcpTimeInMs,
interactiveTimeMs
Expand Down Expand Up @@ -118,4 +118,4 @@ class LanternCumulativeLongQueuingDelay extends LanternMetric {
}
}

module.exports = makeComputedArtifact(LanternCumulativeLongQueuingDelay);
module.exports = makeComputedArtifact(LanternTotalBlockingTime);
Original file line number Diff line number Diff line change
Expand Up @@ -9,28 +9,27 @@ const makeComputedArtifact = require('../computed-artifact.js');
const ComputedMetric = require('./metric.js');
const LHError = require('../../lib/lh-error.js');
const TracingProcessor = require('../../lib/tracehouse/trace-processor.js');
const LanternCumulativeLongQueuingDelay = require('./lantern-cumulative-long-queuing-delay.js');
const LanternTotalBlockingTime = require('./lantern-total-blocking-time.js');
const TimetoInteractive = require('./interactive.js');

/**
* @fileoverview This audit determines Cumulative Long Queuing Delay between FCP and TTI.
* @fileoverview This audit determines Total Blocking Time.
* We define Long Queuing Delay Region as any time interval in the loading timeline where queuing
* time for an input event would be longer than 50ms. For example, if there is a 110ms main thread
* task, the first 60ms of it is Long Queuing Delay Region, because any input event occuring in
* that region has to wait more than 50ms. Cumulative Long Queuing Delay is the sum of all Long
* Queuing Delay Regions between First Contentful Paint and Interactive Time (TTI).
* We define Blocking Time as any time interval in the loading timeline where task length exceeds
* 50ms. For example, if there is a 110ms main thread task, the last 60ms of it is blocking time.
* Total Blocking Time is the sum of all Blocking Time between First Contentful Paint and
* Interactive Time (TTI).
*
* This is a new metric designed to accompany Time to Interactive. TTI is strict and does not
* reflect incremental improvements to the site performance unless the improvement concerns the last
* long task. Cumulative Long Queuing Delay on the other hand is designed to be much more responsive
* long task. Total Blocking Time on the other hand is designed to be much more responsive
* to smaller improvements to main thread responsiveness.
*/
class CumulativeLongQueuingDelay extends ComputedMetric {
class TotalBlockingTime extends ComputedMetric {
/**
* @return {number}
*/
static get LONG_QUEUING_DELAY_THRESHOLD() {
static get BLOCKING_TIME_THRESHOLD() {
return 50;
}
/**
Expand All @@ -39,50 +38,43 @@ class CumulativeLongQueuingDelay extends ComputedMetric {
* @param {number} interactiveTimeMs
* @return {number}
*/
static calculateSumOfLongQueuingDelay(topLevelEvents, fcpTimeInMs, interactiveTimeMs) {
static calculateSumOfBlockingTime(topLevelEvents, fcpTimeInMs, interactiveTimeMs) {
if (interactiveTimeMs <= fcpTimeInMs) return 0;

const threshold = CumulativeLongQueuingDelay.LONG_QUEUING_DELAY_THRESHOLD;
const longQueuingDelayRegions = [];
// First identifying the long queuing delay regions.
const threshold = TotalBlockingTime.BLOCKING_TIME_THRESHOLD;
let sumBlockingTime = 0;
for (const event of topLevelEvents) {
// If the task is less than the delay threshold, it contains no Long Queuing Delay Region.
// Early exit for small tasks, which should far outnumber long tasks.
if (event.duration < threshold) continue;
// Otherwise, the duration of the task before the delay-threshold-sized interval at the end is
// considered Long Queuing Delay Region. Example assuming the threshold is 50ms:
// [ 250ms Task ]
// | Long Queuing Delay Region | Last 50ms |
// 200 ms
longQueuingDelayRegions.push({
start: event.start,
end: event.end - threshold,
duration: event.duration - threshold,
});
}

let sumLongQueuingDelay = 0;
for (const region of longQueuingDelayRegions) {
// We only want to add up the Long Queuing Delay regions that fall between FCP and TTI.
//
// We only want to consider tasks that fall between FCP and TTI.
// FCP is picked as the lower bound because there is little risk of user input happening
// before FCP so Long Queuing Qelay regions do not harm user experience. Developers should be
// optimizing to reach FCP as fast as possible without having to worry about task lengths.
//
if (event.end < fcpTimeInMs) continue;

// TTI is picked as the upper bound because we want a well defined end point so that the
// metric does not rely on how long we trace.
if (region.end < fcpTimeInMs) continue;
if (region.start > interactiveTimeMs) continue;
if (event.start > interactiveTimeMs) continue;

// If a Long Queuing Delay Region spans the edges of our region of interest, we clip it to
// only include the part of the region that falls inside.
const clippedStart = Math.max(region.start, fcpTimeInMs);
const clippedEnd = Math.min(region.end, interactiveTimeMs);
const queuingDelayAfterClipping = clippedEnd - clippedStart;
// We first perform the clipping, and then calculate Blocking Region. So if we have a 150ms
// task [0, 150] and FCP happens midway at 50ms, we first clip the task to [50, 150], and then
// calculate the Blocking Region to be [100, 150]. The rational here is that tasks before FCP
// are unimportant, so we care whether the main thread is busy more than 50ms at a time only
// after FCP.
const clippedStart = Math.max(event.start, fcpTimeInMs);
const clippedEnd = Math.min(event.end, interactiveTimeMs);
const clippedDuration = clippedEnd - clippedStart;
if (clippedDuration < threshold) continue;

sumLongQueuingDelay += queuingDelayAfterClipping;
// The duration of the task beyond 50ms at the beginning is considered the Blocking Region.
// Example:
// [ 250ms Task ]
// | First 50ms | Blocking Region (200ms) |
sumBlockingTime += (clippedDuration - threshold);
}

return sumLongQueuingDelay;
return sumBlockingTime;
}

/**
Expand All @@ -91,7 +83,7 @@ class CumulativeLongQueuingDelay extends ComputedMetric {
* @return {Promise<LH.Artifacts.LanternMetric>}
*/
static computeSimulatedMetric(data, context) {
return LanternCumulativeLongQueuingDelay.request(data, context);
return LanternTotalBlockingTime.request(data, context);
}

/**
Expand All @@ -107,12 +99,10 @@ class CumulativeLongQueuingDelay extends ComputedMetric {

const interactiveTimeMs = (await TimetoInteractive.request(data, context)).timing;

// Not using the start time argument of getMainThreadTopLevelEvents, because
// we need to clip the part of the task before the last 50ms properly.
const events = TracingProcessor.getMainThreadTopLevelEvents(data.traceOfTab);

return {
timing: CumulativeLongQueuingDelay.calculateSumOfLongQueuingDelay(
timing: TotalBlockingTime.calculateSumOfBlockingTime(
events,
firstContentfulPaint,
interactiveTimeMs
Expand All @@ -121,4 +111,4 @@ class CumulativeLongQueuingDelay extends ComputedMetric {
}
}

module.exports = makeComputedArtifact(CumulativeLongQueuingDelay);
module.exports = makeComputedArtifact(TotalBlockingTime);
4 changes: 2 additions & 2 deletions lighthouse-core/config/default-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ const defaultConfig = {
'screenshot-thumbnails',
'final-screenshot',
'metrics/estimated-input-latency',
'metrics/cumulative-long-queuing-delay',
'metrics/total-blocking-time',
'metrics/max-potential-fid',
'errors-in-console',
'time-to-first-byte',
Expand Down Expand Up @@ -369,7 +369,7 @@ const defaultConfig = {
{id: 'first-cpu-idle', weight: 2, group: 'metrics'},
{id: 'max-potential-fid', weight: 0, group: 'metrics'},
{id: 'estimated-input-latency', weight: 0}, // intentionally left out of metrics so it won't be displayed
{id: 'cumulative-long-queuing-delay', weight: 0}, // intentionally left out of metrics so it won't be displayed
{id: 'total-blocking-time', weight: 0}, // intentionally left out of metrics so it won't be displayed
{id: 'render-blocking-resources', weight: 0, group: 'load-opportunities'},
{id: 'uses-responsive-images', weight: 0, group: 'load-opportunities'},
{id: 'offscreen-images', weight: 0, group: 'load-opportunities'},
Expand Down
8 changes: 8 additions & 0 deletions lighthouse-core/lib/i18n/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -1039,6 +1039,14 @@
"message": "Speed Index",
"description": "The name of the metric that summarizes how quickly the page looked visually complete. The name of this metric is largely abstract and can be loosely translated. Shown to users as the label for the numeric metric value. Ideally fits within a ~40 character limit."
},
"lighthouse-core/audits/metrics/total-blocking-time.js | description": {
"message": "Sum of all time periods between FCP and Time to Interactive, when task length exceeded 50ms, expressed in milliseconds.",
"description": "Description of the Total Blocking Time (TBT) metric, which calculates the total duration of blocking time for a web page. Blocking times are time periods when the page would be blocked (prevented) from responding to user input (clicks, taps, and keypresses will feel slow to respond). This is displayed within a tooltip when the user hovers on the metric name to see more. No character length limits."
},
"lighthouse-core/audits/metrics/total-blocking-time.js | title": {
"message": "Total Blocking Time",
"description": "The name of a metric that calculates the total duration of blocking time for a web page. Blocking times are time periods when the page would be blocked (prevented) from responding to user input (clicks, taps, and keypresses will feel slow to respond). Shown to users as the label for the numeric metric value. Ideally fits within a ~40 character limit."
},
"lighthouse-core/audits/network-rtt.js | description": {
"message": "Network round trip times (RTT) have a large impact on performance. If the RTT to an origin is high, it's an indication that servers closer to the user could improve performance. [Learn more](https://hpbn.co/primer-on-latency-and-bandwidth/).",
"description": "Description of a Lighthouse audit that tells the user that a high network round trip time (RTT) can effect their website's performance because the server is physically far away from them thus making the RTT high. This is displayed after a user expands the section to see more. No character length limits. 'Learn More' becomes link text to additional documentation."
Expand Down
Loading

0 comments on commit 85429f9

Please sign in to comment.