From f195e55ca92b68c747a714785d92bf6a74b65574 Mon Sep 17 00:00:00 2001 From: Barry Pollard Date: Fri, 6 Dec 2024 21:17:27 +0000 Subject: [PATCH 01/23] LoAF Summary attribution information --- README.md | 65 ++++++++++++++++++++++++++++++++++ src/attribution/onINP.ts | 75 ++++++++++++++++++++++++++++++++++++++++ src/types.ts | 38 +++++++++++++++++++- src/types/inp.ts | 61 ++++++++++++++++++++++++++++++++ 4 files changed, 238 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 55c033c1..8986d21a 100644 --- a/README.md +++ b/README.md @@ -891,6 +891,15 @@ interface INPAttribution { * are detect, this array will be empty. */ longAnimationFrameEntries: PerformanceLongAnimationFrameTiming[]; + /** + * If the browser supports the Long Animation Frame API, this array will + * include any `long-animation-frame` entries that intersect with the INP + * candidate interaction's `startTime` and the `processingEnd` time of the + * last event processed within that animation frame. If the browser does not + * support the Long Animation Frame API or no `long-animation-frame` entries + * are detect, this array will be empty. + */ + longAnimationFrameSummary?: LongAnimationFrameSummary; /** * The time from when the user interacted with the page until when the * browser was first able to start processing event listeners for that @@ -922,6 +931,62 @@ interface INPAttribution { } ``` +#### `LongAnimationFrameSummary` + +```ts +/** + * An object containing potentially-helpful debugging information summarized + * from the LongAnimationFrames intersecting the INP event. + * + * NOTE: Long Animation Frames below 50 milliseconds are not reported, and + * so their scripts cannot be included. For Long Animation Frames that are + * reported, only scripts above 5 milliseconds are included. + */ +export interface LongAnimationFrameSummary { + /** + * The number of Long Animation Frame scripts that intersect the INP event. + * NOTE: This may be be less than the total count of scripts in the Long + * Animation Frames as some scripts may occur before the interaction. + */ + numScripts?: number; + /** + * The slowest Long Animation Frame script that intersects the INP event. + */ + slowestScript?: PerformanceScriptTiming; + /** + * The INP phase where the longest script ran. + */ + slowestScriptPhase?: + | 'inputDelay' + | 'processingDuration' + | 'presentationDelay'; + /** + * The total blocking durations in each phase by invoker for scripts that + * intersect the INP event. + */ + totalDurationsPerPhase?: Record< + 'inputDelay' | 'processingDuration' | 'presentationDelay', + Record + >; + /** + * The total forced style and layout durations as provided by Long Animation + * Frame scripts intercepting the INP event. + */ + totalForcedStyleAndLayoutDuration?: number; + /** + * The total non-force (i.e. end-of-frame) style and layout duration from any + * Long Animation Frames intercepting INP event. + */ + totalNonForcedStyleAndLayoutDuration?: number; + /** + * The total duration of Long Animation Frame scripts that intersect the INP + * duration. Note, this includes forced style and layout within those + * scripts. + */ + totalScriptDuration?: number; +} +``` + #### `LCPAttribution` ```ts diff --git a/src/attribution/onINP.ts b/src/attribution/onINP.ts index ee1a289a..f5d142fa 100644 --- a/src/attribution/onINP.ts +++ b/src/attribution/onINP.ts @@ -28,6 +28,7 @@ import { INPAttribution, INPMetric, INPMetricWithAttribution, + LongAnimationFrameSummary, ReportOpts, } from '../types.js'; @@ -233,6 +234,78 @@ const getIntersectingLoAFs = ( return intersectingLoAFs; }; +const getLoAFSummary = (attribution: INPAttribution) => { + const loafAttribution: LongAnimationFrameSummary = {}; + + // Stats across all LoAF entries and scripts. + const interactionTime = attribution.interactionTime; + const inputDelay = attribution.inputDelay; + const processingDuration = attribution.processingDuration; + let totalStyleAndLayout = 0; + let totalForcedStyleAndLayout = 0; + let totalScriptTime = 0; + let numScripts = 0; + let slowestScriptDuration = 0; + let slowestScript: PerformanceScriptTiming | null = null; + let slowestScriptPhase: string = ''; + const phases: Record> = {} as Record< + string, + Record + >; + + attribution.longAnimationFrameEntries.forEach((loafEntry) => { + totalStyleAndLayout += + loafEntry.startTime + loafEntry.duration - loafEntry.styleAndLayoutStart; + loafEntry.scripts.forEach((script) => { + const scriptEndTime = script.startTime + script.duration; + if (scriptEndTime < interactionTime) { + return; + } + totalScriptTime += script.duration; + numScripts++; + totalForcedStyleAndLayout += script.forcedStyleAndLayoutDuration; + const blockingDuration = + scriptEndTime - Math.max(interactionTime, script.startTime); + const invokerType = script.invokerType; //.replace('-', '_'); + let phase = 'processingDuration'; + if (script.startTime < interactionTime + inputDelay) { + phase = 'inputDelay'; + } else if ( + script.startTime > + interactionTime + inputDelay + processingDuration + ) { + phase = 'presentation'; + } + if (!(phase in phases)) { + phases[phase] = {}; + } + if (!(invokerType in phases[phase])) { + phases[phase][invokerType] = 0; + } + phases[phase][invokerType] += blockingDuration; + + if (blockingDuration > slowestScriptDuration) { + slowestScript = script; + slowestScriptPhase = phase; + slowestScriptDuration = blockingDuration; + } + }); + }); + loafAttribution.numScripts = numScripts; + if (slowestScript) loafAttribution.slowestScript = slowestScript; + if (slowestScript) + loafAttribution.slowestScriptPhase = slowestScriptPhase as + | 'inputDelay' + | 'processingDuration' + | 'presentationDelay'; + loafAttribution.totalDurationsPerPhase = phases; + loafAttribution.totalForcedStyleAndLayoutDuration = totalForcedStyleAndLayout; + loafAttribution.totalNonForcedStyleAndLayoutDuration = totalStyleAndLayout; + loafAttribution.totalScriptDuration = totalScriptTime; + + return loafAttribution; +}; + const attributeINP = (metric: INPMetric): INPMetricWithAttribution => { const firstEntry = metric.entries[0]; const group = entryToEntriesGroupMap.get(firstEntry)!; @@ -287,6 +360,8 @@ const attributeINP = (metric: INPMetric): INPMetricWithAttribution => { loadState: getLoadState(firstEntry.startTime), }; + attribution.longAnimationFrameSummary = getLoAFSummary(attribution); + // Use `Object.assign()` to ensure the original metric object is returned. const metricWithAttribution: INPMetricWithAttribution = Object.assign( metric, diff --git a/src/types.ts b/src/types.ts index 2f1e2214..a5cf6f1a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -88,9 +88,45 @@ declare global { readonly element: Element | null; } + // https://w3c.github.io/long-animation-frame/#sec-PerformanceLongAnimationFrameTiming + export type ScriptWindowAttribution = + | 'self' + | 'descendant' + | 'ancestor' + | 'same-page' + | 'other'; + export type ScriptInvokerType = + | 'classic-script' + | 'module-script' + | 'event-listener' + | 'user-callback' + | 'resolve-promise' + | 'reject-promise'; + + interface PerformanceScriptTiming extends PerformanceEntry { + readonly startTime: DOMHighResTimeStamp; + readonly duration: DOMHighResTimeStamp; + readonly name: string; + readonly entryType: string; + readonly invokerType: ScriptInvokerType; + readonly invoker: string; + readonly executionStart: DOMHighResTimeStamp; + readonly sourceURL: string; + readonly sourceFunctionName: string; + readonly sourceCharPosition: number; + readonly pauseDuration: DOMHighResTimeStamp; + readonly forcedStyleAndLayoutDuration: DOMHighResTimeStamp; + readonly window?: Window; + readonly windowAttribution: ScriptWindowAttribution; + } + // https://w3c.github.io/long-animation-frame/#sec-PerformanceLongAnimationFrameTiming interface PerformanceLongAnimationFrameTiming extends PerformanceEntry { - renderStart: DOMHighResTimeStamp; duration: DOMHighResTimeStamp; + renderStart: DOMHighResTimeStamp; + styleAndLayoutStart: DOMHighResTimeStamp; + firstUIEventTimestamp: DOMHighResTimeStamp; + blockingDuration: DOMHighResTimeStamp; + scripts: Array; } } diff --git a/src/types/inp.ts b/src/types/inp.ts index 59b2acc5..5b406fe9 100644 --- a/src/types/inp.ts +++ b/src/types/inp.ts @@ -24,6 +24,58 @@ export interface INPMetric extends Metric { entries: PerformanceEventTiming[]; } +/** + * An object containing potentially-helpful debugging information summarized + * from the LongAnimationFrames intersecting the INP event. + * + * NOTE: Long Animation Frames below 50 milliseconds are not reported, and + * so their scripts cannot be included. For Long Animation Frames that are + * reported, only scripts above 5 milliseconds are included. + */ +export interface LongAnimationFrameSummary { + /** + * The number of Long Animation Frame scripts that intersect the INP event. + * NOTE: This may be be less than the total count of scripts in the Long + * Animation Frames as some scripts may occur before the interaction. + */ + numScripts?: number; + /** + * The slowest Long Animation Frame script that intersects the INP event. + */ + slowestScript?: PerformanceScriptTiming; + /** + * The INP phase where the longest script ran. + */ + slowestScriptPhase?: + | 'inputDelay' + | 'processingDuration' + | 'presentationDelay'; + /** + * The total blocking durations in each phase by invoker for scripts that + * intersect the INP event. + */ + totalDurationsPerPhase?: Record< + 'inputDelay' | 'processingDuration' | 'presentationDelay', + Record + >; + /** + * The total forced style and layout durations as provided by Long Animation + * Frame scripts intercepting the INP event. + */ + totalForcedStyleAndLayoutDuration?: number; + /** + * The total non-force (i.e. end-of-frame) style and layout duration from any + * Long Animation Frames intercepting INP event. + */ + totalNonForcedStyleAndLayoutDuration?: number; + /** + * The total duration of Long Animation Frame scripts that intersect the INP + * duration. Note, this includes forced style and layout within those + * scripts. + */ + totalScriptDuration?: number; +} + /** * An object containing potentially-helpful debugging information that * can be sent along with the INP value for the current page visit in order @@ -80,6 +132,15 @@ export interface INPAttribution { * are detect, this array will be empty. */ longAnimationFrameEntries: PerformanceLongAnimationFrameTiming[]; + /** + * If the browser supports the Long Animation Frame API, this array will + * include any `long-animation-frame` entries that intersect with the INP + * candidate interaction's `startTime` and the `processingEnd` time of the + * last event processed within that animation frame. If the browser does not + * support the Long Animation Frame API or no `long-animation-frame` entries + * are detect, this array will be empty. + */ + longAnimationFrameSummary?: LongAnimationFrameSummary; /** * The time from when the user interacted with the page until when the * browser was first able to start processing event listeners for that From 0cb43d60d2c912215e3937798995d30f0c620219 Mon Sep 17 00:00:00 2001 From: Barry Pollard Date: Sat, 7 Dec 2024 14:08:47 +0000 Subject: [PATCH 02/23] Add tests --- test/e2e/onINP-test.js | 51 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/test/e2e/onINP-test.js b/test/e2e/onINP-test.js index 80e2175d..061f69ea 100644 --- a/test/e2e/onINP-test.js +++ b/test/e2e/onINP-test.js @@ -26,7 +26,7 @@ const ROUNDING_ERROR = 8; describe('onINP()', async function () { // Retry all tests in this suite up to 2 times. - this.retries(2); + this.retries(0); let browserSupportsINP; let browserSupportsLoAF; @@ -714,6 +714,55 @@ describe('onINP()', async function () { const [inp1] = await getBeacons(); assert(inp1.attribution.longAnimationFrameEntries.length > 0); + assert(inp1.attribution.longAnimationFrameSummary != {}); + assert.equal(inp1.attribution.longAnimationFrameSummary.numScripts, 1); + assert.equal( + JSON.stringify( + inp1.attribution.longAnimationFrameSummary.slowestScript, + ), + JSON.stringify( + inp1.attribution.longAnimationFrameEntries[0].scripts[0], + ), + ); + assert.equal( + inp1.attribution.longAnimationFrameSummary.slowestScriptPhase, + 'processingDuration', + ); + assert.equal( + Object.keys( + inp1.attribution.longAnimationFrameSummary.totalDurationsPerPhase, + ), + 'processingDuration', + ); + assert.equal( + Object.keys( + inp1.attribution.longAnimationFrameSummary.totalDurationsPerPhase + .processingDuration, + ), + 'event-listener', + ); + assert( + inp1.attribution.longAnimationFrameSummary.totalDurationsPerPhase + .processingDuration['event-listener'] >= 100, + ); + assert.equal( + inp1.attribution.longAnimationFrameSummary + .totalForcedStyleAndLayoutDuration, + 0, + ); + assert( + inp1.attribution.longAnimationFrameSummary + .totalNonForcedStyleAndLayoutDuration >= 0, + ); + assert( + inp1.attribution.longAnimationFrameSummary + .totalNonForcedStyleAndLayoutDuration <= + inp1.attribution.presentationDelay, + ); + assert.equal( + inp1.attribution.longAnimationFrameSummary.totalScriptDuration, + 100, + ); }); }); }); From 95237c656ba3179f5c6702a5bf1660ff31dedb25 Mon Sep 17 00:00:00 2001 From: Barry Pollard Date: Sat, 7 Dec 2024 14:30:26 +0000 Subject: [PATCH 03/23] Fix tests --- test/e2e/onINP-test.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/e2e/onINP-test.js b/test/e2e/onINP-test.js index 061f69ea..50621392 100644 --- a/test/e2e/onINP-test.js +++ b/test/e2e/onINP-test.js @@ -26,7 +26,7 @@ const ROUNDING_ERROR = 8; describe('onINP()', async function () { // Retry all tests in this suite up to 2 times. - this.retries(0); + this.retries(2); let browserSupportsINP; let browserSupportsLoAF; @@ -750,18 +750,18 @@ describe('onINP()', async function () { .totalForcedStyleAndLayoutDuration, 0, ); - assert( + assert.equals( inp1.attribution.longAnimationFrameSummary - .totalNonForcedStyleAndLayoutDuration >= 0, + .totalNonForcedStyleAndLayoutDuration, + 0, ); assert( inp1.attribution.longAnimationFrameSummary .totalNonForcedStyleAndLayoutDuration <= inp1.attribution.presentationDelay, ); - assert.equal( - inp1.attribution.longAnimationFrameSummary.totalScriptDuration, - 100, + assert( + inp1.attribution.longAnimationFrameSummary.totalScriptDuration >= 100, ); }); }); From 194cc08372fcc7d6a255bf126608e8f29a395eaf Mon Sep 17 00:00:00 2001 From: Barry Pollard Date: Sat, 7 Dec 2024 14:34:00 +0000 Subject: [PATCH 04/23] Typo --- test/e2e/onINP-test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/e2e/onINP-test.js b/test/e2e/onINP-test.js index 50621392..5c10eb80 100644 --- a/test/e2e/onINP-test.js +++ b/test/e2e/onINP-test.js @@ -745,7 +745,7 @@ describe('onINP()', async function () { inp1.attribution.longAnimationFrameSummary.totalDurationsPerPhase .processingDuration['event-listener'] >= 100, ); - assert.equal( + assert.equals( inp1.attribution.longAnimationFrameSummary .totalForcedStyleAndLayoutDuration, 0, From 1b49c03774cbda53eee4ca59614cc9cbc3b71f3c Mon Sep 17 00:00:00 2001 From: Barry Pollard Date: Sat, 7 Dec 2024 14:41:05 +0000 Subject: [PATCH 05/23] Fixed the wrong one! --- test/e2e/onINP-test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/e2e/onINP-test.js b/test/e2e/onINP-test.js index 5c10eb80..b98dfc53 100644 --- a/test/e2e/onINP-test.js +++ b/test/e2e/onINP-test.js @@ -745,12 +745,12 @@ describe('onINP()', async function () { inp1.attribution.longAnimationFrameSummary.totalDurationsPerPhase .processingDuration['event-listener'] >= 100, ); - assert.equals( + assert.equal( inp1.attribution.longAnimationFrameSummary .totalForcedStyleAndLayoutDuration, 0, ); - assert.equals( + assert.equal( inp1.attribution.longAnimationFrameSummary .totalNonForcedStyleAndLayoutDuration, 0, From d8cb2a197f36b960c1975a56a697160630fc18bd Mon Sep 17 00:00:00 2001 From: Barry Pollard Date: Sat, 7 Dec 2024 14:46:05 +0000 Subject: [PATCH 06/23] Debug test --- test/e2e/onINP-test.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/e2e/onINP-test.js b/test/e2e/onINP-test.js index b98dfc53..99d5b4e8 100644 --- a/test/e2e/onINP-test.js +++ b/test/e2e/onINP-test.js @@ -750,6 +750,10 @@ describe('onINP()', async function () { .totalForcedStyleAndLayoutDuration, 0, ); + assert( + inp1.attribution.longAnimationFrameSummary + .totalNonForcedStyleAndLayoutDuration >= 0, + ); assert.equal( inp1.attribution.longAnimationFrameSummary .totalNonForcedStyleAndLayoutDuration, From 0b6b12f57fb165d0bc4cfe18c5de1514996f07e1 Mon Sep 17 00:00:00 2001 From: Barry Pollard Date: Sat, 7 Dec 2024 14:48:50 +0000 Subject: [PATCH 07/23] Remove failing test --- test/e2e/onINP-test.js | 9 --------- 1 file changed, 9 deletions(-) diff --git a/test/e2e/onINP-test.js b/test/e2e/onINP-test.js index 99d5b4e8..31184a4e 100644 --- a/test/e2e/onINP-test.js +++ b/test/e2e/onINP-test.js @@ -750,15 +750,6 @@ describe('onINP()', async function () { .totalForcedStyleAndLayoutDuration, 0, ); - assert( - inp1.attribution.longAnimationFrameSummary - .totalNonForcedStyleAndLayoutDuration >= 0, - ); - assert.equal( - inp1.attribution.longAnimationFrameSummary - .totalNonForcedStyleAndLayoutDuration, - 0, - ); assert( inp1.attribution.longAnimationFrameSummary .totalNonForcedStyleAndLayoutDuration <= From aad54d8345d0c2602c3c5652a828e1d4e57fef80 Mon Sep 17 00:00:00 2001 From: Barry Pollard Date: Mon, 9 Dec 2024 17:48:19 +0000 Subject: [PATCH 08/23] Review feedback --- README.md | 10 ++++------ src/attribution/onINP.ts | 37 +++++++++++++++++-------------------- src/types/inp.ts | 14 ++++++-------- 3 files changed, 27 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index 8986d21a..96181025 100644 --- a/README.md +++ b/README.md @@ -892,12 +892,10 @@ interface INPAttribution { */ longAnimationFrameEntries: PerformanceLongAnimationFrameTiming[]; /** - * If the browser supports the Long Animation Frame API, this array will - * include any `long-animation-frame` entries that intersect with the INP - * candidate interaction's `startTime` and the `processingEnd` time of the - * last event processed within that animation frame. If the browser does not - * support the Long Animation Frame API or no `long-animation-frame` entries - * are detect, this array will be empty. + * If the browser supports the Long Animation Frame API, this object + * summarises information relevant to INP across the long animation frames + * intersecting the INP event. See the LongAnimationFrameSummary definition + * for an explanation of what is included. */ longAnimationFrameSummary?: LongAnimationFrameSummary; /** diff --git a/src/attribution/onINP.ts b/src/attribution/onINP.ts index f5d142fa..a1679349 100644 --- a/src/attribution/onINP.ts +++ b/src/attribution/onINP.ts @@ -241,32 +241,32 @@ const getLoAFSummary = (attribution: INPAttribution) => { const interactionTime = attribution.interactionTime; const inputDelay = attribution.inputDelay; const processingDuration = attribution.processingDuration; - let totalStyleAndLayout = 0; + let totalNonForcedStyleAndLayoutDuration = 0; let totalForcedStyleAndLayout = 0; let totalScriptTime = 0; let numScripts = 0; let slowestScriptDuration = 0; let slowestScript: PerformanceScriptTiming | null = null; let slowestScriptPhase: string = ''; - const phases: Record> = {} as Record< + const phases: Record< string, - Record - >; + Record + > = {} as Record>; attribution.longAnimationFrameEntries.forEach((loafEntry) => { - totalStyleAndLayout += + totalNonForcedStyleAndLayoutDuration += loafEntry.startTime + loafEntry.duration - loafEntry.styleAndLayoutStart; loafEntry.scripts.forEach((script) => { const scriptEndTime = script.startTime + script.duration; if (scriptEndTime < interactionTime) { return; } - totalScriptTime += script.duration; + const interestingScriptDuration = + scriptEndTime - Math.max(interactionTime, script.startTime); + totalScriptTime += interestingScriptDuration; numScripts++; totalForcedStyleAndLayout += script.forcedStyleAndLayoutDuration; - const blockingDuration = - scriptEndTime - Math.max(interactionTime, script.startTime); - const invokerType = script.invokerType; //.replace('-', '_'); + const invokerType = script.invokerType; let phase = 'processingDuration'; if (script.startTime < interactionTime + inputDelay) { phase = 'inputDelay'; @@ -274,20 +274,16 @@ const getLoAFSummary = (attribution: INPAttribution) => { script.startTime > interactionTime + inputDelay + processingDuration ) { - phase = 'presentation'; - } - if (!(phase in phases)) { - phases[phase] = {}; - } - if (!(invokerType in phases[phase])) { - phases[phase][invokerType] = 0; + phase = 'presentationDelay'; } - phases[phase][invokerType] += blockingDuration; + phases[phase] ??= {} as Record; + phases[phase][invokerType] ??= 0; + phases[phase][invokerType] += interestingScriptDuration; - if (blockingDuration > slowestScriptDuration) { + if (interestingScriptDuration > slowestScriptDuration) { slowestScript = script; slowestScriptPhase = phase; - slowestScriptDuration = blockingDuration; + slowestScriptDuration = interestingScriptDuration; } }); }); @@ -300,7 +296,8 @@ const getLoAFSummary = (attribution: INPAttribution) => { | 'presentationDelay'; loafAttribution.totalDurationsPerPhase = phases; loafAttribution.totalForcedStyleAndLayoutDuration = totalForcedStyleAndLayout; - loafAttribution.totalNonForcedStyleAndLayoutDuration = totalStyleAndLayout; + loafAttribution.totalNonForcedStyleAndLayoutDuration = + totalNonForcedStyleAndLayoutDuration; loafAttribution.totalScriptDuration = totalScriptTime; return loafAttribution; diff --git a/src/types/inp.ts b/src/types/inp.ts index 5b406fe9..89b3b9f0 100644 --- a/src/types/inp.ts +++ b/src/types/inp.ts @@ -60,12 +60,12 @@ export interface LongAnimationFrameSummary { >; /** * The total forced style and layout durations as provided by Long Animation - * Frame scripts intercepting the INP event. + * Frame scripts intersecting the INP event. */ totalForcedStyleAndLayoutDuration?: number; /** * The total non-force (i.e. end-of-frame) style and layout duration from any - * Long Animation Frames intercepting INP event. + * Long Animation Frames intersecting INP event. */ totalNonForcedStyleAndLayoutDuration?: number; /** @@ -133,12 +133,10 @@ export interface INPAttribution { */ longAnimationFrameEntries: PerformanceLongAnimationFrameTiming[]; /** - * If the browser supports the Long Animation Frame API, this array will - * include any `long-animation-frame` entries that intersect with the INP - * candidate interaction's `startTime` and the `processingEnd` time of the - * last event processed within that animation frame. If the browser does not - * support the Long Animation Frame API or no `long-animation-frame` entries - * are detect, this array will be empty. + * If the browser supports the Long Animation Frame API, this object + * summarises information relevant to INP across the long animation frames + * intersecting the INP event. See the LongAnimationFrameSummary definition + * for an explanation of what is included. */ longAnimationFrameSummary?: LongAnimationFrameSummary; /** From 0cdc93fa3d6046c6a19e35269cdc784a8aca44c3 Mon Sep 17 00:00:00 2001 From: Barry Pollard Date: Mon, 9 Dec 2024 18:30:09 +0000 Subject: [PATCH 09/23] Typo --- src/attribution/onINP.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/attribution/onINP.ts b/src/attribution/onINP.ts index a1679349..0798077c 100644 --- a/src/attribution/onINP.ts +++ b/src/attribution/onINP.ts @@ -261,9 +261,9 @@ const getLoAFSummary = (attribution: INPAttribution) => { if (scriptEndTime < interactionTime) { return; } - const interestingScriptDuration = + const intersectingScriptDuration = scriptEndTime - Math.max(interactionTime, script.startTime); - totalScriptTime += interestingScriptDuration; + totalScriptTime += intersectingScriptDuration; numScripts++; totalForcedStyleAndLayout += script.forcedStyleAndLayoutDuration; const invokerType = script.invokerType; @@ -276,14 +276,17 @@ const getLoAFSummary = (attribution: INPAttribution) => { ) { phase = 'presentationDelay'; } + + // Define the record if necessary phases[phase] ??= {} as Record; phases[phase][invokerType] ??= 0; - phases[phase][invokerType] += interestingScriptDuration; + // Increment it with this value + phases[phase][invokerType] += intersectingScriptDuration; - if (interestingScriptDuration > slowestScriptDuration) { + if (intersectingScriptDuration > slowestScriptDuration) { slowestScript = script; slowestScriptPhase = phase; - slowestScriptDuration = interestingScriptDuration; + slowestScriptDuration = intersectingScriptDuration; } }); }); From 918e068f9a5f1063544eb78aa509ac2c0b4e2684 Mon Sep 17 00:00:00 2001 From: Barry Pollard Date: Mon, 9 Dec 2024 18:32:04 +0000 Subject: [PATCH 10/23] Fix ScriptInvokerType --- README.md | 2 +- src/attribution/onINP.ts | 5 +---- src/types/inp.ts | 2 +- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 96181025..e9822e6c 100644 --- a/README.md +++ b/README.md @@ -964,7 +964,7 @@ export interface LongAnimationFrameSummary { */ totalDurationsPerPhase?: Record< 'inputDelay' | 'processingDuration' | 'presentationDelay', - Record + Record >; /** * The total forced style and layout durations as provided by Long Animation diff --git a/src/attribution/onINP.ts b/src/attribution/onINP.ts index 0798077c..ad0ac0e3 100644 --- a/src/attribution/onINP.ts +++ b/src/attribution/onINP.ts @@ -248,10 +248,7 @@ const getLoAFSummary = (attribution: INPAttribution) => { let slowestScriptDuration = 0; let slowestScript: PerformanceScriptTiming | null = null; let slowestScriptPhase: string = ''; - const phases: Record< - string, - Record - > = {} as Record>; + const phases = {} as Record>; attribution.longAnimationFrameEntries.forEach((loafEntry) => { totalNonForcedStyleAndLayoutDuration += diff --git a/src/types/inp.ts b/src/types/inp.ts index 89b3b9f0..b2b42705 100644 --- a/src/types/inp.ts +++ b/src/types/inp.ts @@ -56,7 +56,7 @@ export interface LongAnimationFrameSummary { */ totalDurationsPerPhase?: Record< 'inputDelay' | 'processingDuration' | 'presentationDelay', - Record + Record >; /** * The total forced style and layout durations as provided by Long Animation From 4b16faca4cd34a368dec0ab4d35ab53cfdee7148 Mon Sep 17 00:00:00 2001 From: Barry Pollard Date: Tue, 10 Dec 2024 00:04:56 +0000 Subject: [PATCH 11/23] Comments and names --- README.md | 105 +++++++++++++++++++++++++++++++++------ src/attribution/onINP.ts | 44 ++++++++++++---- src/types/inp.ts | 101 +++++++++++++++++++++++++++++++------ 3 files changed, 211 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index e9822e6c..c4ec7cc0 100644 --- a/README.md +++ b/README.md @@ -894,8 +894,8 @@ interface INPAttribution { /** * If the browser supports the Long Animation Frame API, this object * summarises information relevant to INP across the long animation frames - * intersecting the INP event. See the LongAnimationFrameSummary definition - * for an explanation of what is included. + * intersecting the INP interaction. See the LongAnimationFrameSummary + * definition for an explanation of what is included. */ longAnimationFrameSummary?: LongAnimationFrameSummary; /** @@ -934,7 +934,7 @@ interface INPAttribution { ```ts /** * An object containing potentially-helpful debugging information summarized - * from the LongAnimationFrames intersecting the INP event. + * from the LongAnimationFrames intersecting the INP interaction. * * NOTE: Long Animation Frames below 50 milliseconds are not reported, and * so their scripts cannot be included. For Long Animation Frames that are @@ -942,25 +942,24 @@ interface INPAttribution { */ export interface LongAnimationFrameSummary { /** - * The number of Long Animation Frame scripts that intersect the INP event. + * The number of Long Animation Frame scripts that intersect the INP + * interaction. * NOTE: This may be be less than the total count of scripts in the Long * Animation Frames as some scripts may occur before the interaction. */ - numScripts?: number; + numIntersectingScripts?: number; /** - * The slowest Long Animation Frame script that intersects the INP event. + * The number of Long Animation Frames intersecting the INP interaction. */ - slowestScript?: PerformanceScriptTiming; + numLongAnimationFrames?: number; /** - * The INP phase where the longest script ran. + * Summary details about the slowest Long Animation Frame script that + * intersects the INP interaction. */ - slowestScriptPhase?: - | 'inputDelay' - | 'processingDuration' - | 'presentationDelay'; + slowestScript?: slowestScriptSummary; /** * The total blocking durations in each phase by invoker for scripts that - * intersect the INP event. + * intersect the INP interaction. */ totalDurationsPerPhase?: Record< 'inputDelay' | 'processingDuration' | 'presentationDelay', @@ -968,12 +967,12 @@ export interface LongAnimationFrameSummary { >; /** * The total forced style and layout durations as provided by Long Animation - * Frame scripts intercepting the INP event. + * Frame scripts intercepting the INP interaction. */ totalForcedStyleAndLayoutDuration?: number; /** * The total non-force (i.e. end-of-frame) style and layout duration from any - * Long Animation Frames intercepting INP event. + * Long Animation Frames intercepting INP interaction. */ totalNonForcedStyleAndLayoutDuration?: number; /** @@ -985,6 +984,82 @@ export interface LongAnimationFrameSummary { } ``` +#### `slowestScriptSummary` + +```ts +/** + * An object containing potentially-helpful debugging information summarized + * from the slowest script intersecting the INP duration (ignoring any script + * duration prior to the INP time). + */ +export interface SlowestScriptSummary { + /** + * The slowest Long Animation Frame script that intersects the INP + * interaction. + */ + entry: PerformanceScriptTiming; + /** + * The INP phase where the longest script ran. + */ + phase: 'inputDelay' | 'processingDuration' | 'presentationDelay'; + /** + * The amount of time the slowest script intersected the INP duration. + */ + intersectingDuration: number; + /** + * The total duration time of the slowest script (compile and execution, + * including forced style and layout). Note this may be longer than the + * intersectingScriptDuration if the INP interaction happened mid-script. + */ + totalDuration: number; + /** + * The compile duration of the slowest script. Note this may be longer + * than the intersectingScriptDuration if the INP interaction happened + * mid-script. + */ + compileDuration: number; + /** + * The execution duration of the slowest script. Note this may be longer + * than the intersectingScriptDuration if the INP interaction happened + * mid-script. + */ + executionDuration: number; + /** + /** + * The forced style and layoult duration of the slowest script. Note this + * may be longer than the intersectingScriptDuration if the INP interaction + * happened mid-script. + */ + forcedStyleAndLayoutDuration: number; + /** + * The pause duration of the slowest script. Note this may be longer + * than the intersectingScriptDuration if the INP interaction happened + * mid-script. + */ + pauseDuration: number; + /** + * The invokerType of the slowest script. + */ + invokerType: ScriptInvokerType; + /** + * The invoker of the slowest script. + */ + invoker?: string; + /** + * The sourceURL of the slowest script. + */ + sourceURL?: string; + /** + * The sourceFunctionName of the slowest script. + */ + sourceFunctionName?: string; + /** + * The sourceCharPosition of the slowest script. + */ + sourceCharPosition?: number; +} +``` + #### `LCPAttribution` ```ts diff --git a/src/attribution/onINP.ts b/src/attribution/onINP.ts index ad0ac0e3..49fb783d 100644 --- a/src/attribution/onINP.ts +++ b/src/attribution/onINP.ts @@ -29,6 +29,7 @@ import { INPMetric, INPMetricWithAttribution, LongAnimationFrameSummary, + SlowestScriptSummary, ReportOpts, } from '../types.js'; @@ -246,7 +247,7 @@ const getLoAFSummary = (attribution: INPAttribution) => { let totalScriptTime = 0; let numScripts = 0; let slowestScriptDuration = 0; - let slowestScript: PerformanceScriptTiming | null = null; + let slowestScriptEntry!: PerformanceScriptTiming; let slowestScriptPhase: string = ''; const phases = {} as Record>; @@ -281,19 +282,44 @@ const getLoAFSummary = (attribution: INPAttribution) => { phases[phase][invokerType] += intersectingScriptDuration; if (intersectingScriptDuration > slowestScriptDuration) { - slowestScript = script; + slowestScriptEntry = script; slowestScriptPhase = phase; slowestScriptDuration = intersectingScriptDuration; } }); }); - loafAttribution.numScripts = numScripts; - if (slowestScript) loafAttribution.slowestScript = slowestScript; - if (slowestScript) - loafAttribution.slowestScriptPhase = slowestScriptPhase as - | 'inputDelay' - | 'processingDuration' - | 'presentationDelay'; + + // Gather the summary information into the loafAttribution object + loafAttribution.numLongAnimationFrames = + attribution.longAnimationFrameEntries.length; + loafAttribution.numIntersectingScripts = numScripts; + if (slowestScriptEntry !== null) { + const slowestScript: SlowestScriptSummary = { + entry: slowestScriptEntry, + phase: slowestScriptPhase as + | 'inputDelay' + | 'processingDuration' + | 'presentationDelay', + intersectingDuration: slowestScriptDuration, + totalDuration: slowestScriptEntry.duration, + compileDuration: + slowestScriptEntry.executionStart - slowestScriptEntry.startTime, + executionDuration: + slowestScriptEntry.startTime + + slowestScriptEntry.duration - + slowestScriptEntry.executionStart, + forcedStyleAndLayoutDuration: + slowestScriptEntry.forcedStyleAndLayoutDuration, + pauseDuration: slowestScriptEntry.pauseDuration, + invokerType: slowestScriptEntry.invokerType, + invoker: slowestScriptEntry.invoker, + sourceURL: slowestScriptEntry.sourceURL, + sourceFunctionName: slowestScriptEntry.sourceFunctionName, + sourceCharPosition: slowestScriptEntry.sourceCharPosition, + }; + + loafAttribution.slowestScript = slowestScript; + } loafAttribution.totalDurationsPerPhase = phases; loafAttribution.totalForcedStyleAndLayoutDuration = totalForcedStyleAndLayout; loafAttribution.totalNonForcedStyleAndLayoutDuration = diff --git a/src/types/inp.ts b/src/types/inp.ts index b2b42705..ebd8ec4f 100644 --- a/src/types/inp.ts +++ b/src/types/inp.ts @@ -26,7 +26,79 @@ export interface INPMetric extends Metric { /** * An object containing potentially-helpful debugging information summarized - * from the LongAnimationFrames intersecting the INP event. + * from the slowest script intersecting the INP duration (ignoring any script + * duration prior to the INP time). + */ +export interface SlowestScriptSummary { + /** + * The slowest Long Animation Frame script that intersects the INP + * interaction. + */ + entry: PerformanceScriptTiming; + /** + * The INP phase where the longest script ran. + */ + phase: 'inputDelay' | 'processingDuration' | 'presentationDelay'; + /** + * The amount of time the slowest script intersected the INP duration. + */ + intersectingDuration: number; + /** + * The total duration time of the slowest script (compile and execution, + * including forced style and layout). Note this may be longer than the + * intersectingScriptDuration if the INP interaction happened mid-script. + */ + totalDuration: number; + /** + * The compile duration of the slowest script. Note this may be longer + * than the intersectingScriptDuration if the INP interaction happened + * mid-script. + */ + compileDuration: number; + /** + * The execution duration of the slowest script. Note this may be longer + * than the intersectingScriptDuration if the INP interaction happened + * mid-script. + */ + executionDuration: number; + /** + /** + * The forced style and layoult duration of the slowest script. Note this + * may be longer than the intersectingScriptDuration if the INP interaction + * happened mid-script. + */ + forcedStyleAndLayoutDuration: number; + /** + * The pause duration of the slowest script. Note this may be longer + * than the intersectingScriptDuration if the INP interaction happened + * mid-script. + */ + pauseDuration: number; + /** + * The invokerType of the slowest script. + */ + invokerType: ScriptInvokerType; + /** + * The invoker of the slowest script. + */ + invoker?: string; + /** + * The sourceURL of the slowest script. + */ + sourceURL?: string; + /** + * The sourceFunctionName of the slowest script. + */ + sourceFunctionName?: string; + /** + * The sourceCharPosition of the slowest script. + */ + sourceCharPosition?: number; +} + +/** + * An object containing potentially-helpful debugging information summarized + * from the LongAnimationFrames intersecting the INP interaction. * * NOTE: Long Animation Frames below 50 milliseconds are not reported, and * so their scripts cannot be included. For Long Animation Frames that are @@ -34,25 +106,24 @@ export interface INPMetric extends Metric { */ export interface LongAnimationFrameSummary { /** - * The number of Long Animation Frame scripts that intersect the INP event. + * The number of Long Animation Frame scripts that intersect the INP + * interaction. * NOTE: This may be be less than the total count of scripts in the Long * Animation Frames as some scripts may occur before the interaction. */ - numScripts?: number; + numIntersectingScripts?: number; /** - * The slowest Long Animation Frame script that intersects the INP event. + * The number of Long Animation Frames intersecting the INP interaction. */ - slowestScript?: PerformanceScriptTiming; + numLongAnimationFrames?: number; /** - * The INP phase where the longest script ran. + * Summary details about the slowest Long Animation Frame script that + * intersects the INP interaction. */ - slowestScriptPhase?: - | 'inputDelay' - | 'processingDuration' - | 'presentationDelay'; + slowestScript?: SlowestScriptSummary; /** * The total blocking durations in each phase by invoker for scripts that - * intersect the INP event. + * intersect the INP interaction. */ totalDurationsPerPhase?: Record< 'inputDelay' | 'processingDuration' | 'presentationDelay', @@ -60,12 +131,12 @@ export interface LongAnimationFrameSummary { >; /** * The total forced style and layout durations as provided by Long Animation - * Frame scripts intersecting the INP event. + * Frame scripts intersecting the INP interaction. */ totalForcedStyleAndLayoutDuration?: number; /** * The total non-force (i.e. end-of-frame) style and layout duration from any - * Long Animation Frames intersecting INP event. + * Long Animation Frames intersecting INP interaction. */ totalNonForcedStyleAndLayoutDuration?: number; /** @@ -135,8 +206,8 @@ export interface INPAttribution { /** * If the browser supports the Long Animation Frame API, this object * summarises information relevant to INP across the long animation frames - * intersecting the INP event. See the LongAnimationFrameSummary definition - * for an explanation of what is included. + * intersecting the INP interaction. See the LongAnimationFrameSummary + * definition for an explanation of what is included. */ longAnimationFrameSummary?: LongAnimationFrameSummary; /** From add385a8783d856a23671f639b3800152c218fd5 Mon Sep 17 00:00:00 2001 From: Barry Pollard Date: Tue, 10 Dec 2024 00:13:29 +0000 Subject: [PATCH 12/23] Fix test --- test/e2e/onINP-test.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/test/e2e/onINP-test.js b/test/e2e/onINP-test.js index 31184a4e..43c29517 100644 --- a/test/e2e/onINP-test.js +++ b/test/e2e/onINP-test.js @@ -715,17 +715,20 @@ describe('onINP()', async function () { const [inp1] = await getBeacons(); assert(inp1.attribution.longAnimationFrameEntries.length > 0); assert(inp1.attribution.longAnimationFrameSummary != {}); - assert.equal(inp1.attribution.longAnimationFrameSummary.numScripts, 1); + assert.equal( + inp1.attribution.longAnimationFrameSummary.numIntersectingScripts, + 1, + ); assert.equal( JSON.stringify( - inp1.attribution.longAnimationFrameSummary.slowestScript, + inp1.attribution.longAnimationFrameSummary.slowestScript.entry, ), JSON.stringify( inp1.attribution.longAnimationFrameEntries[0].scripts[0], ), ); assert.equal( - inp1.attribution.longAnimationFrameSummary.slowestScriptPhase, + inp1.attribution.longAnimationFrameSummary.slowestScript.phase, 'processingDuration', ); assert.equal( From 94abf790aac07ed762d1b0887a346a9e93d5e5c1 Mon Sep 17 00:00:00 2001 From: Barry Pollard Date: Tue, 10 Dec 2024 13:29:43 +0000 Subject: [PATCH 13/23] Review feedback --- src/attribution/onINP.ts | 64 +++++++++++++++++++++------------------- src/types/inp.ts | 26 +++++++++------- test/e2e/onINP-test.js | 8 ++--- 3 files changed, 52 insertions(+), 46 deletions(-) diff --git a/src/attribution/onINP.ts b/src/attribution/onINP.ts index 76e778e5..c61822eb 100644 --- a/src/attribution/onINP.ts +++ b/src/attribution/onINP.ts @@ -28,6 +28,7 @@ import { INPAttribution, INPMetric, INPMetricWithAttribution, + INPSubpart, LongAnimationFrameSummary, SlowestScriptSummary, ReportOpts, @@ -236,8 +237,6 @@ const getIntersectingLoAFs = ( }; const getLoAFSummary = (attribution: INPAttribution) => { - const loafAttribution: LongAnimationFrameSummary = {}; - // Stats across all LoAF entries and scripts. const interactionTime = attribution.interactionTime; const inputDelay = attribution.inputDelay; @@ -248,10 +247,12 @@ const getLoAFSummary = (attribution: INPAttribution) => { let numScripts = 0; let slowestScriptDuration = 0; let slowestScriptEntry!: PerformanceScriptTiming; - let slowestScriptPhase: string = ''; - const phases = {} as Record>; + let slowestScriptPhase!: INPSubpart; + const subparts: Partial< + Record>> + > = {}; - attribution.longAnimationFrameEntries.forEach((loafEntry) => { + for (const loafEntry of attribution.longAnimationFrameEntries) { totalNonForcedStyleAndLayoutDuration += loafEntry.startTime + loafEntry.duration - loafEntry.styleAndLayoutStart; loafEntry.scripts.forEach((script) => { @@ -265,41 +266,38 @@ const getLoAFSummary = (attribution: INPAttribution) => { numScripts++; totalForcedStyleAndLayout += script.forcedStyleAndLayoutDuration; const invokerType = script.invokerType; - let phase = 'processingDuration'; + let subpart: INPSubpart = 'processingDuration'; if (script.startTime < interactionTime + inputDelay) { - phase = 'inputDelay'; + subpart = 'inputDelay'; } else if ( script.startTime > interactionTime + inputDelay + processingDuration ) { - phase = 'presentationDelay'; + subpart = 'presentationDelay'; } - // Define the record if necessary - phases[phase] ??= {} as Record; - phases[phase][invokerType] ??= 0; + // Define the record if necessary. Annoyingly Typescript doesn't yet + // recognise this so need a few `!`s on the next two lines to convinced + // it is typed. + subparts[subpart] ??= {}; + subparts[subpart]![invokerType] ??= 0; // Increment it with this value - phases[phase][invokerType] += intersectingScriptDuration; + subparts[subpart]![invokerType]! += intersectingScriptDuration; if (intersectingScriptDuration > slowestScriptDuration) { slowestScriptEntry = script; - slowestScriptPhase = phase; + slowestScriptPhase = subpart; slowestScriptDuration = intersectingScriptDuration; } }); - }); + } - // Gather the summary information into the loafAttribution object - loafAttribution.numLongAnimationFrames = - attribution.longAnimationFrameEntries.length; - loafAttribution.numIntersectingScripts = numScripts; + // Gather the slowest script summary information + let slowestScript: SlowestScriptSummary | undefined = undefined; if (slowestScriptEntry !== null) { - const slowestScript: SlowestScriptSummary = { + slowestScript = { entry: slowestScriptEntry, - phase: slowestScriptPhase as - | 'inputDelay' - | 'processingDuration' - | 'presentationDelay', + subpart: slowestScriptPhase, intersectingDuration: slowestScriptDuration, totalDuration: slowestScriptEntry.duration, compileDuration: @@ -317,16 +315,20 @@ const getLoAFSummary = (attribution: INPAttribution) => { sourceFunctionName: slowestScriptEntry.sourceFunctionName, sourceCharPosition: slowestScriptEntry.sourceCharPosition, }; - - loafAttribution.slowestScript = slowestScript; } - loafAttribution.totalDurationsPerPhase = phases; - loafAttribution.totalForcedStyleAndLayoutDuration = totalForcedStyleAndLayout; - loafAttribution.totalNonForcedStyleAndLayoutDuration = - totalNonForcedStyleAndLayoutDuration; - loafAttribution.totalScriptDuration = totalScriptTime; - return loafAttribution; + // Gather the summary information into the loafAttribution object + const loafSummary: LongAnimationFrameSummary = { + numLongAnimationFrames: attribution.longAnimationFrameEntries.length, + numIntersectingScripts: numScripts, + slowestScript: slowestScript, + totalDurationsPerSubpart: subparts, + totalForcedStyleAndLayoutDuration: totalForcedStyleAndLayout, + totalNonForcedStyleAndLayoutDuration: totalNonForcedStyleAndLayoutDuration, + totalScriptDuration: totalScriptTime, + }; + + return loafSummary; }; const attributeINP = (metric: INPMetric): INPMetricWithAttribution => { diff --git a/src/types/inp.ts b/src/types/inp.ts index ebd8ec4f..36a0cd05 100644 --- a/src/types/inp.ts +++ b/src/types/inp.ts @@ -24,6 +24,11 @@ export interface INPMetric extends Metric { entries: PerformanceEventTiming[]; } +export type INPSubpart = + | 'inputDelay' + | 'processingDuration' + | 'presentationDelay'; + /** * An object containing potentially-helpful debugging information summarized * from the slowest script intersecting the INP duration (ignoring any script @@ -36,9 +41,9 @@ export interface SlowestScriptSummary { */ entry: PerformanceScriptTiming; /** - * The INP phase where the longest script ran. + * The INP subpart where the longest script ran. */ - phase: 'inputDelay' | 'processingDuration' | 'presentationDelay'; + subpart: INPSubpart; /** * The amount of time the slowest script intersected the INP duration. */ @@ -111,40 +116,39 @@ export interface LongAnimationFrameSummary { * NOTE: This may be be less than the total count of scripts in the Long * Animation Frames as some scripts may occur before the interaction. */ - numIntersectingScripts?: number; + numIntersectingScripts: number; /** * The number of Long Animation Frames intersecting the INP interaction. */ - numLongAnimationFrames?: number; + numLongAnimationFrames: number; /** * Summary details about the slowest Long Animation Frame script that * intersects the INP interaction. */ slowestScript?: SlowestScriptSummary; /** - * The total blocking durations in each phase by invoker for scripts that + * The total blocking durations in each subpart by invoker for scripts that * intersect the INP interaction. */ - totalDurationsPerPhase?: Record< - 'inputDelay' | 'processingDuration' | 'presentationDelay', - Record + totalDurationsPerSubpart: Partial< + Record>> >; /** * The total forced style and layout durations as provided by Long Animation * Frame scripts intersecting the INP interaction. */ - totalForcedStyleAndLayoutDuration?: number; + totalForcedStyleAndLayoutDuration: number; /** * The total non-force (i.e. end-of-frame) style and layout duration from any * Long Animation Frames intersecting INP interaction. */ - totalNonForcedStyleAndLayoutDuration?: number; + totalNonForcedStyleAndLayoutDuration: number; /** * The total duration of Long Animation Frame scripts that intersect the INP * duration. Note, this includes forced style and layout within those * scripts. */ - totalScriptDuration?: number; + totalScriptDuration: number; } /** diff --git a/test/e2e/onINP-test.js b/test/e2e/onINP-test.js index 43c29517..34827d70 100644 --- a/test/e2e/onINP-test.js +++ b/test/e2e/onINP-test.js @@ -728,24 +728,24 @@ describe('onINP()', async function () { ), ); assert.equal( - inp1.attribution.longAnimationFrameSummary.slowestScript.phase, + inp1.attribution.longAnimationFrameSummary.slowestScript.subpart, 'processingDuration', ); assert.equal( Object.keys( - inp1.attribution.longAnimationFrameSummary.totalDurationsPerPhase, + inp1.attribution.longAnimationFrameSummary.totalDurationsPerSubpart, ), 'processingDuration', ); assert.equal( Object.keys( - inp1.attribution.longAnimationFrameSummary.totalDurationsPerPhase + inp1.attribution.longAnimationFrameSummary.totalDurationsPerSubpart .processingDuration, ), 'event-listener', ); assert( - inp1.attribution.longAnimationFrameSummary.totalDurationsPerPhase + inp1.attribution.longAnimationFrameSummary.totalDurationsPerSubpart .processingDuration['event-listener'] >= 100, ); assert.equal( From dcd95df339250891718de8cdb7bc6d0a966a84f3 Mon Sep 17 00:00:00 2001 From: Barry Pollard Date: Tue, 10 Dec 2024 16:40:10 +0000 Subject: [PATCH 14/23] Document updates --- README.md | 18 +++++++++++------- src/types/inp.ts | 11 ++++++++--- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index e5595dec..ef742ee1 100644 --- a/README.md +++ b/README.md @@ -958,12 +958,16 @@ export interface LongAnimationFrameSummary { */ slowestScript?: slowestScriptSummary; /** - * The total blocking durations in each phase by invoker for scripts that + * The total blocking durations in each sub-part by invoker for scripts that * intersect the INP interaction. - */ - totalDurationsPerPhase?: Record< - 'inputDelay' | 'processingDuration' | 'presentationDelay', - Record + * For example: + * { + * 'inputDelay': { 'event-listener': 185, 'user-callback': 28}, + * 'processingDuration': { 'event-listener': 144}, + * } + */ + totalDurationsPerSubpart?: Partial< + Record>> >; /** * The total forced style and layout durations as provided by Long Animation @@ -999,9 +1003,9 @@ export interface SlowestScriptSummary { */ entry: PerformanceScriptTiming; /** - * The INP phase where the longest script ran. + * The INP sub-part where the longest script ran. */ - phase: 'inputDelay' | 'processingDuration' | 'presentationDelay'; + subpart: INPSubpart; //'inputDelay' | 'processingDuration' | 'presentationDelay'; /** * The amount of time the slowest script intersected the INP duration. */ diff --git a/src/types/inp.ts b/src/types/inp.ts index 36a0cd05..257f65ef 100644 --- a/src/types/inp.ts +++ b/src/types/inp.ts @@ -41,9 +41,9 @@ export interface SlowestScriptSummary { */ entry: PerformanceScriptTiming; /** - * The INP subpart where the longest script ran. + * The INP sub-part where the longest script ran. */ - subpart: INPSubpart; + subpart: INPSubpart; //'inputDelay' | 'processingDuration' | 'presentationDelay'; /** * The amount of time the slowest script intersected the INP duration. */ @@ -127,8 +127,13 @@ export interface LongAnimationFrameSummary { */ slowestScript?: SlowestScriptSummary; /** - * The total blocking durations in each subpart by invoker for scripts that + * The total blocking durations in each sub-part by invoker for scripts that * intersect the INP interaction. + * For example: + * { + * 'inputDelay': { 'event-listener': 185, 'user-callback': 28}, + * 'processingDuration': { 'event-listener': 144}, + * } */ totalDurationsPerSubpart: Partial< Record>> From 9513369c40dded45fff16a9b87cca0906d3e13bb Mon Sep 17 00:00:00 2001 From: Barry Pollard Date: Tue, 10 Dec 2024 16:43:50 +0000 Subject: [PATCH 15/23] Sync README --- README.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index ef742ee1..c22020e5 100644 --- a/README.md +++ b/README.md @@ -947,16 +947,16 @@ export interface LongAnimationFrameSummary { * NOTE: This may be be less than the total count of scripts in the Long * Animation Frames as some scripts may occur before the interaction. */ - numIntersectingScripts?: number; + numIntersectingScripts: number; /** * The number of Long Animation Frames intersecting the INP interaction. */ - numLongAnimationFrames?: number; + numLongAnimationFrames: number; /** * Summary details about the slowest Long Animation Frame script that * intersects the INP interaction. */ - slowestScript?: slowestScriptSummary; + slowestScript?: SlowestScriptSummary; /** * The total blocking durations in each sub-part by invoker for scripts that * intersect the INP interaction. @@ -966,25 +966,25 @@ export interface LongAnimationFrameSummary { * 'processingDuration': { 'event-listener': 144}, * } */ - totalDurationsPerSubpart?: Partial< + totalDurationsPerSubpart: Partial< Record>> >; /** * The total forced style and layout durations as provided by Long Animation - * Frame scripts intercepting the INP interaction. + * Frame scripts intersecting the INP interaction. */ - totalForcedStyleAndLayoutDuration?: number; + totalForcedStyleAndLayoutDuration: number; /** * The total non-force (i.e. end-of-frame) style and layout duration from any - * Long Animation Frames intercepting INP interaction. + * Long Animation Frames intersecting INP interaction. */ - totalNonForcedStyleAndLayoutDuration?: number; + totalNonForcedStyleAndLayoutDuration: number; /** * The total duration of Long Animation Frame scripts that intersect the INP * duration. Note, this includes forced style and layout within those * scripts. */ - totalScriptDuration?: number; + totalScriptDuration: number; } ``` From e0c2f08801a7198b639dc2afbe2b75cb1113bf79 Mon Sep 17 00:00:00 2001 From: Barry Pollard Date: Tue, 10 Dec 2024 16:54:34 +0000 Subject: [PATCH 16/23] Flatten SlowestScriptSummary into LongAnimationFrameSummary --- README.md | 99 +++++++++++++++++----------------------- src/attribution/onINP.ts | 49 ++++++++------------ src/types/inp.ts | 73 ++++++++++++----------------- test/e2e/onINP-test.js | 4 +- 4 files changed, 93 insertions(+), 132 deletions(-) diff --git a/README.md b/README.md index c22020e5..18911c57 100644 --- a/README.md +++ b/README.md @@ -952,115 +952,98 @@ export interface LongAnimationFrameSummary { * The number of Long Animation Frames intersecting the INP interaction. */ numLongAnimationFrames: number; - /** - * Summary details about the slowest Long Animation Frame script that - * intersects the INP interaction. - */ - slowestScript?: SlowestScriptSummary; - /** - * The total blocking durations in each sub-part by invoker for scripts that - * intersect the INP interaction. - * For example: - * { - * 'inputDelay': { 'event-listener': 185, 'user-callback': 28}, - * 'processingDuration': { 'event-listener': 144}, - * } - */ - totalDurationsPerSubpart: Partial< - Record>> - >; - /** - * The total forced style and layout durations as provided by Long Animation - * Frame scripts intersecting the INP interaction. - */ - totalForcedStyleAndLayoutDuration: number; - /** - * The total non-force (i.e. end-of-frame) style and layout duration from any - * Long Animation Frames intersecting INP interaction. - */ - totalNonForcedStyleAndLayoutDuration: number; - /** - * The total duration of Long Animation Frame scripts that intersect the INP - * duration. Note, this includes forced style and layout within those - * scripts. - */ - totalScriptDuration: number; -} -``` - -#### `slowestScriptSummary` - -```ts -/** - * An object containing potentially-helpful debugging information summarized - * from the slowest script intersecting the INP duration (ignoring any script - * duration prior to the INP time). - */ -export interface SlowestScriptSummary { /** * The slowest Long Animation Frame script that intersects the INP * interaction. */ - entry: PerformanceScriptTiming; + slowestScriptEntry: PerformanceScriptTiming; /** * The INP sub-part where the longest script ran. */ - subpart: INPSubpart; //'inputDelay' | 'processingDuration' | 'presentationDelay'; + slowestScriptSubpart: INPSubpart; //'inputDelay' | 'processingDuration' | 'presentationDelay'; /** * The amount of time the slowest script intersected the INP duration. */ - intersectingDuration: number; + slowestScriptIntersectingDuration: number; /** * The total duration time of the slowest script (compile and execution, * including forced style and layout). Note this may be longer than the * intersectingScriptDuration if the INP interaction happened mid-script. */ - totalDuration: number; + slowestScriptTotalDuration: number; /** * The compile duration of the slowest script. Note this may be longer * than the intersectingScriptDuration if the INP interaction happened * mid-script. */ - compileDuration: number; + slowestScriptCompileDuration: number; /** * The execution duration of the slowest script. Note this may be longer * than the intersectingScriptDuration if the INP interaction happened * mid-script. */ - executionDuration: number; + slowestScriptExecutionDuration: number; /** /** * The forced style and layoult duration of the slowest script. Note this * may be longer than the intersectingScriptDuration if the INP interaction * happened mid-script. */ - forcedStyleAndLayoutDuration: number; + slowestScriptForcedStyleAndLayoutDuration: number; /** * The pause duration of the slowest script. Note this may be longer * than the intersectingScriptDuration if the INP interaction happened * mid-script. */ - pauseDuration: number; + slowestScriptPauseDuration: number; /** * The invokerType of the slowest script. */ - invokerType: ScriptInvokerType; + slowestScriptInvokerType: ScriptInvokerType; /** * The invoker of the slowest script. */ - invoker?: string; + slowestScriptInvoker?: string; /** * The sourceURL of the slowest script. */ - sourceURL?: string; + slowestScriptSourceURL?: string; /** * The sourceFunctionName of the slowest script. */ - sourceFunctionName?: string; + slowestScriptSourceFunctionName?: string; /** * The sourceCharPosition of the slowest script. */ - sourceCharPosition?: number; + slowestScriptSourceCharPosition?: number; + /** + * The total blocking durations in each sub-part by invoker for scripts that + * intersect the INP interaction. + * For example: + * { + * 'inputDelay': { 'event-listener': 185, 'user-callback': 28}, + * 'processingDuration': { 'event-listener': 144}, + * } + */ + totalDurationsPerSubpart: Partial< + Record>> + >; + /** + * The total forced style and layout durations as provided by Long Animation + * Frame scripts intersecting the INP interaction. + */ + totalForcedStyleAndLayoutDuration: number; + /** + * The total non-force (i.e. end-of-frame) style and layout duration from any + * Long Animation Frames intersecting INP interaction. + */ + totalNonForcedStyleAndLayoutDuration: number; + /** + * The total duration of Long Animation Frame scripts that intersect the INP + * duration. Note, this includes forced style and layout within those + * scripts. + */ + totalScriptDuration: number; } ``` diff --git a/src/attribution/onINP.ts b/src/attribution/onINP.ts index c61822eb..c09bcf33 100644 --- a/src/attribution/onINP.ts +++ b/src/attribution/onINP.ts @@ -30,7 +30,6 @@ import { INPMetricWithAttribution, INPSubpart, LongAnimationFrameSummary, - SlowestScriptSummary, ReportOpts, } from '../types.js'; @@ -247,7 +246,7 @@ const getLoAFSummary = (attribution: INPAttribution) => { let numScripts = 0; let slowestScriptDuration = 0; let slowestScriptEntry!: PerformanceScriptTiming; - let slowestScriptPhase!: INPSubpart; + let slowestScriptSubpart!: INPSubpart; const subparts: Partial< Record>> > = {}; @@ -286,42 +285,34 @@ const getLoAFSummary = (attribution: INPAttribution) => { if (intersectingScriptDuration > slowestScriptDuration) { slowestScriptEntry = script; - slowestScriptPhase = subpart; + slowestScriptSubpart = subpart; slowestScriptDuration = intersectingScriptDuration; } }); } - // Gather the slowest script summary information - let slowestScript: SlowestScriptSummary | undefined = undefined; - if (slowestScriptEntry !== null) { - slowestScript = { - entry: slowestScriptEntry, - subpart: slowestScriptPhase, - intersectingDuration: slowestScriptDuration, - totalDuration: slowestScriptEntry.duration, - compileDuration: - slowestScriptEntry.executionStart - slowestScriptEntry.startTime, - executionDuration: - slowestScriptEntry.startTime + - slowestScriptEntry.duration - - slowestScriptEntry.executionStart, - forcedStyleAndLayoutDuration: - slowestScriptEntry.forcedStyleAndLayoutDuration, - pauseDuration: slowestScriptEntry.pauseDuration, - invokerType: slowestScriptEntry.invokerType, - invoker: slowestScriptEntry.invoker, - sourceURL: slowestScriptEntry.sourceURL, - sourceFunctionName: slowestScriptEntry.sourceFunctionName, - sourceCharPosition: slowestScriptEntry.sourceCharPosition, - }; - } - // Gather the summary information into the loafAttribution object const loafSummary: LongAnimationFrameSummary = { numLongAnimationFrames: attribution.longAnimationFrameEntries.length, numIntersectingScripts: numScripts, - slowestScript: slowestScript, + slowestScriptEntry: slowestScriptEntry, + slowestScriptSubpart: slowestScriptSubpart, + slowestScriptIntersectingDuration: slowestScriptDuration, + slowestScriptTotalDuration: slowestScriptEntry?.duration, + slowestScriptCompileDuration: + slowestScriptEntry?.executionStart - slowestScriptEntry?.startTime, + slowestScriptExecutionDuration: + slowestScriptEntry?.startTime + + slowestScriptEntry?.duration - + slowestScriptEntry?.executionStart, + slowestScriptForcedStyleAndLayoutDuration: + slowestScriptEntry?.forcedStyleAndLayoutDuration, + slowestScriptPauseDuration: slowestScriptEntry?.pauseDuration, + slowestScriptInvokerType: slowestScriptEntry?.invokerType, + slowestScriptInvoker: slowestScriptEntry?.invoker, + slowestScriptSourceURL: slowestScriptEntry?.sourceURL, + slowestScriptSourceFunctionName: slowestScriptEntry?.sourceFunctionName, + slowestScriptSourceCharPosition: slowestScriptEntry?.sourceCharPosition, totalDurationsPerSubpart: subparts, totalForcedStyleAndLayoutDuration: totalForcedStyleAndLayout, totalNonForcedStyleAndLayoutDuration: totalNonForcedStyleAndLayoutDuration, diff --git a/src/types/inp.ts b/src/types/inp.ts index 257f65ef..29ede4cf 100644 --- a/src/types/inp.ts +++ b/src/types/inp.ts @@ -31,101 +31,88 @@ export type INPSubpart = /** * An object containing potentially-helpful debugging information summarized - * from the slowest script intersecting the INP duration (ignoring any script - * duration prior to the INP time). + * from the LongAnimationFrames intersecting the INP interaction. + * + * NOTE: Long Animation Frames below 50 milliseconds are not reported, and + * so their scripts cannot be included. For Long Animation Frames that are + * reported, only scripts above 5 milliseconds are included. */ -export interface SlowestScriptSummary { +export interface LongAnimationFrameSummary { + /** + * The number of Long Animation Frame scripts that intersect the INP + * interaction. + * NOTE: This may be be less than the total count of scripts in the Long + * Animation Frames as some scripts may occur before the interaction. + */ + numIntersectingScripts: number; + /** + * The number of Long Animation Frames intersecting the INP interaction. + */ + numLongAnimationFrames: number; /** * The slowest Long Animation Frame script that intersects the INP * interaction. */ - entry: PerformanceScriptTiming; + slowestScriptEntry: PerformanceScriptTiming; /** * The INP sub-part where the longest script ran. */ - subpart: INPSubpart; //'inputDelay' | 'processingDuration' | 'presentationDelay'; + slowestScriptSubpart: INPSubpart; //'inputDelay' | 'processingDuration' | 'presentationDelay'; /** * The amount of time the slowest script intersected the INP duration. */ - intersectingDuration: number; + slowestScriptIntersectingDuration: number; /** * The total duration time of the slowest script (compile and execution, * including forced style and layout). Note this may be longer than the * intersectingScriptDuration if the INP interaction happened mid-script. */ - totalDuration: number; + slowestScriptTotalDuration: number; /** * The compile duration of the slowest script. Note this may be longer * than the intersectingScriptDuration if the INP interaction happened * mid-script. */ - compileDuration: number; + slowestScriptCompileDuration: number; /** * The execution duration of the slowest script. Note this may be longer * than the intersectingScriptDuration if the INP interaction happened * mid-script. */ - executionDuration: number; + slowestScriptExecutionDuration: number; /** /** * The forced style and layoult duration of the slowest script. Note this * may be longer than the intersectingScriptDuration if the INP interaction * happened mid-script. */ - forcedStyleAndLayoutDuration: number; + slowestScriptForcedStyleAndLayoutDuration: number; /** * The pause duration of the slowest script. Note this may be longer * than the intersectingScriptDuration if the INP interaction happened * mid-script. */ - pauseDuration: number; + slowestScriptPauseDuration: number; /** * The invokerType of the slowest script. */ - invokerType: ScriptInvokerType; + slowestScriptInvokerType: ScriptInvokerType; /** * The invoker of the slowest script. */ - invoker?: string; + slowestScriptInvoker?: string; /** * The sourceURL of the slowest script. */ - sourceURL?: string; + slowestScriptSourceURL?: string; /** * The sourceFunctionName of the slowest script. */ - sourceFunctionName?: string; + slowestScriptSourceFunctionName?: string; /** * The sourceCharPosition of the slowest script. */ - sourceCharPosition?: number; -} - -/** - * An object containing potentially-helpful debugging information summarized - * from the LongAnimationFrames intersecting the INP interaction. - * - * NOTE: Long Animation Frames below 50 milliseconds are not reported, and - * so their scripts cannot be included. For Long Animation Frames that are - * reported, only scripts above 5 milliseconds are included. - */ -export interface LongAnimationFrameSummary { - /** - * The number of Long Animation Frame scripts that intersect the INP - * interaction. - * NOTE: This may be be less than the total count of scripts in the Long - * Animation Frames as some scripts may occur before the interaction. - */ - numIntersectingScripts: number; - /** - * The number of Long Animation Frames intersecting the INP interaction. - */ - numLongAnimationFrames: number; - /** - * Summary details about the slowest Long Animation Frame script that - * intersects the INP interaction. - */ - slowestScript?: SlowestScriptSummary; + slowestScriptSourceCharPosition?: number; /** * The total blocking durations in each sub-part by invoker for scripts that * intersect the INP interaction. diff --git a/test/e2e/onINP-test.js b/test/e2e/onINP-test.js index 34827d70..4de39385 100644 --- a/test/e2e/onINP-test.js +++ b/test/e2e/onINP-test.js @@ -721,14 +721,14 @@ describe('onINP()', async function () { ); assert.equal( JSON.stringify( - inp1.attribution.longAnimationFrameSummary.slowestScript.entry, + inp1.attribution.longAnimationFrameSummary.slowestScriptEntry, ), JSON.stringify( inp1.attribution.longAnimationFrameEntries[0].scripts[0], ), ); assert.equal( - inp1.attribution.longAnimationFrameSummary.slowestScript.subpart, + inp1.attribution.longAnimationFrameSummary.slowestScriptSubpart, 'processingDuration', ); assert.equal( From 80d272a1628c926d8dbe0cbcbf2bcc2d06bad59a Mon Sep 17 00:00:00 2001 From: Barry Pollard Date: Tue, 10 Dec 2024 19:19:24 +0000 Subject: [PATCH 17/23] Types --- src/types.ts | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/src/types.ts b/src/types.ts index a5cf6f1a..e76c627a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -88,13 +88,7 @@ declare global { readonly element: Element | null; } - // https://w3c.github.io/long-animation-frame/#sec-PerformanceLongAnimationFrameTiming - export type ScriptWindowAttribution = - | 'self' - | 'descendant' - | 'ancestor' - | 'same-page' - | 'other'; + // https://w3c.github.io/long-animation-frames/#sec-PerformanceLongAnimationFrameTiming export type ScriptInvokerType = | 'classic-script' | 'module-script' @@ -102,12 +96,18 @@ declare global { | 'user-callback' | 'resolve-promise' | 'reject-promise'; - + export type ScriptWindowAttribution = + | 'self' + | 'descendant' + | 'ancestor' + | 'same-page' + | 'other'; interface PerformanceScriptTiming extends PerformanceEntry { readonly startTime: DOMHighResTimeStamp; readonly duration: DOMHighResTimeStamp; readonly name: string; readonly entryType: string; + readonly invokerType: ScriptInvokerType; readonly invoker: string; readonly executionStart: DOMHighResTimeStamp; @@ -119,14 +119,16 @@ declare global { readonly window?: Window; readonly windowAttribution: ScriptWindowAttribution; } - - // https://w3c.github.io/long-animation-frame/#sec-PerformanceLongAnimationFrameTiming interface PerformanceLongAnimationFrameTiming extends PerformanceEntry { - duration: DOMHighResTimeStamp; - renderStart: DOMHighResTimeStamp; - styleAndLayoutStart: DOMHighResTimeStamp; - firstUIEventTimestamp: DOMHighResTimeStamp; - blockingDuration: DOMHighResTimeStamp; - scripts: Array; + readonly startTime: DOMHighResTimeStamp; + readonly duration: DOMHighResTimeStamp; + readonly name: string; + readonly entryType: string; + + readonly renderStart: DOMHighResTimeStamp; + readonly styleAndLayoutStart: DOMHighResTimeStamp; + readonly blockingDuration: DOMHighResTimeStamp; + readonly firstUIEventTimestamp: DOMHighResTimeStamp; + readonly scripts: Array; } } From 807c85b1a3d776bdb682b8fa64e6367e815f1a02 Mon Sep 17 00:00:00 2001 From: Barry Pollard Date: Mon, 16 Dec 2024 22:23:13 +0000 Subject: [PATCH 18/23] Move slowestScript back under it's own object --- README.md | 230 +++++++++++++++++++-------------------- src/attribution/onINP.ts | 40 +++---- src/types/inp.ts | 75 ++++++++----- test/e2e/onINP-test.js | 4 +- 4 files changed, 180 insertions(+), 169 deletions(-) diff --git a/README.md b/README.md index 18911c57..ab9a0af9 100644 --- a/README.md +++ b/README.md @@ -898,6 +898,118 @@ interface INPAttribution { * definition for an explanation of what is included. */ longAnimationFrameSummary?: LongAnimationFrameSummary; + { + /** + * The number of Long Animation Frame scripts that intersect the INP + * interaction. + * NOTE: This may be be less than the total count of scripts in the Long + * Animation Frames as some scripts may occur before the interaction. + */ + numIntersectingScripts: number; + /** + * The number of Long Animation Frames intersecting the INP interaction. + */ + numLongAnimationFrames: number; + /** + * The slowest Long Animation Frame script that intersects the INP + * interaction. + */ + slowestScript: SlowestScriptSummary; + { + /** + * The slowest Long Animation Frame script that intersects the INP + * interaction. + */ + entry: PerformanceScriptTiming; + /** + * The INP sub-part where the longest script ran. + */ + subpart: INPSubpart; //'inputDelay' | 'processingDuration' | 'presentationDelay'; + /** + * The amount of time the slowest script intersected the INP duration. + */ + intersectingDuration: number; + /** + * The total duration time of the slowest script (compile and execution, + * including forced style and layout). Note this may be longer than the + * intersectingScriptDuration if the INP interaction happened mid-script. + */ + totalDuration: number; + /** + * The compile duration of the slowest script. Note this may be longer + * than the intersectingScriptDuration if the INP interaction happened + * mid-script. + */ + compileDuration: number; + /** + * The execution duration of the slowest script. Note this may be longer + * than the intersectingScriptDuration if the INP interaction happened + * mid-script. + */ + executionDuration: number; + /** + /** + * The forced style and layoult duration of the slowest script. Note this + * may be longer than the intersectingScriptDuration if the INP interaction + * happened mid-script. + */ + forcedStyleAndLayoutDuration: number; + /** + * The pause duration of the slowest script. Note this may be longer + * than the intersectingScriptDuration if the INP interaction happened + * mid-script. + */ + pauseDuration: number; + /** + * The invokerType of the slowest script. + */ + invokerType: ScriptInvokerType; + /** + * The invoker of the slowest script. + */ + invoker?: string; + /** + * The sourceURL of the slowest script. + */ + sourceURL?: string; + /** + * The sourceFunctionName of the slowest script. + */ + sourceFunctionName?: string; + /** + * The sourceCharPosition of the slowest script. + */ + sourceCharPosition?: number; + } + /** + * The total blocking durations in each sub-part by invoker for scripts that + * intersect the INP interaction. + * For example: + * { + * 'inputDelay': { 'event-listener': 185, 'user-callback': 28}, + * 'processingDuration': { 'event-listener': 144}, + * } + */ + totalDurationsPerSubpart: Partial< + Record>> + >; + /** + * The total forced style and layout durations as provided by Long Animation + * Frame scripts intersecting the INP interaction. + */ + totalForcedStyleAndLayoutDuration: number; + /** + * The total non-force (i.e. end-of-frame) style and layout duration from any + * Long Animation Frames intersecting INP interaction. + */ + totalNonForcedStyleAndLayoutDuration: number; + /** + * The total duration of Long Animation Frame scripts that intersect the INP + * duration. Note, this includes forced style and layout within those + * scripts. + */ + totalScriptDuration: number; + } /** * The time from when the user interacted with the page until when the * browser was first able to start processing event listeners for that @@ -929,124 +1041,6 @@ interface INPAttribution { } ``` -#### `LongAnimationFrameSummary` - -```ts -/** - * An object containing potentially-helpful debugging information summarized - * from the LongAnimationFrames intersecting the INP interaction. - * - * NOTE: Long Animation Frames below 50 milliseconds are not reported, and - * so their scripts cannot be included. For Long Animation Frames that are - * reported, only scripts above 5 milliseconds are included. - */ -export interface LongAnimationFrameSummary { - /** - * The number of Long Animation Frame scripts that intersect the INP - * interaction. - * NOTE: This may be be less than the total count of scripts in the Long - * Animation Frames as some scripts may occur before the interaction. - */ - numIntersectingScripts: number; - /** - * The number of Long Animation Frames intersecting the INP interaction. - */ - numLongAnimationFrames: number; - /** - * The slowest Long Animation Frame script that intersects the INP - * interaction. - */ - slowestScriptEntry: PerformanceScriptTiming; - /** - * The INP sub-part where the longest script ran. - */ - slowestScriptSubpart: INPSubpart; //'inputDelay' | 'processingDuration' | 'presentationDelay'; - /** - * The amount of time the slowest script intersected the INP duration. - */ - slowestScriptIntersectingDuration: number; - /** - * The total duration time of the slowest script (compile and execution, - * including forced style and layout). Note this may be longer than the - * intersectingScriptDuration if the INP interaction happened mid-script. - */ - slowestScriptTotalDuration: number; - /** - * The compile duration of the slowest script. Note this may be longer - * than the intersectingScriptDuration if the INP interaction happened - * mid-script. - */ - slowestScriptCompileDuration: number; - /** - * The execution duration of the slowest script. Note this may be longer - * than the intersectingScriptDuration if the INP interaction happened - * mid-script. - */ - slowestScriptExecutionDuration: number; - /** - /** - * The forced style and layoult duration of the slowest script. Note this - * may be longer than the intersectingScriptDuration if the INP interaction - * happened mid-script. - */ - slowestScriptForcedStyleAndLayoutDuration: number; - /** - * The pause duration of the slowest script. Note this may be longer - * than the intersectingScriptDuration if the INP interaction happened - * mid-script. - */ - slowestScriptPauseDuration: number; - /** - * The invokerType of the slowest script. - */ - slowestScriptInvokerType: ScriptInvokerType; - /** - * The invoker of the slowest script. - */ - slowestScriptInvoker?: string; - /** - * The sourceURL of the slowest script. - */ - slowestScriptSourceURL?: string; - /** - * The sourceFunctionName of the slowest script. - */ - slowestScriptSourceFunctionName?: string; - /** - * The sourceCharPosition of the slowest script. - */ - slowestScriptSourceCharPosition?: number; - /** - * The total blocking durations in each sub-part by invoker for scripts that - * intersect the INP interaction. - * For example: - * { - * 'inputDelay': { 'event-listener': 185, 'user-callback': 28}, - * 'processingDuration': { 'event-listener': 144}, - * } - */ - totalDurationsPerSubpart: Partial< - Record>> - >; - /** - * The total forced style and layout durations as provided by Long Animation - * Frame scripts intersecting the INP interaction. - */ - totalForcedStyleAndLayoutDuration: number; - /** - * The total non-force (i.e. end-of-frame) style and layout duration from any - * Long Animation Frames intersecting INP interaction. - */ - totalNonForcedStyleAndLayoutDuration: number; - /** - * The total duration of Long Animation Frame scripts that intersect the INP - * duration. Note, this includes forced style and layout within those - * scripts. - */ - totalScriptDuration: number; -} -``` - #### `LCPAttribution` ```ts diff --git a/src/attribution/onINP.ts b/src/attribution/onINP.ts index c09bcf33..d6f62c7f 100644 --- a/src/attribution/onINP.ts +++ b/src/attribution/onINP.ts @@ -291,28 +291,30 @@ const getLoAFSummary = (attribution: INPAttribution) => { }); } - // Gather the summary information into the loafAttribution object + // Gather the loaf summary information into the loafAttribution object const loafSummary: LongAnimationFrameSummary = { numLongAnimationFrames: attribution.longAnimationFrameEntries.length, numIntersectingScripts: numScripts, - slowestScriptEntry: slowestScriptEntry, - slowestScriptSubpart: slowestScriptSubpart, - slowestScriptIntersectingDuration: slowestScriptDuration, - slowestScriptTotalDuration: slowestScriptEntry?.duration, - slowestScriptCompileDuration: - slowestScriptEntry?.executionStart - slowestScriptEntry?.startTime, - slowestScriptExecutionDuration: - slowestScriptEntry?.startTime + - slowestScriptEntry?.duration - - slowestScriptEntry?.executionStart, - slowestScriptForcedStyleAndLayoutDuration: - slowestScriptEntry?.forcedStyleAndLayoutDuration, - slowestScriptPauseDuration: slowestScriptEntry?.pauseDuration, - slowestScriptInvokerType: slowestScriptEntry?.invokerType, - slowestScriptInvoker: slowestScriptEntry?.invoker, - slowestScriptSourceURL: slowestScriptEntry?.sourceURL, - slowestScriptSourceFunctionName: slowestScriptEntry?.sourceFunctionName, - slowestScriptSourceCharPosition: slowestScriptEntry?.sourceCharPosition, + slowestScript: { + entry: slowestScriptEntry, + subpart: slowestScriptSubpart, + intersectingDuration: slowestScriptDuration, + totalDuration: slowestScriptEntry?.duration, + compileDuration: + slowestScriptEntry?.executionStart - slowestScriptEntry?.startTime, + executionDuration: + slowestScriptEntry?.startTime + + slowestScriptEntry?.duration - + slowestScriptEntry?.executionStart, + forcedStyleAndLayoutDuration: + slowestScriptEntry?.forcedStyleAndLayoutDuration, + pauseDuration: slowestScriptEntry?.pauseDuration, + invokerType: slowestScriptEntry?.invokerType, + invoker: slowestScriptEntry?.invoker, + sourceURL: slowestScriptEntry?.sourceURL, + sourceFunctionName: slowestScriptEntry?.sourceFunctionName, + sourceCharPosition: slowestScriptEntry?.sourceCharPosition, + }, totalDurationsPerSubpart: subparts, totalForcedStyleAndLayoutDuration: totalForcedStyleAndLayout, totalNonForcedStyleAndLayoutDuration: totalNonForcedStyleAndLayoutDuration, diff --git a/src/types/inp.ts b/src/types/inp.ts index 29ede4cf..18385dfb 100644 --- a/src/types/inp.ts +++ b/src/types/inp.ts @@ -30,89 +30,104 @@ export type INPSubpart = | 'presentationDelay'; /** - * An object containing potentially-helpful debugging information summarized - * from the LongAnimationFrames intersecting the INP interaction. + * Summary information about the slowest script intersecting the INP duration + * as provided by the Long Animation Frame API. * - * NOTE: Long Animation Frames below 50 milliseconds are not reported, and - * so their scripts cannot be included. For Long Animation Frames that are - * reported, only scripts above 5 milliseconds are included. + * NOTE: Only scripts above 5 milliseconds are included in long animation + * frames. */ -export interface LongAnimationFrameSummary { - /** - * The number of Long Animation Frame scripts that intersect the INP - * interaction. - * NOTE: This may be be less than the total count of scripts in the Long - * Animation Frames as some scripts may occur before the interaction. - */ - numIntersectingScripts: number; - /** - * The number of Long Animation Frames intersecting the INP interaction. - */ - numLongAnimationFrames: number; +export interface SlowestScriptSummary { /** * The slowest Long Animation Frame script that intersects the INP * interaction. */ - slowestScriptEntry: PerformanceScriptTiming; + entry: PerformanceScriptTiming; /** * The INP sub-part where the longest script ran. */ - slowestScriptSubpart: INPSubpart; //'inputDelay' | 'processingDuration' | 'presentationDelay'; + subpart: INPSubpart; //'inputDelay' | 'processingDuration' | 'presentationDelay'; /** * The amount of time the slowest script intersected the INP duration. */ - slowestScriptIntersectingDuration: number; + intersectingDuration: number; /** * The total duration time of the slowest script (compile and execution, * including forced style and layout). Note this may be longer than the * intersectingScriptDuration if the INP interaction happened mid-script. */ - slowestScriptTotalDuration: number; + totalDuration: number; /** * The compile duration of the slowest script. Note this may be longer * than the intersectingScriptDuration if the INP interaction happened * mid-script. */ - slowestScriptCompileDuration: number; + compileDuration: number; /** * The execution duration of the slowest script. Note this may be longer * than the intersectingScriptDuration if the INP interaction happened * mid-script. */ - slowestScriptExecutionDuration: number; + executionDuration: number; /** /** * The forced style and layoult duration of the slowest script. Note this * may be longer than the intersectingScriptDuration if the INP interaction * happened mid-script. */ - slowestScriptForcedStyleAndLayoutDuration: number; + forcedStyleAndLayoutDuration: number; /** * The pause duration of the slowest script. Note this may be longer * than the intersectingScriptDuration if the INP interaction happened * mid-script. */ - slowestScriptPauseDuration: number; + pauseDuration: number; /** * The invokerType of the slowest script. */ - slowestScriptInvokerType: ScriptInvokerType; + invokerType: ScriptInvokerType; /** * The invoker of the slowest script. */ - slowestScriptInvoker?: string; + invoker?: string; /** * The sourceURL of the slowest script. */ - slowestScriptSourceURL?: string; + sourceURL?: string; /** * The sourceFunctionName of the slowest script. */ - slowestScriptSourceFunctionName?: string; + sourceFunctionName?: string; /** * The sourceCharPosition of the slowest script. */ - slowestScriptSourceCharPosition?: number; + sourceCharPosition?: number; +} + +/** + * An object containing potentially-helpful debugging information summarized + * from the LongAnimationFrames intersecting the INP interaction. + * + * NOTE: Long Animation Frames below 50 milliseconds are not reported, and + * so their scripts cannot be included. For Long Animation Frames that are + * reported, only scripts above 5 milliseconds are included. + */ +export interface LongAnimationFrameSummary { + /** + * The number of Long Animation Frame scripts that intersect the INP + * interaction. + * NOTE: This may be be less than the total count of scripts in the Long + * Animation Frames as some scripts may occur before the interaction. + */ + numIntersectingScripts: number; + /** + * The number of Long Animation Frames intersecting the INP interaction. + */ + numLongAnimationFrames: number; + /** + * The slowest Long Animation Frame script that intersects the INP + * interaction. + */ + slowestScript: SlowestScriptSummary; /** * The total blocking durations in each sub-part by invoker for scripts that * intersect the INP interaction. diff --git a/test/e2e/onINP-test.js b/test/e2e/onINP-test.js index 4de39385..34827d70 100644 --- a/test/e2e/onINP-test.js +++ b/test/e2e/onINP-test.js @@ -721,14 +721,14 @@ describe('onINP()', async function () { ); assert.equal( JSON.stringify( - inp1.attribution.longAnimationFrameSummary.slowestScriptEntry, + inp1.attribution.longAnimationFrameSummary.slowestScript.entry, ), JSON.stringify( inp1.attribution.longAnimationFrameEntries[0].scripts[0], ), ); assert.equal( - inp1.attribution.longAnimationFrameSummary.slowestScriptSubpart, + inp1.attribution.longAnimationFrameSummary.slowestScript.subpart, 'processingDuration', ); assert.equal( From f8fa54302fcc2131e9721387325c20f84f41d93c Mon Sep 17 00:00:00 2001 From: Barry Pollard Date: Wed, 15 Jan 2025 16:53:15 +0000 Subject: [PATCH 19/23] Review feedback --- README.md | 6 +++--- src/attribution/onINP.ts | 13 +++++++------ src/types/inp.ts | 6 +++--- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index d9afc477..dbe644de 100644 --- a/README.md +++ b/README.md @@ -1018,8 +1018,8 @@ interface INPAttribution { sourceCharPosition?: number; } /** - * The total blocking durations in each sub-part by invoker for scripts that - * intersect the INP interaction. + * The total intersecting durations in each sub-part by invoker for + * scripts that intersect the INP interaction. * For example: * { * 'inputDelay': { 'event-listener': 185, 'user-callback': 28}, @@ -1044,7 +1044,7 @@ interface INPAttribution { * duration. Note, this includes forced style and layout within those * scripts. */ - totalScriptDuration: number; + totalIntersectingScriptsDuration: number; } /** * The time from when the user interacted with the page until when the diff --git a/src/attribution/onINP.ts b/src/attribution/onINP.ts index edd65a65..854338e1 100644 --- a/src/attribution/onINP.ts +++ b/src/attribution/onINP.ts @@ -269,7 +269,7 @@ export const onINP = ( const processingDuration = attribution.processingDuration; let totalNonForcedStyleAndLayoutDuration = 0; let totalForcedStyleAndLayout = 0; - let totalScriptTime = 0; + let totalIntersectingScriptsDuration = 0; let numScripts = 0; let slowestScriptDuration = 0; let slowestScriptEntry!: PerformanceScriptTiming; @@ -283,14 +283,15 @@ export const onINP = ( loafEntry.startTime + loafEntry.duration - loafEntry.styleAndLayoutStart; - loafEntry.scripts.forEach((script) => { + + for (const script of loafEntry.scripts) { const scriptEndTime = script.startTime + script.duration; if (scriptEndTime < interactionTime) { return; } const intersectingScriptDuration = scriptEndTime - Math.max(interactionTime, script.startTime); - totalScriptTime += intersectingScriptDuration; + totalIntersectingScriptsDuration += intersectingScriptDuration; numScripts++; totalForcedStyleAndLayout += script.forcedStyleAndLayoutDuration; const invokerType = script.invokerType; @@ -305,7 +306,7 @@ export const onINP = ( } // Define the record if necessary. Annoyingly TypeScript doesn't yet - // recognise this so need a few `!`s on the next two lines to convinced + // recognize this so need a few `!`s on the next two lines to convinced // it is typed. subparts[subpart] ??= {}; subparts[subpart]![invokerType] ??= 0; @@ -317,7 +318,7 @@ export const onINP = ( slowestScriptSubpart = subpart; slowestScriptDuration = intersectingScriptDuration; } - }); + } } // Gather the loaf summary information into the loafAttribution object @@ -348,7 +349,7 @@ export const onINP = ( totalForcedStyleAndLayoutDuration: totalForcedStyleAndLayout, totalNonForcedStyleAndLayoutDuration: totalNonForcedStyleAndLayoutDuration, - totalScriptDuration: totalScriptTime, + totalIntersectingScriptsDuration: totalIntersectingScriptsDuration, }; return loafSummary; diff --git a/src/types/inp.ts b/src/types/inp.ts index 9988966e..bc1ffcaa 100644 --- a/src/types/inp.ts +++ b/src/types/inp.ts @@ -142,8 +142,8 @@ export interface LongAnimationFrameSummary { */ slowestScript: SlowestScriptSummary; /** - * The total blocking durations in each sub-part by invoker for scripts that - * intersect the INP interaction. + * The total intersecting durations in each sub-part by invoker for + * scripts that intersect the INP interaction. * For example: * { * 'inputDelay': { 'event-listener': 185, 'user-callback': 28}, @@ -168,7 +168,7 @@ export interface LongAnimationFrameSummary { * duration. Note, this includes forced style and layout within those * scripts. */ - totalScriptDuration: number; + totalIntersectingScriptsDuration: number; } /** From 9c61110977672ad422b94f361a155f03e9490db6 Mon Sep 17 00:00:00 2001 From: Barry Pollard Date: Wed, 15 Jan 2025 17:05:56 +0000 Subject: [PATCH 20/23] More feedback --- README.md | 2 +- src/types/inp.ts | 2 +- test/e2e/onINP-test.js | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index dbe644de..51fbde16 100644 --- a/README.md +++ b/README.md @@ -1042,7 +1042,7 @@ interface INPAttribution { /** * The total duration of Long Animation Frame scripts that intersect the INP * duration. Note, this includes forced style and layout within those - * scripts. + * scripts and is limited to scripts > 5 milliseconds. */ totalIntersectingScriptsDuration: number; } diff --git a/src/types/inp.ts b/src/types/inp.ts index bc1ffcaa..919478d6 100644 --- a/src/types/inp.ts +++ b/src/types/inp.ts @@ -166,7 +166,7 @@ export interface LongAnimationFrameSummary { /** * The total duration of Long Animation Frame scripts that intersect the INP * duration. Note, this includes forced style and layout within those - * scripts. + * scriptsn and is limited to scripts > 5 milliseconds. */ totalIntersectingScriptsDuration: number; } diff --git a/test/e2e/onINP-test.js b/test/e2e/onINP-test.js index 645b9b7c..685f1947 100644 --- a/test/e2e/onINP-test.js +++ b/test/e2e/onINP-test.js @@ -886,7 +886,8 @@ describe('onINP()', async function () { inp1.attribution.presentationDelay, ); assert( - inp1.attribution.longAnimationFrameSummary.totalScriptDuration >= 100, + inp1.attribution.longAnimationFrameSummary + .totalIntersectingScriptsDuration >= 100, ); }); }); From 19a2f4212f6712da96aed3f4faaef677ba57f6fc Mon Sep 17 00:00:00 2001 From: Barry Pollard Date: Mon, 20 Jan 2025 16:07:46 +0000 Subject: [PATCH 21/23] Update test/e2e/onINP-test.js Co-authored-by: Philip Walton --- test/e2e/onINP-test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/e2e/onINP-test.js b/test/e2e/onINP-test.js index 685f1947..770176d2 100644 --- a/test/e2e/onINP-test.js +++ b/test/e2e/onINP-test.js @@ -841,7 +841,7 @@ describe('onINP()', async function () { const [inp1] = await getBeacons(); assert(inp1.attribution.longAnimationFrameEntries.length > 0); - assert(inp1.attribution.longAnimationFrameSummary != {}); + assert(Object.keys(inp1.attribution.longAnimationFrameSummary).length !== 0); assert.equal( inp1.attribution.longAnimationFrameSummary.numIntersectingScripts, 1, From 934b803ea586ccaa5573d4c6f9ba7fc30ef1c1ae Mon Sep 17 00:00:00 2001 From: Barry Pollard Date: Mon, 20 Jan 2025 16:07:57 +0000 Subject: [PATCH 22/23] Update src/attribution/onINP.ts Co-authored-by: Philip Walton --- src/attribution/onINP.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/attribution/onINP.ts b/src/attribution/onINP.ts index 854338e1..9e755103 100644 --- a/src/attribution/onINP.ts +++ b/src/attribution/onINP.ts @@ -299,7 +299,7 @@ export const onINP = ( if (script.startTime < interactionTime + inputDelay) { subpart = 'inputDelay'; } else if ( - script.startTime > + script.startTime >= interactionTime + inputDelay + processingDuration ) { subpart = 'presentationDelay'; From 3fa8ae03d1d49c59b6bd960d0fd507cc3349fe3e Mon Sep 17 00:00:00 2001 From: Barry Pollard Date: Mon, 20 Jan 2025 16:09:22 +0000 Subject: [PATCH 23/23] Linting --- test/e2e/onINP-test.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/e2e/onINP-test.js b/test/e2e/onINP-test.js index 770176d2..ce8d822a 100644 --- a/test/e2e/onINP-test.js +++ b/test/e2e/onINP-test.js @@ -841,7 +841,9 @@ describe('onINP()', async function () { const [inp1] = await getBeacons(); assert(inp1.attribution.longAnimationFrameEntries.length > 0); - assert(Object.keys(inp1.attribution.longAnimationFrameSummary).length !== 0); + assert( + Object.keys(inp1.attribution.longAnimationFrameSummary).length !== 0, + ); assert.equal( inp1.attribution.longAnimationFrameSummary.numIntersectingScripts, 1,