Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

LoAF Summary attribution information #574

Open
wants to merge 25 commits into
base: v5
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
tunetheweb marked this conversation as resolved.
Show resolved Hide resolved
* 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
Expand Down Expand Up @@ -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;
tunetheweb marked this conversation as resolved.
Show resolved Hide resolved
/**
* The INP phase where the longest script ran.
*/
slowestScriptPhase?:
| 'inputDelay'
| 'processingDuration'
| 'presentationDelay';
tunetheweb marked this conversation as resolved.
Show resolved Hide resolved
/**
* The total blocking durations in each phase by invoker for scripts that
* intersect the INP event.
*/
totalDurationsPerPhase?: Record<
'inputDelay' | 'processingDuration' | 'presentationDelay',
Record<string, number>
tunetheweb marked this conversation as resolved.
Show resolved Hide resolved
>;
/**
* The total forced style and layout durations as provided by Long Animation
* Frame scripts intercepting the INP event.
tunetheweb marked this conversation as resolved.
Show resolved Hide resolved
*/
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;
tunetheweb marked this conversation as resolved.
Show resolved Hide resolved
}
```

#### `LCPAttribution`

```ts
Expand Down
75 changes: 75 additions & 0 deletions src/attribution/onINP.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
INPAttribution,
INPMetric,
INPMetricWithAttribution,
LongAnimationFrameSummary,
ReportOpts,
} from '../types.js';

Expand Down Expand Up @@ -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;
tunetheweb marked this conversation as resolved.
Show resolved Hide resolved
let totalForcedStyleAndLayout = 0;
let totalScriptTime = 0;
let numScripts = 0;
let slowestScriptDuration = 0;
let slowestScript: PerformanceScriptTiming | null = null;
let slowestScriptPhase: string = '';
const phases: Record<string, Record<string, number>> = {} as Record<
string,
Record<string, number>
>;

attribution.longAnimationFrameEntries.forEach((loafEntry) => {
tunetheweb marked this conversation as resolved.
Show resolved Hide resolved
totalStyleAndLayout +=
loafEntry.startTime + loafEntry.duration - loafEntry.styleAndLayoutStart;
loafEntry.scripts.forEach((script) => {
tunetheweb marked this conversation as resolved.
Show resolved Hide resolved
const scriptEndTime = script.startTime + script.duration;
if (scriptEndTime < interactionTime) {
return;
}
totalScriptTime += script.duration;
tunetheweb marked this conversation as resolved.
Show resolved Hide resolved
numScripts++;
totalForcedStyleAndLayout += script.forcedStyleAndLayoutDuration;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there's an open question whether we should only be taking the blocking/intersecting portion of all duration metrics. WDYT?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately we can't for forced style and layout since we don't have the timings LoAF. Maybe that's a good reason to keep this simple and just take it for all intersecting scripts (which is what it does currently)?

const blockingDuration =
tunetheweb marked this conversation as resolved.
Show resolved Hide resolved
scriptEndTime - Math.max(interactionTime, script.startTime);
const invokerType = script.invokerType; //.replace('-', '_');
tunetheweb marked this conversation as resolved.
Show resolved Hide resolved
let phase = 'processingDuration';
tunetheweb marked this conversation as resolved.
Show resolved Hide resolved
if (script.startTime < interactionTime + inputDelay) {
tunetheweb marked this conversation as resolved.
Show resolved Hide resolved
phase = 'inputDelay';
} else if (
script.startTime >
interactionTime + inputDelay + processingDuration
tunetheweb marked this conversation as resolved.
Show resolved Hide resolved
) {
phase = 'presentation';
tunetheweb marked this conversation as resolved.
Show resolved Hide resolved
}
if (!(phase in phases)) {
phases[phase] = {};
}
if (!(invokerType in phases[phase])) {
phases[phase][invokerType] = 0;
}
tunetheweb marked this conversation as resolved.
Show resolved Hide resolved
phases[phase][invokerType] += blockingDuration;

if (blockingDuration > slowestScriptDuration) {
slowestScript = script;
slowestScriptPhase = phase;
slowestScriptDuration = blockingDuration;
tunetheweb marked this conversation as resolved.
Show resolved Hide resolved
}
});
});
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;
tunetheweb marked this conversation as resolved.
Show resolved Hide resolved
};

const attributeINP = (metric: INPMetric): INPMetricWithAttribution => {
const firstEntry = metric.entries[0];
const group = entryToEntriesGroupMap.get(firstEntry)!;
Expand Down Expand Up @@ -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,
Expand Down
38 changes: 37 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<PerformanceScriptTiming>;
}
}
61 changes: 61 additions & 0 deletions src/types/inp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, number>
>;
/**
* 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;
tunetheweb marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* An object containing potentially-helpful debugging information that
* can be sent along with the INP value for the current page visit in order
Expand Down Expand Up @@ -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
Expand Down
44 changes: 44 additions & 0 deletions test/e2e/onINP-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -714,6 +714,50 @@ describe('onINP()', async function () {

const [inp1] = await getBeacons();
assert(inp1.attribution.longAnimationFrameEntries.length > 0);
assert(inp1.attribution.longAnimationFrameSummary != {});
tunetheweb marked this conversation as resolved.
Show resolved Hide resolved
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 <=
inp1.attribution.presentationDelay,
);
assert(
inp1.attribution.longAnimationFrameSummary.totalScriptDuration >= 100,
);
});
});
});
Expand Down
Loading