lighthouse
Version:
Automated auditing, performance metrics, and best practices for the web.
245 lines (197 loc) • 8.53 kB
JavaScript
/**
* @license
* Copyright 2023 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import * as Lantern from '../lib/lantern/lantern.js';
import {makeComputedArtifact} from './computed-artifact.js';
import {MainThreadTasks} from './main-thread-tasks.js';
import {FirstContentfulPaint} from './metrics/first-contentful-paint.js';
import {Interactive} from './metrics/interactive.js';
import {TotalBlockingTime} from './metrics/total-blocking-time.js';
import {ProcessedTrace} from './processed-trace.js';
const {calculateTbtImpactForEvent} = Lantern.Metrics.TBTUtils;
class TBTImpactTasks {
/**
* @param {LH.Artifacts.TaskNode} task
* @return {LH.Artifacts.TaskNode}
*/
static getTopLevelTask(task) {
let topLevelTask = task;
while (topLevelTask.parent) {
topLevelTask = topLevelTask.parent;
}
return topLevelTask;
}
/**
* @param {LH.Artifacts.MetricComputationDataInput} metricComputationData
* @param {LH.Artifacts.ComputedContext} context
* @return {Promise<{startTimeMs: number, endTimeMs: number}>}
*/
static async getTbtBounds(metricComputationData, context) {
const processedTrace = await ProcessedTrace.request(metricComputationData.trace, context);
if (metricComputationData.gatherContext.gatherMode !== 'navigation') {
return {
startTimeMs: 0,
endTimeMs: processedTrace.timings.traceEnd,
};
}
const fcpResult = await FirstContentfulPaint.request(metricComputationData, context);
const ttiResult = await Interactive.request(metricComputationData, context);
let startTimeMs = fcpResult.timing;
let endTimeMs = ttiResult.timing;
// When using lantern, we want to get a pessimistic view of the long tasks.
// This means we assume the earliest possible start time and latest possible end time.
if ('optimisticEstimate' in fcpResult) {
startTimeMs = fcpResult.optimisticEstimate.timeInMs;
}
if ('pessimisticEstimate' in ttiResult) {
endTimeMs = ttiResult.pessimisticEstimate.timeInMs;
}
return {startTimeMs, endTimeMs};
}
/**
* @param {LH.Artifacts.TaskNode[]} tasks
* @param {Map<LH.Artifacts.TaskNode, number>} taskToImpact
* @param {Map<LH.Artifacts.TaskNode, number>} taskToBlockingTime
*/
static createImpactTasks(tasks, taskToImpact, taskToBlockingTime) {
/** @type {LH.Artifacts.TBTImpactTask[]} */
const tbtImpactTasks = [];
for (const task of tasks) {
const tbtImpact = taskToImpact.get(task) || 0;
let selfTbtImpact = tbtImpact;
const blockingTime = taskToBlockingTime.get(task) || 0;
let selfBlockingTime = blockingTime;
for (const child of task.children) {
const childTbtImpact = taskToImpact.get(child) || 0;
selfTbtImpact -= childTbtImpact;
const childBlockingTime = taskToBlockingTime.get(child) || 0;
selfBlockingTime -= childBlockingTime;
}
tbtImpactTasks.push({
...task,
// Floating point numbers are not perfectly precise, so the subtraction operations above
// can sometimes output negative numbers close to 0 here. To prevent potentially confusing
// output we should bump those values to 0.
tbtImpact: Math.max(tbtImpact, 0),
selfTbtImpact: Math.max(selfTbtImpact, 0),
selfBlockingTime: Math.max(selfBlockingTime, 0),
});
}
return tbtImpactTasks;
}
/**
* @param {LH.Artifacts.TaskNode[]} tasks
* @param {number} startTimeMs
* @param {number} endTimeMs
* @return {LH.Artifacts.TBTImpactTask[]}
*/
static computeImpactsFromObservedTasks(tasks, startTimeMs, endTimeMs) {
/** @type {Map<LH.Artifacts.TaskNode, number>} */
const taskToImpact = new Map();
/** @type {Map<LH.Artifacts.TaskNode, number>} */
const taskToBlockingTime = new Map();
for (const task of tasks) {
const event = {
start: task.startTime,
end: task.endTime,
duration: task.duration,
};
const topLevelTask = this.getTopLevelTask(task);
const topLevelEvent = {
start: topLevelTask.startTime,
end: topLevelTask.endTime,
duration: topLevelTask.duration,
};
const tbtImpact = calculateTbtImpactForEvent(event, startTimeMs, endTimeMs, topLevelEvent);
const blockingTime = calculateTbtImpactForEvent(event, -Infinity, Infinity, topLevelEvent);
taskToImpact.set(task, tbtImpact);
taskToBlockingTime.set(task, blockingTime);
}
return this.createImpactTasks(tasks, taskToImpact, taskToBlockingTime);
}
/**
* @param {LH.Artifacts.TaskNode[]} tasks
* @param {LH.Gatherer.Simulation.Result['nodeTimings']} tbtNodeTimings
* @param {number} startTimeMs
* @param {number} endTimeMs
* @return {LH.Artifacts.TBTImpactTask[]}
*/
static computeImpactsFromLantern(tasks, tbtNodeTimings, startTimeMs, endTimeMs) {
/** @type {Map<LH.Artifacts.TaskNode, number>} */
const taskToImpact = new Map();
/** @type {Map<LH.Artifacts.TaskNode, number>} */
const taskToBlockingTime = new Map();
/** @type {Map<LH.Artifacts.TaskNode, {start: number, end: number, duration: number}>} */
const topLevelTaskToEvent = new Map();
/** @type {Map<Lantern.Types.TraceEvent, LH.Artifacts.TaskNode>} */
const traceEventToTask = new Map();
for (const task of tasks) {
traceEventToTask.set(task.event, task);
}
// Use lantern TBT timings to calculate the TBT impact of top level tasks.
for (const [node, timing] of tbtNodeTimings) {
if (node.type !== 'cpu') continue;
const event = {
start: timing.startTime,
end: timing.endTime,
duration: timing.duration,
};
const tbtImpact = calculateTbtImpactForEvent(event, startTimeMs, endTimeMs);
const blockingTime = calculateTbtImpactForEvent(event, -Infinity, Infinity);
const task = traceEventToTask.get(node.event);
if (!task) continue;
topLevelTaskToEvent.set(task, event);
taskToImpact.set(task, tbtImpact);
taskToBlockingTime.set(task, blockingTime);
}
// Interpolate the TBT impact of remaining tasks using the top level ancestor tasks.
// We don't have any lantern estimates for tasks that are not top level, so we need to estimate
// the lantern timing based on the task's observed timing relative to it's top level task's observed timing.
for (const task of tasks) {
if (taskToImpact.has(task) || taskToBlockingTime.has(task)) continue;
const topLevelTask = this.getTopLevelTask(task);
const topLevelEvent = topLevelTaskToEvent.get(topLevelTask);
if (!topLevelEvent) continue;
const startRatio = (task.startTime - topLevelTask.startTime) / topLevelTask.duration;
const start = startRatio * topLevelEvent.duration + topLevelEvent.start;
const endRatio = (topLevelTask.endTime - task.endTime) / topLevelTask.duration;
const end = topLevelEvent.end - endRatio * topLevelEvent.duration;
const event = {
start,
end,
duration: end - start,
};
const tbtImpact = calculateTbtImpactForEvent(event, startTimeMs, endTimeMs, topLevelEvent);
const blockingTime = calculateTbtImpactForEvent(event, -Infinity, Infinity, topLevelEvent);
taskToImpact.set(task, tbtImpact);
taskToBlockingTime.set(task, blockingTime);
}
return this.createImpactTasks(tasks, taskToImpact, taskToBlockingTime);
}
/**
* @param {LH.Artifacts.MetricComputationDataInput} metricComputationData
* @param {LH.Artifacts.ComputedContext} context
* @return {Promise<LH.Artifacts.TBTImpactTask[]>}
*/
static async compute_(metricComputationData, context) {
const tbtResult = await TotalBlockingTime.request(metricComputationData, context);
const tasks = await MainThreadTasks.request(metricComputationData.trace, context);
const {startTimeMs, endTimeMs} = await this.getTbtBounds(metricComputationData, context);
if ('pessimisticEstimate' in tbtResult) {
return this.computeImpactsFromLantern(
tasks,
tbtResult.pessimisticEstimate.nodeTimings,
startTimeMs,
endTimeMs
);
}
return this.computeImpactsFromObservedTasks(tasks, startTimeMs, endTimeMs);
}
}
const TBTImpactTasksComputed = makeComputedArtifact(
TBTImpactTasks,
['trace', 'devtoolsLog', 'URL', 'SourceMaps', 'gatherContext', 'settings', 'simulator']
);
export {TBTImpactTasksComputed as TBTImpactTasks};