-
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.
Move snippets, live-expressions, lib, around and start fps-meter utility
- Loading branch information
Showing
26 changed files
with
480 additions
and
17 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,43 @@ | ||
import { raf } from '../lib/raf.js'; | ||
|
||
export class FpsTracker extends EventTarget { | ||
constructor(dur = 1000) { | ||
super(); | ||
this._dur = dur; | ||
this._frameTimes = []; | ||
// TODO: Allow replacing the raf loop with a stream that is given | ||
// or something to allow worker raf polling | ||
this.start(); | ||
} | ||
|
||
async start() { | ||
for (; ;) { | ||
const frameTime = await raf(); | ||
const fps = this.reportNewFrame(frameTime); | ||
// TODO: Experiment with observables | ||
this.dispatchEvent(new CustomEvent('fps', { detail: { fps } })); | ||
} | ||
} | ||
|
||
reportNewFrame(frameTime) { | ||
this._frameTimes.push(frameTime); | ||
return this.computeFps(frameTime); | ||
} | ||
|
||
// Separate this | ||
computeFps(ts = performance.now()) { | ||
this._frameTimes = this._frameTimes.filter(t => (ts - t) <= this._dur); | ||
return (this._frameTimes.length / this._dur) * 1000; | ||
|
||
} | ||
|
||
static fpsToColor(fps) { | ||
const percentDropped = 1 - (fps / 60); | ||
if (percentDropped > 3 / 4) return 'red'; | ||
if (percentDropped > 2 / 4) return 'orange'; | ||
if (percentDropped > 1 / 4) return 'yellow'; | ||
return '#73AD21'; | ||
} | ||
}; | ||
|
||
|
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,31 @@ | ||
export async function raf() { | ||
return new Promise(resolve => { | ||
requestAnimationFrame(resolve); | ||
}); | ||
} | ||
|
||
export async function* ThreadLocalRAFIterator() { | ||
for (;;) { | ||
yield raf(); | ||
} | ||
} | ||
|
||
// TODO: What is the right way to create channel? | ||
export async function* PostMessageRAFIterator() { | ||
for (;;) { | ||
yield new Promise(resolve => { | ||
const handler = (e) => { | ||
switch (e.data.msg) { | ||
case 'raf': | ||
resolve(e.data.frameTime); | ||
self.removeEventListener('message', handler); | ||
break; | ||
} | ||
}; | ||
self.addEventListener('message', handler); // TODO: investigate using { once: true } | ||
}); | ||
} | ||
} | ||
export function SendPostMessageRAF(worker, frameTime) { | ||
worker.postMessage({ msg: 'raf', frameTime }); | ||
} |
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,43 @@ | ||
import { ThreadLocalRAFIterator } from './AnimationFrameIterator.js'; | ||
|
||
export class CanvasFps { | ||
constructor(ctx, fpsTracker) { | ||
this.ctx = ctx; | ||
this.fpsTracker = fpsTracker; | ||
|
||
this._x = ctx.canvas.width / 2; | ||
this._y = ctx.canvas.height / 2; | ||
this._r = Math.min(this._x, this._y); | ||
} | ||
|
||
/* | ||
* Note: Drawing uses a local rAF loop to run the canvas paints, but it doesnt use the | ||
* rAF timing to visualize results. Those come from fpsTracker which may or may not be | ||
* using a local rAF loop to count. | ||
*/ | ||
async startDrawing() { | ||
for await (let frameTime of ThreadLocalRAFIterator()) { | ||
let fps = this.fpsTracker.mostRecentFps.toFixed(1); | ||
|
||
this.ctx.clearRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height); | ||
this.drawCircle(this.fpsTracker.getColor()); | ||
this.drawText(fps); | ||
|
||
} | ||
} | ||
|
||
drawCircle(color) { | ||
this.ctx.beginPath(); | ||
this.ctx.fillStyle = color; | ||
this.ctx.arc(this._x, this._y, this._r, 0, 2 * Math.PI, false); | ||
this.ctx.fill(); | ||
} | ||
|
||
drawText(text) { | ||
this.ctx.fillStyle = "#000"; | ||
this.ctx.font = `${this._r * 0.75}px Roboto`; | ||
this.ctx.textAlign = 'center'; | ||
this.ctx.textBaseline = 'middle'; | ||
this.ctx.fillText(text, this._x, this.ctx.canvas.height / 2); | ||
} | ||
} |
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,42 @@ | ||
import { FpsTracker } from './FpsTracker.mjs'; | ||
import { CanvasFps } from './CanvasFps.mjs'; | ||
import { ThreadLocalRAFIterator, PostMessageRAFIterator } from './AnimationFrameIterator.mjs'; | ||
|
||
// As per: https://developers.google.com/web/updates/2018/08/offscreen-canvas | ||
|
||
addEventListener('message', e => { | ||
switch (e.data.msg) { | ||
case 'start': | ||
watchPostMessageFps(); | ||
watchThreadLocalFps(); | ||
startCanvasFps( | ||
e.data.canvas_wrkr_raf_main.getContext('2d'), | ||
e.data.canvas_wrkr_raf_wrkr.getContext('2d') | ||
); | ||
removeEventListener('messasge', this); | ||
break; | ||
} | ||
}); | ||
|
||
const mainFpsTracker = new FpsTracker(5000); | ||
const wrkrFpsTracker = new FpsTracker(5000); | ||
|
||
async function watchPostMessageFps() { | ||
for await (let frameTime of PostMessageRAFIterator()) { | ||
mainFpsTracker.reportNewFrame(frameTime); | ||
} | ||
} | ||
|
||
async function watchThreadLocalFps(ctx1, ctx2) { | ||
for await (let frameTime of ThreadLocalRAFIterator()) { | ||
mainFpsTracker.updateForTimestamp(performance.now()); | ||
wrkrFpsTracker.reportNewFrame(frameTime); | ||
} | ||
} | ||
|
||
function startCanvasFps(ctx_main, ctx_wrkr) { | ||
const c_main = new CanvasFps(ctx_main, mainFpsTracker); | ||
const c_wrkr = new CanvasFps(ctx_wrkr, wrkrFpsTracker); | ||
c_main.startDrawing(); | ||
c_wrkr.startDrawing(); | ||
} |
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,188 @@ | ||
import { FpsTracker } from '../FpsTracker.mjs'; | ||
import { CanvasFps } from '../CanvasFps.mjs'; | ||
import { ThreadLocalRAFIterator, SendPostMessageRAF } from '../AnimationFrameIterator.js'; | ||
|
||
|
||
// Set up OffscreenCanvas WebWorker | ||
const canvas_wrkr_raf_main = document.querySelector('#canvas_wrkr_raf_main').transferControlToOffscreen(); | ||
const canvas_wrkr_raf_wrkr = document.querySelector('#canvas_wrkr_raf_wrkr').transferControlToOffscreen(); | ||
const worker = new Worker('./CanvasWorker.mjs', { type: 'module' }); | ||
|
||
worker.postMessage({ | ||
msg: 'start', | ||
canvas_wrkr_raf_main, | ||
canvas_wrkr_raf_wrkr | ||
}, [canvas_wrkr_raf_main, canvas_wrkr_raf_wrkr]); | ||
|
||
|
||
// Start tracking frames on main, reporting frame times to worker | ||
(async () => { | ||
const fpsTracker = new FpsTracker(5000); | ||
const canvas_main_raf_main = document.querySelector('#canvas_main_raf_main'); | ||
const c = new CanvasFps(canvas_main_raf_main.getContext('2d'), fpsTracker); | ||
c.startDrawing(); | ||
|
||
for await (let frameTime of ThreadLocalRAFIterator()) { | ||
let fps = fpsTracker.reportNewFrame(frameTime); | ||
SendPostMessageRAF(worker, frameTime); | ||
} | ||
})(); | ||
|
||
// Start adding Long Tasks on main | ||
(async function () { | ||
function blog(text) { | ||
const el = document.getElementById('long_task_tracker'); | ||
el.textContent = text; | ||
} | ||
|
||
function block(block_ms) { | ||
blog(`blocked for ${block_ms.toFixed(0)}ms`); | ||
|
||
let now = performance.now(); | ||
let end = now + block_ms; | ||
while (now < end) { | ||
now = performance.now(); | ||
} | ||
} | ||
|
||
// Will delay at least one single animation frame | ||
async function block_with_delay(block_ms, delay_ms = 0) { | ||
const then = performance.now() + delay_ms; | ||
|
||
for await (let time of ThreadLocalRAFIterator()) { | ||
// blog(`will block for ${block_ms.toFixed(0)}ms, in ${(then-time).toFixed(0)}ms`); | ||
|
||
if (time >= then) break; | ||
} | ||
|
||
block(block_ms); | ||
} | ||
|
||
let interval; | ||
function toggleLongTasks() { | ||
if (interval) { | ||
interval = clearInterval(interval); | ||
} else { | ||
// setInterval default (min) interval is 4ms | ||
interval = setInterval(() => { | ||
//block((1000/90)/2); | ||
block(100); | ||
// block_with_delay((1000/90)/2); | ||
}); | ||
} | ||
} | ||
|
||
// await block_with_delay(5000, 5000); | ||
toggleLongTasks(); | ||
|
||
document.getElementById('t').addEventListener('keydown', (evt) => { | ||
block(100); | ||
}); | ||
})(); | ||
|
||
|
||
// https://dbaron.org/log/20100309-faster-timeouts | ||
// Only add setZeroTimeout to the window object, and hide everything | ||
// else in a closure. | ||
(function () { | ||
var timeouts = []; | ||
var messageName = "zero-timeout-message"; | ||
|
||
// Like setTimeout, but only takes a function argument. There's | ||
// no time argument (always zero) and no arguments (you have to | ||
// use a closure). | ||
function setZeroTimeout(fn) { | ||
timeouts.push(fn); | ||
window.postMessage(messageName, "*"); | ||
} | ||
|
||
function handleMessage(event) { | ||
if (event.source == window && event.data == messageName) { | ||
event.stopPropagation(); | ||
if (timeouts.length > 0) { | ||
var fn = timeouts.shift(); | ||
fn(); | ||
} | ||
} | ||
} | ||
|
||
window.addEventListener("message", handleMessage, true); | ||
|
||
// Add the one thing we want added to the window object. | ||
window.setZeroTimeout = setZeroTimeout; | ||
})(); | ||
|
||
export async function reportTimeToNextFrame() { | ||
let start = performance.now(); | ||
let id = `MyFrame-${(Math.random() * 100000).toFixed(0)}`; | ||
|
||
let p = document.createElement('div'); | ||
p.id = id; | ||
p.innerText = '.'; | ||
p.setAttribute('elementtiming', id); | ||
document.body.appendChild(p); | ||
|
||
let viaElementTiming = new Promise(resolve => { | ||
// TODO, wrap this in a promise | ||
const observer = new PerformanceObserver(entryList => { | ||
for (const entry of entryList.getEntries()) { | ||
if (entry.identifier != id) continue; | ||
|
||
let duration = entry.renderTime - start; | ||
|
||
console.log('via Element Timing', duration, start, entry); | ||
|
||
resolve(duration); | ||
} | ||
observer.disconnect(); | ||
}); | ||
observer.observe({ type: 'element' }); | ||
}); | ||
|
||
let viaSingleRaf = new Promise(resolve => { | ||
requestAnimationFrame((t1) => { | ||
let duration = t1 - start; | ||
|
||
console.log('via Single rAF', duration, t1); | ||
|
||
resolve(duration); | ||
}) | ||
}); | ||
|
||
let viaDoubleRaf = new Promise(resolve => { | ||
requestAnimationFrame((t1) => { | ||
requestAnimationFrame((t2) => { | ||
let duration = t2 - start; | ||
|
||
console.log('via Double rAF', duration, t1, t2); | ||
|
||
resolve(duration); | ||
}) | ||
}) | ||
}); | ||
|
||
let viaRafNTask = new Promise(resolve => { | ||
requestAnimationFrame((t1) => { | ||
// TODO: try out different types of task scheduling methods to get higher priority task | ||
// See: https://github.com/WICG/scheduling-apis/blob/main/explainers/prioritized-post-task.md | ||
// setZeroTimeout(() => { | ||
setTimeout(() => { | ||
let t2 = performance.now(); | ||
let duration = t2 - start; | ||
|
||
console.log('via rAF n Task', duration, t1, t2); | ||
|
||
resolve(duration); | ||
}, 0); | ||
}) | ||
}); | ||
|
||
return Promise.all([ | ||
viaElementTiming, | ||
viaSingleRaf, | ||
viaDoubleRaf, | ||
viaRafNTask, | ||
]); | ||
} | ||
|
||
window.reportTimeToNextFrame = reportTimeToNextFrame; |
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,18 @@ | ||
|
||
// import workerize from 'workerize'; | ||
// function withinWorker() { | ||
// return Math.random(); | ||
// } | ||
// const fn = workerize(withinWorker); | ||
|
||
import { FpsTracker } from "./FpsTracker"; | ||
|
||
export async function fpsMeter() { | ||
const fpsTracker = new FpsTracker; | ||
fpsTracker.addEventListener('fps', (e) => { | ||
console.log('fps', e.detail.fps); | ||
}); | ||
|
||
} | ||
|
||
export default fpsMeter; |
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 @@ | ||
{ | ||
"name": "fps-meter", | ||
"version": "1.0.0", | ||
"description": "", | ||
"main": "index.js", | ||
"scripts": { | ||
"test": "echo \"Error: no test specified\" && exit 1" | ||
}, | ||
"keywords": [], | ||
"author": "", | ||
"license": "ISC", | ||
"dependencies": { | ||
"workerize": "^0.1.8" | ||
} | ||
} |
Oops, something went wrong.