Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
josephwynn-sc committed Feb 21, 2025
1 parent f0c5594 commit a1fd61f
Show file tree
Hide file tree
Showing 7 changed files with 125 additions and 48 deletions.
1 change: 1 addition & 0 deletions src/lux.ts
Original file line number Diff line number Diff line change
Expand Up @@ -887,6 +887,7 @@ LUX = (function () {
LCP.reset();
CLS.reset();
INP.reset();
LoAF.reset();
nErrors = 0;
gFirstInputDelay = undefined;

Expand Down
6 changes: 1 addition & 5 deletions src/math.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
export function floor(x: number): number {
return Math.floor(x);
}

export const max = Math.max;

export const floor = Math.floor;
export const round = Math.round;

/**
Expand Down
33 changes: 30 additions & 3 deletions src/metric/INP.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { BeaconMetricData, BeaconMetricKey } from "../beacon";
import { getNodeSelector } from "../dom";
import { clamp, floor } from "../math";
import { clamp, floor, max } from "../math";

Check failure on line 3 in src/metric/INP.ts

View workflow job for this annotation

GitHub Actions / build

'max' is declared but its value is never read.

Check failure on line 3 in src/metric/INP.ts

View workflow job for this annotation

GitHub Actions / build

'max' is defined but never used
import { performance } from "../performance";
import { processTimeMetric } from "../timing";
import { getEntries as getLoAFEntries, summarizeLoAFScripts } from "./LoAF";
Expand All @@ -25,6 +25,12 @@ export interface Interaction {
target: Node | null;
}

export enum INPPhase {
InputDelay = "ID",
ProcessingTime = "PT",
PresentationDelay = "PD",
}

// A list of the slowest interactions
let slowestEntries: Interaction[] = [];

Expand Down Expand Up @@ -120,8 +126,16 @@ export function getData(): BeaconMetricData[BeaconMetricKey.INP] | undefined {

const inpScripts = getLoAFEntries()
.flatMap((entry) => entry.scripts)
.filter((script) => script.startTime + script.duration < startTime);
// TODO: Adjust duration, calculate phase
.filter((script) => script.startTime + script.duration >= startTime)
.map((_script) => {
const script = JSON.parse(JSON.stringify(_script));

// Exclude any duration that occurred before the interaction started
script.duration = script.startTime + script.duration - startTime;
script.inpPhase = getINPPhase(script, interaction);

return script as PerformanceScriptTiming;
});

const loafScripts = summarizeLoAFScripts(inpScripts);

Expand All @@ -145,6 +159,19 @@ export function getData(): BeaconMetricData[BeaconMetricKey.INP] | undefined {
};
}

export function getINPPhase(script: PerformanceScriptTiming, interaction: Interaction): INPPhase {
const { processingStart, processingTime, startTime } = interaction;
const inputDelay = processingStart - startTime;

if (script.startTime < startTime + inputDelay) {
return INPPhase.InputDelay;
} else if (script.startTime >= startTime + inputDelay + processingTime) {
return INPPhase.PresentationDelay;
}

return INPPhase.ProcessingTime;
}

function getInteractionCount(): number {
if ("interactionCount" in performance) {
return performance.interactionCount;
Expand Down
30 changes: 19 additions & 11 deletions src/metric/LoAF.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { floor } from "../math";
import { INPPhase } from "./INP";

export type LoAFSummary = {
totalEntries: number;
totalDuration: number;
Expand All @@ -6,7 +9,6 @@ export type LoAFSummary = {
totalWorkDuration: number;
entries: LoAFEntry[];
scripts: LoAFScriptSummary[];
inpScripts: LoAFScriptSummary[];
};

export type LoAFEntry = {
Expand All @@ -20,19 +22,15 @@ export type LoAFEntry = {
export type LoAFScriptSummary = {
sourceUrl: string;
sourceFunctionName: string;
timings: Array<[number, number]>; // [startTime, duration]
totalEntries: number;
totalDuration: number;
totalPauseDuration: number;
totalForcedStyleAndLayoutDuration: number;
invoker: string;
inpPhase?: INPPhase;
};

enum INPPhase {
InputDelay = "ID",
ProcessingTime = "PT",
PresentationDelay = "PD",
}

let entries: PerformanceLongAnimationFrameTiming[] = [];

export function processEntry(entry: PerformanceLongAnimationFrameTiming): void {
Expand All @@ -56,11 +54,14 @@ export function getData(): LoAFSummary {
totalWorkDuration: 0,
entries: [],
scripts: [],
inpScripts: [],
};

entries.forEach((entry) => {
const { startTime, blockingDuration, duration, renderStart, styleAndLayoutStart } = entry;
const startTime = floor(entry.startTime);
const blockingDuration = floor(entry.blockingDuration);
const duration = floor(entry.duration);
const renderStart = floor(entry.renderStart);
const styleAndLayoutStart = floor(entry.styleAndLayoutStart);

data.totalDuration += duration;
data.totalBlockingDuration += blockingDuration;
Expand All @@ -83,6 +84,8 @@ export function getData(): LoAFSummary {
return data;
}

type ScriptWithINPPhase = PerformanceScriptTiming & { inpPhase?: INPPhase };

export function summarizeLoAFScripts(scripts: PerformanceScriptTiming[]): LoAFScriptSummary[] {
const summary: Record<string, LoAFScriptSummary> = {};

Expand All @@ -92,16 +95,21 @@ export function summarizeLoAFScripts(scripts: PerformanceScriptTiming[]): LoAFSc
summary[key] = {
sourceUrl: script.sourceURL,
sourceFunctionName: script.sourceFunctionName,
timings: [],
totalEntries: 0,
totalDuration: 0,
totalPauseDuration: 0,
totalForcedStyleAndLayoutDuration: 0,
invoker: script.invoker,
inpPhase: (script as ScriptWithINPPhase).inpPhase,
};
}

summary[key].totalEntries++;
summary[key].totalDuration += script.duration;
summary[key].totalForcedStyleAndLayoutDuration += script.forcedStyleAndLayoutDuration;
summary[key].totalDuration += floor(script.duration);
summary[key].totalPauseDuration += floor(script.pauseDuration);
summary[key].totalForcedStyleAndLayoutDuration += floor(script.forcedStyleAndLayoutDuration);
summary[key].timings.push([floor(script.startTime), floor(script.duration)]);
});

return Object.values(summary);
Expand Down
69 changes: 69 additions & 0 deletions tests/integration/post-beacon/loaf.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { test, expect } from "@playwright/test";
import { BeaconPayload } from "../../../src/beacon";
import { entryTypeSupported } from "../../helpers/browsers";
import RequestInterceptor from "../../request-interceptor";

test.describe("POST beacon LoAF", () => {
test("LoAFs are measured", async ({ page }) => {
const luxRequests = new RequestInterceptor(page).createRequestMatcher("/store/");
await page.goto("/long-animation-frames.html", { waitUntil: "networkidle" });
await page.goto("/default.html");
await luxRequests.waitForMatchingRequest();
const b = luxRequests.get(0)!.postDataJSON() as BeaconPayload;
const loafSupported = await entryTypeSupported(page, "long-animation-frame");

if (loafSupported) {
const loaf = b.loaf!;
expect(loaf.totalBlockingDuration).toBeGreaterThan(0);
expect(loaf.totalDuration).toBeGreaterThan(0);
expect(loaf.totalEntries).toBeGreaterThan(0);
expect(loaf.totalStyleAndLayoutDuration).toBeGreaterThan(0);
expect(loaf.totalWorkDuration).toBeGreaterThan(0);
expect(loaf.entries.length).toBeGreaterThan(0);
expect(loaf.scripts.length).toBeGreaterThan(0);
} else {
expect(b.loaf).toBeUndefined();
}
});

test("LoAFs are collected as INP attribution", async ({ page }) => {
const luxRequests = new RequestInterceptor(page).createRequestMatcher("/store/");
await page.goto("/long-animation-frames.html", { waitUntil: "networkidle" });
await page.locator("#create-long-task").click();
await page.waitForTimeout(1000);
await page.goto("/default.html");
await luxRequests.waitForMatchingRequest();
const b = luxRequests.get(0)!.postDataJSON() as BeaconPayload;
const inpSupported = await entryTypeSupported(page, "event");
const loafSupported = await entryTypeSupported(page, "long-animation-frame");
const inp = b.inp!;

if (inpSupported) {
expect(inp.value).toBeGreaterThanOrEqual(0);
} else {
expect(inp).toBeUndefined();
}

const loafScripts = inp.attribution!.loafScripts;

if (loafSupported) {
expect(loafScripts.length).toEqual(1);

const script = loafScripts[0];
const sourceUrl = new URL(script.sourceUrl);
const [scriptStartTime, scriptDuration] = script.timings[0];

expect(sourceUrl.pathname).toEqual("/long-animation-frames.html");
expect(script.invoker).toEqual("BUTTON#create-long-task.onclick");
expect(script.sourceFunctionName).toEqual("globalClickHandler");
expect(script.totalEntries).toEqual(1);
expect(script.totalDuration).toBeGreaterThanOrEqual(50);
expect(script.totalDuration).toBeLessThan(60);
expect(scriptStartTime).toBeGreaterThanOrEqual(inp.startTime);
expect(scriptDuration).toBeGreaterThanOrEqual(50);
expect(scriptDuration).toBeLessThan(60);
} else {
expect(loafScripts.length).toEqual(0);
}
});
});
28 changes: 0 additions & 28 deletions tests/integration/post-beacon/long-animation-frames.spec.ts

This file was deleted.

6 changes: 5 additions & 1 deletion tests/test-pages/long-animation-frames.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ <h1>LUX long animation frames test page</h1>

<img src="eve.jpg" elementtiming="eve-image">

<button id="create-long-task" type="button">Create another long task</button>
<button id="create-long-task" type="button">Make a delayed paint</button>

<div style="margin-top: 2000px;">
<p id="scroll-anchor">This element can be scrolled to.</p>
Expand All @@ -25,6 +25,10 @@ <h1>LUX long animation frames test page</h1>

function globalClickHandler() {
createLongTask(50);

const firstImage = document.querySelector("img");
const newImage = firstImage.cloneNode();
firstImage.insertAdjacentElement("afterend", newImage);
}

console.log("App start");
Expand Down

0 comments on commit a1fd61f

Please sign in to comment.