Skip to content

Commit

Permalink
Update whyNP.js script
Browse files Browse the repository at this point in the history
  • Loading branch information
mmocny committed Aug 18, 2023
1 parent 037adbf commit cb1d6fe
Show file tree
Hide file tree
Showing 6 changed files with 311 additions and 0 deletions.
3 changes: 3 additions & 0 deletions sandbox/whyNP/onNewLoAFInteraction.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function onNewLoAFInteraction(loaf, events) {
console.log("New LoAF Interaction!", loaf, events);
}
56 changes: 56 additions & 0 deletions sandbox/whyNP/processLoAFEntry.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
function floorObject(o) {
return Object.fromEntries(
Array.from(Object.entries(o)).map(([key, value]) => [
key,
typeof value === "number" ? Math.floor(value) : value
])
);
}

export default function processLoAFEntry(entry) {
const startTime = entry.startTime;
const endTime = entry.startTime + entry.duration;

const delay = entry.desiredRenderStart
? Math.max(0, entry.startTime - entry.desiredRenderStart)
: 0;
const deferredDuration = Math.max(
0,
entry.desiredRenderStart - entry.startTime
);

const rafDuration = entry.styleAndLayoutStart - entry.renderStart;
const totalForcedStyleAndLayoutDuration = entry.scripts.reduce(
(sum, script) => sum + script.forcedStyleAndLayoutDuration,
0
);
const styleAndLayoutDuration = entry.styleAndLayoutStart
? endTime - entry.styleAndLayoutStart
: 0;

const scripts = entry.scripts.map((script) => {
const delay = script.startTime - script.desiredExecutionStart;
const scriptEnd = script.startTime + script.duration;
const compileDuration = script.executionStart - script.startTime;
const execDuration = scriptEnd - script.executionStart;
return floorObject({
delay,
compileDuration,
execDuration,
...script.toJSON()
});
});

return floorObject({
startTime,
endTime,
delay,
deferredDuration,
rafDuration,
styleAndLayoutDuration,
totalForcedStyleAndLayoutDuration,
...entry.toJSON(),
scripts
});
}

117 changes: 117 additions & 0 deletions sandbox/whyNP/public/whyNP.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
(() => {
// processLoAFEntry.js
function floorObject(o) {
return Object.fromEntries(
Array.from(Object.entries(o)).map(([key, value]) => [
key,
typeof value === "number" ? Math.floor(value) : value
])
);
}
function processLoAFEntry(entry) {
const startTime = entry.startTime;
const endTime = entry.startTime + entry.duration;
const delay = entry.desiredRenderStart ? Math.max(0, entry.startTime - entry.desiredRenderStart) : 0;
const deferredDuration = Math.max(
0,
entry.desiredRenderStart - entry.startTime
);
const rafDuration = entry.styleAndLayoutStart - entry.renderStart;
const totalForcedStyleAndLayoutDuration = entry.scripts.reduce(
(sum, script) => sum + script.forcedStyleAndLayoutDuration,
0
);
const styleAndLayoutDuration = entry.styleAndLayoutStart ? endTime - entry.styleAndLayoutStart : 0;
const scripts = entry.scripts.map((script) => {
const delay2 = script.startTime - script.desiredExecutionStart;
const scriptEnd = script.startTime + script.duration;
const compileDuration = script.executionStart - script.startTime;
const execDuration = scriptEnd - script.executionStart;
return floorObject({
delay: delay2,
compileDuration,
execDuration,
...script.toJSON()
});
});
return floorObject({
startTime,
endTime,
delay,
deferredDuration,
rafDuration,
styleAndLayoutDuration,
totalForcedStyleAndLayoutDuration,
...entry.toJSON(),
scripts
});
}

// onNewLoAFInteraction.js
function onNewLoAFInteraction(loaf, events2) {
console.log("New LoAF Interaction!", loaf, events2);
}

// whyNP.js
var loafs = [];
var events = [];
function groupInteractionEventsByLoAF() {
let i = 0;
const framesData = loafs.map((loaf, j) => {
const frameData = {
frameNum: j,
blockingDuration: loaf.blockingDuration,
loaf,
scripts: loaf.scripts,
events: []
};
for (; i < events.length; i++) {
const event = events[i];
const eventEndTime = event.startTime + event.duration;
if (eventEndTime < loaf.startTime)
continue;
if (event.processingStart > loaf.endTime)
break;
const loafAndEventOverlap = loaf.startTime <= event.processingStart;
frameData.events.push(event);
}
return frameData;
});
const longFramesWithInteractions = framesData.filter(
(fd) => (
/* fd.blockingDuration && */
fd.events.some((entry) => entry.interactionId > 0)
)
);
return longFramesWithInteractions;
}
var previousLoggedFrame = -1;
function logIfInteresting() {
const results = groupInteractionEventsByLoAF();
if (results.length === 0)
return;
const newestFrame = results.at(-1);
if (newestFrame.frameNum === previousLoggedFrame)
return;
onNewLoAFInteraction(newestFrame.loaf, newestFrame.events);
previousLoggedFrame = newestFrame.frameNum;
}
new PerformanceObserver((entries) => {
loafs.push(...entries.getEntries().map(processLoAFEntry));
logIfInteresting();
}).observe({
type: "long-animation-frame",
buffered: true
});
new PerformanceObserver((entries) => {
const interactionEntries = entries.getEntries();
if (interactionEntries.length === 0)
return;
events.push(...interactionEntries);
logIfInteresting();
}).observe({
type: "event",
buffered: true,
durationThreshold: 0
});
})();
15 changes: 15 additions & 0 deletions sandbox/whyNP/test/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html>
<head>
<title>WhyNP.js test page</title>
<meta charset="UTF-8" />
</head>

<body>
<div id="app">Click Anywhere and watch console.log</div>

<script src="../whyNP.js" type="module"></script>

<script src="./index.js"></script>
</body>
</html>
19 changes: 19 additions & 0 deletions sandbox/whyNP/test/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
function block(ms) {
const target = performance.now() + ms;
while (performance.now() < target);
}

document.addEventListener("click", async () => {
// Sync block for less than 50ms
block(30);

// Optional: visual update
document.body.innerText = performance.now();

// microtask hop
await 0;

// block for over 50ms
block(100);
});

101 changes: 101 additions & 0 deletions sandbox/whyNP/whyNP.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// whyNP.js
// https://gist.github.com/mmocny/77a9e748edf95232df2e555cb494af2d
// Based on work from Noam Rosenthal

import processLoAFEntry from "./processLoAFEntry.js";
import onNewLoAFInteraction from "./onNewLoAFInteraction.js";

let loafs = [];
let events = [];

// Algorithm:
// - Walk both sorted lists: (loafs and events)
// - For each event
function groupInteractionEventsByLoAF() {
let i = 0;

const framesData = loafs.map((loaf, j) => {
const frameData = {
frameNum: j,
blockingDuration: loaf.blockingDuration,
loaf,
scripts: loaf.scripts,
events: []
};

for (; i < events.length; i++) {
const event = events[i];
const eventEndTime = event.startTime + event.duration;

// console.log('event', i, event);

// This event is obviously from a previous frame (or isn't long and doesn't need Next Paint)
if (eventEndTime < loaf.startTime) continue;

// This event is obviously for a future frame
if (event.processingStart > loaf.endTime) break;

if (loaf.startTime <= event.processingStart) {
// This event is guarenteed to overlap LoAF
frameData.events.push(event);
} else {
// Even if it isn't guarenteed-- there will not be a better fit.
// This is the first LoAF entry to follow an event timing entry...
// It is possible there was a BeginMainFrame that went unreported and we are blind to it.
// To work around that, we would need to force LoAF to report after long Event Timings.
frameData.events.push(event);
}
}

return frameData;
});

const longFramesWithInteractions = framesData.filter(
(fd) => /* fd.blockingDuration && */ fd.events.some((entry) => entry.interactionId > 0)
);

return longFramesWithInteractions;
}

let previousLoggedFrame = -1;

function logIfInteresting() {
const results = groupInteractionEventsByLoAF();

if (results.length === 0) return;

const newestFrame = results.at(-1);

if (newestFrame.frameNum === previousLoggedFrame) return;

// TODO: Right now we have to wait for all events and loaf to arrive...
// Perhaps one strategy is to wait for the next frame, or, log every time a frame change
// Or use a timeout, or use rAF + rIC to assume things are logged.

onNewLoAFInteraction(newestFrame.loaf, newestFrame.events);

previousLoggedFrame = newestFrame.frameNum;
}

new PerformanceObserver((entries) => {
loafs.push(...entries.getEntries().map(processLoAFEntry));
logIfInteresting();
}).observe({
type: "long-animation-frame",
buffered: true
});

new PerformanceObserver((entries) => {
const interactionEntries = entries
.getEntries()
// .filter((entry) => entry.interactonId > 0);

if (interactionEntries.length === 0) return;

events.push(...interactionEntries);
logIfInteresting();
}).observe({
type: "event",
buffered: true,
durationThreshold: 0
});

0 comments on commit cb1d6fe

Please sign in to comment.