Skip to content

Commit

Permalink
Move snippets, live-expressions, lib, around and start fps-meter utility
Browse files Browse the repository at this point in the history
  • Loading branch information
mmocny committed Dec 15, 2023
1 parent 8b7d34b commit 594bf86
Show file tree
Hide file tree
Showing 26 changed files with 480 additions and 17 deletions.
43 changes: 43 additions & 0 deletions fps-meter/FpsTracker.js
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';
}
};


31 changes: 31 additions & 0 deletions fps-meter/bak/AnimationFrameIterator.js
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 });
}
43 changes: 43 additions & 0 deletions fps-meter/bak/CanvasFps.js
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);
}
}
42 changes: 42 additions & 0 deletions fps-meter/bak/CanvasWorker.mjs
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();
}
188 changes: 188 additions & 0 deletions fps-meter/bak/main.mjs
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;
18 changes: 18 additions & 0 deletions fps-meter/index.js
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;
15 changes: 15 additions & 0 deletions fps-meter/package.json
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"
}
}
Loading

0 comments on commit 594bf86

Please sign in to comment.