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 12 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
138 changes: 138 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -891,6 +891,13 @@ interface INPAttribution {
* are detect, this array will be empty.
*/
longAnimationFrameEntries: PerformanceLongAnimationFrameTiming[];
/**
* If the browser supports the Long Animation Frame API, this object
* summarises information relevant to INP across the long animation frames
* intersecting the INP interaction. See the LongAnimationFrameSummary
* definition for an explanation of what is included.
*/
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 +929,137 @@ 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;
/**
* 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
* intersect the INP interaction.
*/
totalDurationsPerPhase?: Record<
'inputDelay' | 'processingDuration' | 'presentationDelay',
Record<ScriptInvokerType, number>
>;
/**
* The total forced style and layout durations as provided by Long Animation
* 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 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;
tunetheweb marked this conversation as resolved.
Show resolved Hide resolved
}
```

#### `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
Expand Down
98 changes: 98 additions & 0 deletions src/attribution/onINP.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ import {
INPAttribution,
INPMetric,
INPMetricWithAttribution,
LongAnimationFrameSummary,
SlowestScriptSummary,
ReportOpts,
} from '../types.js';

Expand Down Expand Up @@ -233,6 +235,100 @@ 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 totalNonForcedStyleAndLayoutDuration = 0;
let totalForcedStyleAndLayout = 0;
let totalScriptTime = 0;
let numScripts = 0;
let slowestScriptDuration = 0;
let slowestScriptEntry!: PerformanceScriptTiming;
let slowestScriptPhase: string = '';
const phases = {} as Record<string, Record<ScriptInvokerType, number>>;
tunetheweb marked this conversation as resolved.
Show resolved Hide resolved

attribution.longAnimationFrameEntries.forEach((loafEntry) => {
tunetheweb marked this conversation as resolved.
Show resolved Hide resolved
totalNonForcedStyleAndLayoutDuration +=
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;
}
const intersectingScriptDuration =
scriptEndTime - Math.max(interactionTime, script.startTime);
totalScriptTime += intersectingScriptDuration;
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 invokerType = script.invokerType;
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 = 'presentationDelay';
}

// Define the record if necessary
phases[phase] ??= {} as Record<ScriptInvokerType, number>;
tunetheweb marked this conversation as resolved.
Show resolved Hide resolved
phases[phase][invokerType] ??= 0;
// Increment it with this value
phases[phase][invokerType] += intersectingScriptDuration;

if (intersectingScriptDuration > slowestScriptDuration) {
slowestScriptEntry = script;
slowestScriptPhase = phase;
slowestScriptDuration = intersectingScriptDuration;
}
});
});

// 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',
tunetheweb marked this conversation as resolved.
Show resolved Hide resolved
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 =
totalNonForcedStyleAndLayoutDuration;
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 +383,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>;
}
}
Loading
Loading