diff --git a/core/audits/redirects.js b/core/audits/redirects.js index d5742e20e0bd..6201544c9de5 100644 --- a/core/audits/redirects.js +++ b/core/audits/redirects.js @@ -90,6 +90,7 @@ class Redirects extends Audit { const processedTrace = await ProcessedTrace.request(trace, context); const networkRecords = await NetworkRecords.request(devtoolsLog, context); + const documentRequests = Redirects.getDocumentRequestChain(networkRecords, processedTrace); const metricComputationData = {trace, devtoolsLog, gatherContext, settings, URL: artifacts.URL}; const metricResult = await LanternInteractive.request(metricComputationData, context); @@ -102,8 +103,6 @@ class Redirects extends Audit { } } - const documentRequests = Redirects.getDocumentRequestChain(networkRecords, processedTrace); - let totalWastedMs = 0; const tableRows = []; diff --git a/core/computed/metrics/lantern-metric.js b/core/computed/metrics/lantern-metric.js index fc8c56f9b6c1..f431d3523674 100644 --- a/core/computed/metrics/lantern-metric.js +++ b/core/computed/metrics/lantern-metric.js @@ -9,6 +9,8 @@ import {LighthouseError} from '../../lib/lh-error.js'; import {LoadSimulator} from '../load-simulator.js'; import {ProcessedNavigation} from '../processed-navigation.js'; import {PageDependencyGraph} from '../page-dependency-graph.js'; +import {TraceEngineResult} from '../trace-engine-result.js'; +import {createProcessedNavigation} from '../../lib/lantern/lantern.js'; /** * @param {LH.Artifacts.MetricComputationDataInput} data @@ -36,7 +38,8 @@ async function getComputationDataParamsFromTrace(data, context) { } const graph = await PageDependencyGraph.request({...data, fromTrace: true}, context); - const processedNavigation = await ProcessedNavigation.request(data.trace, context); + const traceEngineResult = await TraceEngineResult.request(data, context); + const processedNavigation = createProcessedNavigation(traceEngineResult); const simulator = data.simulator || (await LoadSimulator.request(data, context)); return {simulator, graph, processedNavigation}; diff --git a/core/lib/lantern/lantern.js b/core/lib/lantern/lantern.js index 63312ed5cdec..c4004be6324a 100644 --- a/core/lib/lantern/lantern.js +++ b/core/lib/lantern/lantern.js @@ -4,6 +4,11 @@ * SPDX-License-Identifier: Apache-2.0 */ +import * as Lantern from './types/lantern.js'; + +/** @typedef {import('@paulirish/trace_engine/models/trace/handlers/PageLoadMetricsHandler.js').MetricName} MetricName */ +/** @typedef {import('@paulirish/trace_engine/models/trace/handlers/PageLoadMetricsHandler.js').MetricScore} MetricScore */ + /** @type {LH.Util.SelfMap} */ const NetworkRequestTypes = { XHR: 'XHR', @@ -26,6 +31,48 @@ const NetworkRequestTypes = { Prefetch: 'Prefetch', }; +/** + * @param {LH.Artifacts.TraceEngineResult} traceEngineResult + * @return {Lantern.Simulation.ProcessedNavigation} + */ +function createProcessedNavigation(traceEngineResult) { + const Meta = traceEngineResult.data.Meta; + const frameId = Meta.mainFrameId; + const scoresByNav = traceEngineResult.data.PageLoadMetrics.metricScoresByFrameId.get(frameId); + if (!scoresByNav) { + throw new Error('missing metric scores for main frame'); + } + + const lastNavigationId = Meta.mainFrameNavigations.at(-1)?.args.data?.navigationId; + const scores = lastNavigationId && scoresByNav.get(lastNavigationId); + if (!scores) { + throw new Error('missing metric scores for specified navigation'); + } + + /** @param {MetricName} metric */ + const getTimestampOrUndefined = metric => { + const metricScore = scores.get(metric); + if (!metricScore?.event) return; + return metricScore.event.ts; + }; + /** @param {MetricName} metric */ + const getTimestamp = metric => { + const metricScore = scores.get(metric); + if (!metricScore?.event) throw new Error(`missing metric: ${metric}`); + return metricScore.event.ts; + }; + // TODO: should use `MetricName.LCP`, but it is a const enum. + const FCP = /** @type {MetricName} */('FCP'); + const LCP = /** @type {MetricName} */('LCP'); + return { + timestamps: { + firstContentfulPaint: getTimestamp(FCP), + largestContentfulPaint: getTimestampOrUndefined(LCP), + }, + }; +} + export { NetworkRequestTypes, + createProcessedNavigation, }; diff --git a/core/lib/lantern/metric.js b/core/lib/lantern/metric.js index 8909543e151e..f799c98e2d4b 100644 --- a/core/lib/lantern/metric.js +++ b/core/lib/lantern/metric.js @@ -64,7 +64,7 @@ class Metric { /** * @param {Node} dependencyGraph - * @param {LH.Artifacts.ProcessedNavigation} processedNavigation + * @param {Lantern.Simulation.ProcessedNavigation} processedNavigation * @return {Node} */ static getOptimisticGraph(dependencyGraph, processedNavigation) { // eslint-disable-line no-unused-vars @@ -73,7 +73,7 @@ class Metric { /** * @param {Node} dependencyGraph - * @param {LH.Artifacts.ProcessedNavigation} processedNavigation + * @param {Lantern.Simulation.ProcessedNavigation} processedNavigation * @return {Node} */ static getPessimisticGraph(dependencyGraph, processedNavigation) { // eslint-disable-line no-unused-vars diff --git a/core/lib/lantern/metrics/first-contentful-paint.js b/core/lib/lantern/metrics/first-contentful-paint.js index accecd55752a..ef1c6a8e968a 100644 --- a/core/lib/lantern/metrics/first-contentful-paint.js +++ b/core/lib/lantern/metrics/first-contentful-paint.js @@ -170,7 +170,7 @@ class FirstContentfulPaint extends Metric { /** * @param {Node} dependencyGraph - * @param {LH.Artifacts.ProcessedNavigation} processedNavigation + * @param {Lantern.Simulation.ProcessedNavigation} processedNavigation * @return {Node} */ static getOptimisticGraph(dependencyGraph, processedNavigation) { @@ -186,7 +186,7 @@ class FirstContentfulPaint extends Metric { /** * @param {Node} dependencyGraph - * @param {LH.Artifacts.ProcessedNavigation} processedNavigation + * @param {Lantern.Simulation.ProcessedNavigation} processedNavigation * @return {Node} */ static getPessimisticGraph(dependencyGraph, processedNavigation) { diff --git a/core/lib/lantern/metrics/largest-contentful-paint.js b/core/lib/lantern/metrics/largest-contentful-paint.js index 135ccbabf435..65bb3eb403f7 100644 --- a/core/lib/lantern/metrics/largest-contentful-paint.js +++ b/core/lib/lantern/metrics/largest-contentful-paint.js @@ -39,7 +39,7 @@ class LargestContentfulPaint extends Metric { /** * @param {Node} dependencyGraph - * @param {LH.Artifacts.ProcessedNavigation} processedNavigation + * @param {Lantern.Simulation.ProcessedNavigation} processedNavigation * @return {Node} */ static getOptimisticGraph(dependencyGraph, processedNavigation) { @@ -56,7 +56,7 @@ class LargestContentfulPaint extends Metric { /** * @param {Node} dependencyGraph - * @param {LH.Artifacts.ProcessedNavigation} processedNavigation + * @param {Lantern.Simulation.ProcessedNavigation} processedNavigation * @return {Node} */ static getPessimisticGraph(dependencyGraph, processedNavigation) { diff --git a/core/lib/lantern/types/lantern.d.ts b/core/lib/lantern/types/lantern.d.ts index e69afef87592..f2279891e838 100644 --- a/core/lib/lantern/types/lantern.d.ts +++ b/core/lib/lantern/types/lantern.d.ts @@ -169,9 +169,16 @@ export namespace Simulation { nodeTimings: Map, NodeTiming>; } + interface ProcessedNavigation { + timestamps: { + firstContentfulPaint: number; + largestContentfulPaint?: number; + }; + } + interface MetricComputationDataInput { simulator: Simulator; graph: GraphNode; - processedNavigation: LH.Artifacts.ProcessedNavigation; + processedNavigation: ProcessedNavigation; } } diff --git a/core/test/audits/redirects-test.js b/core/test/audits/redirects-test.js index fa8aecb92f0b..7c33488c8c5c 100644 --- a/core/test/audits/redirects-test.js +++ b/core/test/audits/redirects-test.js @@ -137,6 +137,10 @@ describe('Performance: Redirects audit', () => { }); const navStart = trace.traceEvents.find(e => e.name === 'navigationStart'); navStart.args.data.navigationId = '1'; + const fcp = trace.traceEvents.find(e => e.name === 'firstContentfulPaint'); + fcp.args.data.navigationId = '1'; + const lcp = trace.traceEvents.find(e => e.name === 'largestContentfulPaint::Candidate'); + lcp.args.data.navigationId = '1'; return { GatherContext: {gatherMode: 'navigation'}, @@ -156,6 +160,9 @@ describe('Performance: Redirects audit', () => { const traceEvents = artifacts.traces.defaultPass.traceEvents; const navStart = traceEvents.find(e => e.name === 'navigationStart'); + const fcp = traceEvents.find(e => e.name === 'firstContentfulPaint'); + const lcp = traceEvents.find(e => e.name === 'largestContentfulPaint::Candidate'); + const secondNavStart = JSON.parse(JSON.stringify(navStart)); traceEvents.push(secondNavStart); navStart.args.data.isLoadingMainFrame = true; @@ -165,6 +172,16 @@ describe('Performance: Redirects audit', () => { secondNavStart.args.data.documentLoaderURL = 'https://www.lisairish.com/'; secondNavStart.args.data.navigationId = '2'; + const secondFcp = JSON.parse(JSON.stringify(fcp)); + traceEvents.push(secondFcp); + secondFcp.args.data.navigationId = '2'; + secondFcp.ts += 2; + + const secondLcp = JSON.parse(JSON.stringify(lcp)); + traceEvents.push(secondLcp); + secondLcp.args.data.navigationId = '2'; + secondFcp.ts += 2; + const output = await RedirectsAudit.audit(artifacts, context); expect(output.details.items).toHaveLength(3); expect(Math.round(output.score * 100) / 100).toMatchInlineSnapshot(`0`); @@ -251,15 +268,33 @@ describe('Performance: Redirects audit', () => { const traceEvents = artifacts.traces.defaultPass.traceEvents; const navStart = traceEvents.find(e => e.name === 'navigationStart'); + const fcp = traceEvents.find(e => e.name === 'firstContentfulPaint'); + const lcp = traceEvents.find(e => e.name === 'largestContentfulPaint::Candidate'); const secondNavStart = JSON.parse(JSON.stringify(navStart)); traceEvents.push(secondNavStart); secondNavStart.args.data.navigationId = '2'; + const secondFcp = JSON.parse(JSON.stringify(fcp)); + traceEvents.push(secondFcp); + secondFcp.args.data.navigationId = '2'; + + const secondLcp = JSON.parse(JSON.stringify(lcp)); + traceEvents.push(secondLcp); + secondLcp.args.data.navigationId = '2'; + const thirdNavStart = JSON.parse(JSON.stringify(navStart)); traceEvents.push(thirdNavStart); thirdNavStart.args.data.navigationId = '3'; + const thirdFcp = JSON.parse(JSON.stringify(fcp)); + traceEvents.push(thirdFcp); + thirdFcp.args.data.navigationId = '3'; + + const thirdLcp = JSON.parse(JSON.stringify(lcp)); + traceEvents.push(thirdLcp); + thirdLcp.args.data.navigationId = '3'; + const output = await RedirectsAudit.audit(artifacts, context); expect(output).toMatchObject({ score: 0, diff --git a/core/test/lib/lantern/metrics/metric-test-utils.js b/core/test/lib/lantern/metrics/metric-test-utils.js index 4cdab0982d40..4ae467dd10fc 100644 --- a/core/test/lib/lantern/metrics/metric-test-utils.js +++ b/core/test/lib/lantern/metrics/metric-test-utils.js @@ -4,9 +4,9 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {ProcessedNavigation} from '../../../../computed/processed-navigation.js'; import {ProcessedTrace} from '../../../../computed/processed-trace.js'; import {TraceEngineResult} from '../../../../computed/trace-engine-result.js'; +import {createProcessedNavigation} from '../../../../lib/lantern/lantern.js'; import {PageDependencyGraph} from '../../../../lib/lantern/page-dependency-graph.js'; import {NetworkAnalyzer} from '../../../../lib/lantern/simulator/network-analyzer.js'; import {Simulator} from '../../../../lib/lantern/simulator/simulator.js'; @@ -18,13 +18,13 @@ import {getURLArtifactFromDevtoolsLog} from '../../../test-utils.js'; // TODO(15841): remove usage of Lighthouse code to create test data /** + * @param {LH.Artifacts.TraceEngineResult} traceEngineResult * @param {LH.Artifacts.URL} theURL * @param {LH.Trace} trace * @param {LH.Artifacts.ComputedContext} context */ -async function createGraph(theURL, trace, context) { +async function createGraph(traceEngineResult, theURL, trace, context) { const {mainThreadEvents} = await ProcessedTrace.request(trace, context); - const traceEngineResult = await TraceEngineResult.request({trace}, context); return PageDependencyGraph.createGraphFromTrace( mainThreadEvents, trace, traceEngineResult, theURL); } @@ -38,8 +38,9 @@ async function getComputationDataFromFixture({trace, devtoolsLog, settings, URL} if (!URL) URL = getURLArtifactFromDevtoolsLog(devtoolsLog); const context = {settings, computedCache: new Map()}; - const {graph, records} = await createGraph(URL, trace, context); - const processedNavigation = await ProcessedNavigation.request(trace, context); + const traceEngineResult = await TraceEngineResult.request({trace}, context); + const {graph, records} = await createGraph(traceEngineResult, URL, trace, context); + const processedNavigation = createProcessedNavigation(traceEngineResult); const networkAnalysis = NetworkAnalyzer.analyze(records); const simulator = Simulator.createSimulator({...settings, networkAnalysis});