-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
311 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
}); | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
}); | ||
})(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
}); |