Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

core(lantern): move lantern metrics to lib/lantern #15875

Merged
merged 12 commits into from
Mar 29, 2024
193 changes: 15 additions & 178 deletions core/computed/metrics/lantern-first-contentful-paint.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,192 +5,29 @@
*/

import {makeComputedArtifact} from '../computed-artifact.js';
import {LanternMetric} from './lantern-metric.js';
import {BaseNode} from '../../lib/lantern/base-node.js';
import {getComputationDataParams} from './lantern-metric.js';
import {FirstContentfulPaint} from '../../lib/lantern/metrics/first-contentful-paint.js';

/** @typedef {import('../../lib/lantern/base-node.js').Node<LH.Artifacts.NetworkRequest>} Node */
/** @typedef {import('../../lib/lantern/cpu-node.js').CPUNode<LH.Artifacts.NetworkRequest>} CPUNode */
/** @typedef {import('../../lib/lantern/network-node.js').NetworkNode<LH.Artifacts.NetworkRequest>} NetworkNode */
/** @typedef {import('../../lib/lantern/metric.js').Extras} Extras */

class LanternFirstContentfulPaint extends LanternMetric {
class LanternFirstContentfulPaint extends FirstContentfulPaint {
/**
* @return {LH.Gatherer.Simulation.MetricCoefficients}
* @param {LH.Artifacts.MetricComputationDataInput} data
* @param {LH.Artifacts.ComputedContext} context
* @param {Omit<Extras, 'optimistic'>=} extras
* @return {Promise<LH.Artifacts.LanternMetric>}
*/
static get COEFFICIENTS() {
return {
intercept: 0,
optimistic: 0.5,
pessimistic: 0.5,
};
static async computeMetricWithGraphs(data, context, extras) {
return this.compute(await getComputationDataParams(data, context), extras);
}

/**
* @typedef FirstPaintBasedGraphOpts
* @property {number} cutoffTimestamp The timestamp used to filter out tasks that occured after
* our paint of interest. Typically this is First Contentful Paint or First Meaningful Paint.
* @property {function(NetworkNode):boolean} treatNodeAsRenderBlocking The function that determines
* which resources should be considered *possibly* render-blocking.
* @property {(function(CPUNode):boolean)=} additionalCpuNodesToTreatAsRenderBlocking The function that
* determines which CPU nodes should also be included in our blocking node IDs set,
* beyond what getRenderBlockingNodeData() already includes.
* @param {LH.Artifacts.MetricComputationDataInput} data
* @param {LH.Artifacts.ComputedContext} context
* @return {Promise<LH.Artifacts.LanternMetric>}
*/

/**
* This function computes the set of URLs that *appeared* to be render-blocking based on our filter,
* *but definitely were not* render-blocking based on the timing of their EvaluateScript task.
* It also computes the set of corresponding CPU node ids that were needed for the paint at the
* given timestamp.
*
* @param {Node} graph
* @param {FirstPaintBasedGraphOpts} opts
* @return {{definitelyNotRenderBlockingScriptUrls: Set<string>, renderBlockingCpuNodeIds: Set<string>}}
*/
static getRenderBlockingNodeData(
graph,
{cutoffTimestamp, treatNodeAsRenderBlocking, additionalCpuNodesToTreatAsRenderBlocking}
) {
/** @type {Map<string, CPUNode>} A map of blocking script URLs to the earliest EvaluateScript task node that executed them. */
const scriptUrlToNodeMap = new Map();

/** @type {Array<CPUNode>} */
const cpuNodes = [];
graph.traverse(node => {
if (node.type === BaseNode.TYPES.CPU) {
// A task is *possibly* render blocking if it *started* before cutoffTimestamp.
// We use startTime here because the paint event can be *inside* the task that was render blocking.
if (node.startTime <= cutoffTimestamp) cpuNodes.push(node);

// Build our script URL map to find the earliest EvaluateScript task node.
const scriptUrls = node.getEvaluateScriptURLs();
for (const url of scriptUrls) {
// Use the earliest CPU node we find.
const existing = scriptUrlToNodeMap.get(url) || node;
scriptUrlToNodeMap.set(url, node.startTime < existing.startTime ? node : existing);
}
}
});

cpuNodes.sort((a, b) => a.startTime - b.startTime);

// A script is *possibly* render blocking if it finished loading before cutoffTimestamp.
const possiblyRenderBlockingScriptUrls = LanternMetric.getScriptUrls(graph, node => {
// The optimistic LCP treatNodeAsRenderBlocking fn wants to exclude some images in the graph,
// but here it only receives scripts to evaluate. It's a no-op in this case, but it will
// matter below in the getFirstPaintBasedGraph clone operation.
return node.endTime <= cutoffTimestamp && treatNodeAsRenderBlocking(node);
});

// A script is *definitely not* render blocking if its EvaluateScript task started after cutoffTimestamp.
/** @type {Set<string>} */
const definitelyNotRenderBlockingScriptUrls = new Set();
/** @type {Set<string>} */
const renderBlockingCpuNodeIds = new Set();
for (const url of possiblyRenderBlockingScriptUrls) {
// Lookup the CPU node that had the earliest EvaluateScript for this URL.
const cpuNodeForUrl = scriptUrlToNodeMap.get(url);

// If we can't find it at all, we can't conclude anything, so just skip it.
if (!cpuNodeForUrl) continue;

// If we found it and it was in our `cpuNodes` set that means it finished before cutoffTimestamp, so it really is render-blocking.
if (cpuNodes.includes(cpuNodeForUrl)) {
renderBlockingCpuNodeIds.add(cpuNodeForUrl.id);
continue;
}

// We couldn't find the evaluate script in the set of CPU nodes that ran before our paint, so
// it must not have been necessary for the paint.
definitelyNotRenderBlockingScriptUrls.add(url);
}

// The first layout, first paint, and first ParseHTML are almost always necessary for first paint,
// so we always include those CPU nodes.
const firstLayout = cpuNodes.find(node => node.didPerformLayout());
if (firstLayout) renderBlockingCpuNodeIds.add(firstLayout.id);
const firstPaint = cpuNodes.find(node => node.childEvents.some(e => e.name === 'Paint'));
if (firstPaint) renderBlockingCpuNodeIds.add(firstPaint.id);
const firstParse = cpuNodes.find(node => node.childEvents.some(e => e.name === 'ParseHTML'));
if (firstParse) renderBlockingCpuNodeIds.add(firstParse.id);

// If a CPU filter was passed in, we also want to include those extra nodes.
if (additionalCpuNodesToTreatAsRenderBlocking) {
cpuNodes
.filter(additionalCpuNodesToTreatAsRenderBlocking)
.forEach(node => renderBlockingCpuNodeIds.add(node.id));
}

return {
definitelyNotRenderBlockingScriptUrls,
renderBlockingCpuNodeIds,
};
}

/**
* This function computes the graph required for the first paint of interest.
*
* @param {Node} dependencyGraph
* @param {FirstPaintBasedGraphOpts} opts
* @return {Node}
*/
static getFirstPaintBasedGraph(
dependencyGraph,
{cutoffTimestamp, treatNodeAsRenderBlocking, additionalCpuNodesToTreatAsRenderBlocking}
) {
const rbData = this.getRenderBlockingNodeData(dependencyGraph, {
cutoffTimestamp,
treatNodeAsRenderBlocking,
additionalCpuNodesToTreatAsRenderBlocking,
});
const {definitelyNotRenderBlockingScriptUrls, renderBlockingCpuNodeIds} = rbData;

return dependencyGraph.cloneWithRelationships(node => {
if (node.type === BaseNode.TYPES.NETWORK) {
// Exclude all nodes that ended after cutoffTimestamp (except for the main document which we always consider necessary)
// endTime is negative if request does not finish, make sure startTime isn't after cutoffTimestamp in this case.
const endedAfterPaint = node.endTime > cutoffTimestamp || node.startTime > cutoffTimestamp;
if (endedAfterPaint && !node.isMainDocument()) return false;

const url = node.record.url;
// If the URL definitely wasn't render-blocking then we filter it out.
if (definitelyNotRenderBlockingScriptUrls.has(url)) {
return false;
}

// Lastly, build up the FCP graph of all nodes we consider render blocking
return treatNodeAsRenderBlocking(node);
} else {
// If it's a CPU node, just check if it was blocking.
return renderBlockingCpuNodeIds.has(node.id);
}
});
}

/**
* @param {Node} dependencyGraph
* @param {LH.Artifacts.ProcessedNavigation} processedNavigation
* @return {Node}
*/
static getOptimisticGraph(dependencyGraph, processedNavigation) {
return this.getFirstPaintBasedGraph(dependencyGraph, {
cutoffTimestamp: processedNavigation.timestamps.firstContentfulPaint,
// In the optimistic graph we exclude resources that appeared to be render blocking but were
// initiated by a script. While they typically have a very high importance and tend to have a
// significant impact on the page's content, these resources don't technically block rendering.
treatNodeAsRenderBlocking: node =>
node.hasRenderBlockingPriority() && node.initiatorType !== 'script',
});
}

/**
* @param {Node} dependencyGraph
* @param {LH.Artifacts.ProcessedNavigation} processedNavigation
* @return {Node}
*/
static getPessimisticGraph(dependencyGraph, processedNavigation) {
return this.getFirstPaintBasedGraph(dependencyGraph, {
cutoffTimestamp: processedNavigation.timestamps.firstContentfulPaint,
treatNodeAsRenderBlocking: node => node.hasRenderBlockingPriority(),
});
static async compute_(data, context) {
return this.computeMetricWithGraphs(data, context);
}
}

Expand Down
59 changes: 10 additions & 49 deletions core/computed/metrics/lantern-first-meaningful-paint.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,60 +5,21 @@
*/

import {makeComputedArtifact} from '../computed-artifact.js';
import {LanternMetric} from './lantern-metric.js';
import {LighthouseError} from '../../lib/lh-error.js';
import {getComputationDataParams} from './lantern-metric.js';
import {FirstMeaningfulPaint} from '../../lib/lantern/metrics/first-meaningful-paint.js';
import {LanternFirstContentfulPaint} from './lantern-first-contentful-paint.js';

/** @typedef {import('../../lib/lantern/base-node.js').Node<LH.Artifacts.NetworkRequest>} Node */
/** @typedef {import('../../lib/lantern/metric.js').Extras} Extras */

class LanternFirstMeaningfulPaint extends LanternMetric {
class LanternFirstMeaningfulPaint extends FirstMeaningfulPaint {
/**
* @return {LH.Gatherer.Simulation.MetricCoefficients}
*/
static get COEFFICIENTS() {
return {
intercept: 0,
optimistic: 0.5,
pessimistic: 0.5,
};
}

/**
* @param {Node} dependencyGraph
* @param {LH.Artifacts.ProcessedNavigation} processedNavigation
* @return {Node}
*/
static getOptimisticGraph(dependencyGraph, processedNavigation) {
const fmp = processedNavigation.timestamps.firstMeaningfulPaint;
if (!fmp) {
throw new LighthouseError(LighthouseError.errors.NO_FMP);
}
return LanternFirstContentfulPaint.getFirstPaintBasedGraph(dependencyGraph, {
cutoffTimestamp: fmp,
// See LanternFirstContentfulPaint's getOptimisticGraph implementation for a longer description
// of why we exclude script initiated resources here.
treatNodeAsRenderBlocking: node =>
node.hasRenderBlockingPriority() && node.initiatorType !== 'script',
});
}

/**
* @param {Node} dependencyGraph
* @param {LH.Artifacts.ProcessedNavigation} processedNavigation
* @return {Node}
* @param {LH.Artifacts.MetricComputationDataInput} data
* @param {LH.Artifacts.ComputedContext} context
* @param {Omit<Extras, 'optimistic'>=} extras
* @return {Promise<LH.Artifacts.LanternMetric>}
*/
static getPessimisticGraph(dependencyGraph, processedNavigation) {
const fmp = processedNavigation.timestamps.firstMeaningfulPaint;
if (!fmp) {
throw new LighthouseError(LighthouseError.errors.NO_FMP);
}

return LanternFirstContentfulPaint.getFirstPaintBasedGraph(dependencyGraph, {
cutoffTimestamp: fmp,
treatNodeAsRenderBlocking: node => node.hasRenderBlockingPriority(),
// For pessimistic FMP we'll include *all* layout nodes
additionalCpuNodesToTreatAsRenderBlocking: node => node.didPerformLayout(),
});
static async computeMetricWithGraphs(data, context, extras) {
return this.compute(await getComputationDataParams(data, context), extras);
}

/**
Expand Down
95 changes: 11 additions & 84 deletions core/computed/metrics/lantern-interactive.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,78 +5,21 @@
*/

import {makeComputedArtifact} from '../computed-artifact.js';
import {LanternMetric} from './lantern-metric.js';
import {BaseNode} from '../../lib/lantern/base-node.js';
import {NetworkRequest} from '../../lib/network-request.js';
import {LanternFirstMeaningfulPaint} from './lantern-first-meaningful-paint.js';
import {Interactive} from '../../lib/lantern/metrics/interactive.js';
import {getComputationDataParams} from './lantern-metric.js';

/** @typedef {import('../../lib/lantern/base-node.js').Node<LH.Artifacts.NetworkRequest>} Node */

// 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 LanternInteractive extends LanternMetric {
/**
* @return {LH.Gatherer.Simulation.MetricCoefficients}
*/
static get COEFFICIENTS() {
return {
intercept: 0,
optimistic: 0.5,
pessimistic: 0.5,
};
}

/**
* @param {Node} dependencyGraph
* @return {Node}
*/
static 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 === BaseNode.TYPES.CPU) {
return node.event.dur > minimumCpuTaskDuration;
}

// Include all scripts and high priority requests, exclude all images
const isImage = node.record.resourceType === NetworkRequest.TYPES.Image;
const isScript = node.record.resourceType === NetworkRequest.TYPES.Script;
return (
!isImage &&
(isScript ||
node.record.priority === 'High' ||
node.record.priority === 'VeryHigh')
);
});
}
/** @typedef {import('../../lib/lantern/metric.js').Extras} Extras */

class LanternInteractive extends Interactive {
/**
* @param {Node} dependencyGraph
* @return {Node}
*/
static getPessimisticGraph(dependencyGraph) {
return dependencyGraph;
}

/**
* @param {LH.Gatherer.Simulation.Result} simulationResult
* @param {import('../../lib/lantern/metric.js').Extras} extras
* @return {LH.Gatherer.Simulation.Result}
* @param {LH.Artifacts.MetricComputationDataInput} data
* @param {LH.Artifacts.ComputedContext} context
* @param {Omit<Extras, 'optimistic'>=} extras
* @return {Promise<LH.Artifacts.LanternMetric>}
*/
static getEstimateFromSimulation(simulationResult, extras) {
if (!extras.fmpResult) throw new Error('missing fmpResult');

const lastTaskAt = LanternInteractive.getLastLongTaskEndTime(simulationResult.nodeTimings);
const minimumTime = extras.optimistic
? extras.fmpResult.optimisticEstimate.timeInMs
: extras.fmpResult.pessimisticEstimate.timeInMs;
return {
timeInMs: Math.max(minimumTime, lastTaskAt),
nodeTimings: simulationResult.nodeTimings,
};
static async computeMetricWithGraphs(data, context, extras) {
return this.compute(await getComputationDataParams(data, context), extras);
}

/**
Expand All @@ -86,23 +29,7 @@ class LanternInteractive extends LanternMetric {
*/
static async compute_(data, context) {
const fmpResult = await LanternFirstMeaningfulPaint.request(data, context);
const metricResult = await this.computeMetricWithGraphs(data, context, {fmpResult});
metricResult.timing = Math.max(metricResult.timing, fmpResult.timing);
return metricResult;
}

/**
* @param {LH.Gatherer.Simulation.Result['nodeTimings']} nodeTimings
* @return {number}
*/
static getLastLongTaskEndTime(nodeTimings, duration = 50) {
return Array.from(nodeTimings.entries())
.filter(([node, timing]) => {
if (node.type !== BaseNode.TYPES.CPU) return false;
return timing.duration > duration;
})
.map(([_, timing]) => timing.endTime)
.reduce((max, x) => Math.max(max || 0, x || 0), 0);
return this.computeMetricWithGraphs(data, context, {fmpResult});
}
}

Expand Down
Loading
Loading