-
Star
(128)
You must be signed in to star a gist -
Fork
(20)
You must be signed in to fork a gist
-
-
Save paullewis/55efe5d6f05434a96c36 to your computer and use it in GitHub Desktop.
| /*! | |
| * Copyright 2015 Google Inc. All rights reserved. | |
| * | |
| * Licensed under the Apache License, Version 2.0 (the "License"); | |
| * you may not use this file except in compliance with the License. | |
| * You may obtain a copy of the License at | |
| * | |
| * http://www.apache.org/licenses/LICENSE-2.0 | |
| * | |
| * Unless required by applicable law or agreed to in writing, software | |
| * distributed under the License is distributed on an "AS IS" BASIS, | |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express | |
| * or implied. See the License for the specific language governing | |
| * permissions and limitations under the License. | |
| */ | |
| /* | |
| * @see https://developers.google.com/web/updates/2015/08/using-requestidlecallback | |
| */ | |
| window.requestIdleCallback = window.requestIdleCallback || | |
| function (cb) { | |
| return setTimeout(function () { | |
| var start = Date.now(); | |
| cb({ | |
| didTimeout: false, | |
| timeRemaining: function () { | |
| return Math.max(0, 50 - (Date.now() - start)); | |
| } | |
| }); | |
| }, 1); | |
| } | |
| window.cancelIdleCallback = window.cancelIdleCallback || | |
| function (id) { | |
| clearTimeout(id); | |
| } |
would it be better to try to use requestAnimationFrame(..) for the scheduling if available?
@piuccio I suspect you're right. At least that way there's some kind of deadline to work with. Though, as I think on it, if you call the shim back-to-back you're still going to be inside the same task loop / frame so it's probably as bad. That said, it's definitely closer to spec, so I updated it.
@getify possibly, although rAF may well be just as badly timed here as setTimeout. Let's say you do a rIC callback using rAF as the shim. Now you get rAF firing and you maybe run for 50ms (or longer) and you blow the frame budget. This, I guess, is why rIC exists at all. Us devs don't get enough info to make the decision as to the when, so we have to rely on the UA to tell us "now is a good time". Another thought here is that one should avoid DOM mutation in a rIC callback (fragments are a-ok) since DOM manipulation is far less deterministic than other ops, and you can end up forcing a sync layout and so on. Generally there's just no good way to genuinely shim rIC, so while I don't like setTimeout, all the other options are as ick.
@paullewis Would it be possible for the timeRemaining method to dynamically compute a (somewhat useful) value? Something like:
window.requestIdleCallback = window.requestIdleCallback ||
function (cb) {
var start = Date.now(); // ADDED
return setTimeout(function () {
cb({
didTimeout: false,
timeRemaining: function () {
return 50 - (Date.now() - start); // ADDED
}
});
}, 1);
}It seems to work relatively well:
Note: The first number (20) is the timer id, so the while loop runs 6 times and then correctly ends after about 50 ms have passed.
@simevidas Great idea. Incorporated it, though I made sure it never goes < 0.
Should this be using performance.now() instead of Date.now? :)
@igrigorik Only supported as of iOS 9, which is just a touch too soon for my tastes on a shim. Bit of a shame, really.
As this article says requestIdleCallback accept timeout option.
@getify rAF might still be better if the work to be done in the rIC happens to fit within a frame. Maybe using raf-timeout (an idea I'm experimenting with) with this rIC shim would help?
@paullewis I stumbled on requestIdleCallback because I'm working on a 3D engine at infamous.io. I'd like to be able to determine if there is time left in an rAF, and thought maybe rIC could be a way to put off some less-important rendering tasks. For example, maybe there's a primary animation, and a secondary animation, where the secondary animation can run at a slower fps if necessary, in order for the primary animation to be at 60 fps. If we call requestIdleCallback(..., {timeout:0}), will 0 for the timeout guarantee that the rIC fires after the next rAF, or can it skip an rAF even if the timeout is 0? This would be happening while the user is interacting; I'd still want to execute the logic after each frame, but I'd like to know if there's time left in the frame and rAF doesn't seem to give us that info.
Basically what I'm looking for is something like if (timeRemainingInRAF > 0) doMoreStuff(), or in english "if there's time remaining within the 16ms window of a 60fps loop...", and it seems like rIC is the only way to achieve this (although calculations in the rIC would be for the following rAF, which is fine). If there's some way to guarantee that we can fire an rIC with every rAF, then that would probably serve the purpose.
What exactly is deadline.timeRemaining? Is that the time remaining until the next vsync? The next rAF? The next what? The 50ms max timeRemaining hints that rIC is not necessarily the timeRemaining before the next rAF, which might mean rIC won't help me out.
The requestAnimationFrame API seems like it leaves me blind: I don't know how much time I have, therefore I don't know if my rendering calculations are taking too long, and I don't know how long they should take. The only thing I know is that the time I have for my rendering calculations is less than 16ms because the browser has to do it's own rendering stuff within those 16ms too.
@paullewis Shouldn't start be set when the setTimeout callback runs rather than when it is scheduled?
window.requestIdleCallback = window.requestIdleCallback ||
function (cb) {
- var start = Date.now();
return setTimeout(function () {
+ var start = Date.now();
cb({
didTimeout: false,
timeRemaining: function () {
return Math.max(0, 50 - (Date.now() - start));
}
});
}, 1);See also wikimedia/mediawiki@55fc2a9#mediawiki.requestIdleCallback.js.
@Krinkle it your version Date.now() inside cb will be the same as start.
Whereas here you have access to the start time before any idle callbacks were executed.
Basically in original version you can kind of limit idle period
whereas in your version you can only check execution time of individual idle callbacks:
https://developers.google.com/web/updates/2015/08/using-requestidlecallback
Alirght... seems that using requestAnimationFrame we're able to do a little better, because we can actually simulate an idle cycle and then cap the amount of time we do work in it. With this approximate polyfill I threw together, the framerate of a fairly complex OpenGL animation doesn't drop at all with a splittable task running:
// Internal state
globalThis.__idleRequestCallbacks = [];
globalThis.__runnableIdleCallbacks = [];
globalThis.__idleCallbackId = 0;
globalThis.__idleCallbackMap = new Map();
globalThis.__idleRafScheduled = false;
// IdleDeadline constructor
function IdleDeadline(deadlineTime, didTimeout) {
this.__deadlineTime = deadlineTime;
this.__didTimeout = didTimeout;
}
IdleDeadline.prototype.timeRemaining = function() {
var remaining = this.__deadlineTime - performance.now();
return remaining > 0 ? remaining : 0;
};
Object.defineProperty(IdleDeadline.prototype, 'didTimeout', {
get: function() { return this.__didTimeout; },
});
function scheduleNextIdle() {
if (globalThis.__idleRafScheduled) return;
globalThis.__idleRafScheduled = true;
requestAnimationFrame(() => {
setTimeout(startIdlePeriod, 0);
});
}
// Start an idle period
function startIdlePeriod() {
// console.log("before animaiton frame")
// Move pending to runnable
if (globalThis.__idleRequestCallbacks.length) {
globalThis.__runnableIdleCallbacks.push(...globalThis.__idleRequestCallbacks);
globalThis.__idleRequestCallbacks.length = 0;
}
globalThis.__idleRafScheduled = false; // reset flag
// If no runnable callbacks or already scheduled, exit
if (!globalThis.__runnableIdleCallbacks.length) return;
var deadlineTime = performance.now() + 8; // 8 does not drop framerate on most modern systems
while (globalThis.__runnableIdleCallbacks.length) {
var handle = globalThis.__runnableIdleCallbacks.shift();
var cb = globalThis.__idleCallbackMap.get(handle);
if (!cb) continue; // canceled
// Cancel this, so we no longer call it on timeout
globalThis.__idleCallbackMap.delete(handle);
var deadline = new IdleDeadline(deadlineTime, false);
try { cb(deadline); } catch (e) { setTimeout(() => { throw e; }, 0); }
if (performance.now() >= deadlineTime) break; // yield mid-frame
}
// Reschedule if any callbacks remain
if (globalThis.__runnableIdleCallbacks.length) {
scheduleNextIdle();
}
}
function requestIdleCallback(callback, options) {
var handle = ++globalThis.__idleCallbackId;
globalThis.__idleCallbackMap.set(handle, callback);
globalThis.__idleRequestCallbacks.push(handle);
if (options && options.timeout && options.timeout > 0) {
// FIXME: Spec says that the timeout calling must sort by currentTime +
// options.timeout, however maintainng such a queue would be very dedious
setTimeout(function timeoutCallback() {
var cb = globalThis.__idleCallbackMap.get(handle);
if (!cb) return;
var i = globalThis.__idleRequestCallbacks.indexOf(handle);
if (i > -1) globalThis.__idleRequestCallbacks.splice(i, 1);
i = globalThis.__runnableIdleCallbacks.indexOf(handle);
if (i > -1) globalThis.__runnableIdleCallbacks.splice(i, 1);
var deadline = new IdleDeadline(performance.now(), true);
try { cb(deadline); } catch (e) { setTimeout(() => { throw e; }, 0); }
}, options.timeout);
}
scheduleNextIdle();
return handle;
}
function cancelIdleCallback(handle) {
globalThis.__idleCallbackMap.delete(handle);
var i = globalThis.__idleRequestCallbacks.indexOf(handle);
if (i > -1) globalThis.__idleRequestCallbacks.splice(i, 1);
i = globalThis.__runnableIdleCallbacks.indexOf(handle);
if (i > -1) globalThis.__runnableIdleCallbacks.splice(i, 1);
}
I'm using an OpenGL animation generated by ChatGPT:
Animation
(function GPUStressDemo() {
'use strict';
// --- utility: create element with styles ---
function el(tag, style) {
const e = document.createElement(tag);
Object.assign(e.style, style || {});
return e;
}
// --- create canvas & UI (no HTML required) ---
const canvas = el('canvas', {
position: 'fixed',
inset: '0', // top/right/bottom/left 0
width: '100%',
height: '100%',
zIndex: 2147483647, // topmost (very high)
display: 'block',
cursor: 'default',
pointerEvents: 'none', // so it doesn't block interaction
background: 'black',
});
document.body.appendChild(canvas);
const ui = el('div', {
position: 'fixed',
top: '8px',
left: '8px',
zIndex: 2147483647,
padding: '6px 10px',
borderRadius: '6px',
background: 'rgba(0,0,0,0.6)',
color: '#0f0',
fontFamily: 'monospace',
fontSize: '13px',
lineHeight: '1.2',
pointerEvents: 'auto', // allow button clicks
});
document.body.appendChild(ui);
const fpsEl = el('div');
const infoEl = el('div', { marginTop: '6px', color: '#9f9' });
const stopBtn = el('button', {
marginTop: '6px',
padding: '4px 8px',
cursor: 'pointer',
borderRadius: '4px',
border: '1px solid rgba(255,255,255,0.12)',
background: 'rgba(255,255,255,0.02)',
color: '#fff',
fontSize: '12px'
});
stopBtn.textContent = 'Stop Demo';
ui.appendChild(fpsEl);
ui.appendChild(infoEl);
ui.appendChild(stopBtn);
// --- WebGL setup ---
const gl = canvas.getContext('webgl', { antialias: false, preserveDrawingBuffer: false }) ||
canvas.getContext('experimental-webgl', { antialias: false });
if (!gl) {
fpsEl.textContent = 'WebGL not available in this browser.';
return;
}
// Vertex shader (simple full-screen triangle)
const vertSrc = `
attribute vec2 a_position;
varying vec2 v_uv;
void main() {
v_uv = a_position * 0.5 + 0.5;
gl_Position = vec4(a_position, 0.0, 1.0);
}
`;
// Fragment shader: heavy per-pixel iterative computation (GPU-hungry)
const fragSrc = `
precision highp float;
varying vec2 v_uv;
uniform vec2 u_resolution;
uniform float u_time;
// A tiny hash for variety
float hashf(float n){ return fract(sin(n)*43758.5453123); }
void main(){
vec2 uv = (gl_FragCoord.xy / u_resolution.xy) * 2.0 - 1.0;
uv.x *= u_resolution.x / u_resolution.y;
// center + zoom animation
float t = u_time * 0.35;
vec2 z = uv * 1.25;
vec3 col = vec3(0.0);
// Heavy iterative loop: 200 iterations (constant loop, expensive ops)
// This is intentionally costly to expose GPU lag.
for(int k = 0; k < 200; k++) {
float kk = float(k) + 0.5;
float a = kk * 0.141592 + t * 0.15;
// complex transform mixing trig and multiplication
vec2 z2 = vec2(
z.x*z.x - z.y*z.y,
2.0*z.x*z.y
);
// twist with sin/cos and an evolving offset
z = 0.95 * z2 + 0.3 * vec2(sin(a + z2.x*0.7), cos(a*0.87 + z2.y*0.9));
// energy-like accumulator: uses length and multiple trig calls
float m = length(z) + 0.0001;
float s = 1.0 / (0.0005 + m*m*0.7);
col += s * vec3( sin(kk*0.123 + t*0.4),
cos(kk*0.117 + t*0.23),
sin(kk*0.197 - t*0.31) );
}
// tone map, saturation
col = col * 0.01;
col = pow(clamp(col, 0.0, 1.0), vec3(0.9));
gl_FragColor = vec4(col, 1.0);
}
`;
function compileShader(source, type) {
const s = gl.createShader(type);
gl.shaderSource(s, source);
gl.compileShader(s);
if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) {
const msg = gl.getShaderInfoLog(s);
gl.deleteShader(s);
throw new Error('Shader compile error: ' + msg);
}
return s;
}
function createProgram(vsSrc, fsSrc) {
const vs = compileShader(vsSrc, gl.VERTEX_SHADER);
const fs = compileShader(fsSrc, gl.FRAGMENT_SHADER);
const prog = gl.createProgram();
gl.attachShader(prog, vs);
gl.attachShader(prog, fs);
gl.linkProgram(prog);
if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) {
const msg = gl.getProgramInfoLog(prog);
gl.deleteProgram(prog);
throw new Error('Program link error: ' + msg);
}
gl.deleteShader(vs);
gl.deleteShader(fs);
return prog;
}
const program = createProgram(vertSrc, fragSrc);
gl.useProgram(program);
// a_position buffer for a fullscreen triangle (two triangles via strip)
const quad = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, quad);
// full-screen triangle strip coords
const vertices = new Float32Array([
-1, -1,
1, -1,
-1, 1,
1, 1
]);
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
const aPos = gl.getAttribLocation(program, 'a_position');
gl.enableVertexAttribArray(aPos);
gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 0, 0);
const uResolution = gl.getUniformLocation(program, 'u_resolution');
const uTime = gl.getUniformLocation(program, 'u_time');
// Resize handling with devicePixelRatio cap to avoid blindingly huge canvases
function resize() {
const dpr = Math.min(window.devicePixelRatio || 1, 2.0); // cap DPR at 2 for sanity
const width = Math.max(1, Math.round(window.innerWidth * dpr));
const height = Math.max(1, Math.round(window.innerHeight * dpr));
if (canvas.width !== width || canvas.height !== height) {
canvas.width = width;
canvas.height = height;
canvas.style.width = window.innerWidth + 'px';
canvas.style.height = window.innerHeight + 'px';
gl.viewport(0, 0, width, height);
}
}
window.addEventListener('resize', resize, { passive: true });
resize();
// --- animation loop with FPS tracking ---
let running = true;
let last = performance.now();
let fps = 0;
let frames = 0;
let acc = 0;
let startTime = performance.now();
function updateFPS(delta) {
frames++;
acc += delta;
if (acc >= 250) { // update 4x/sec for a responsive readout
fps = Math.round((frames / acc) * 1000);
frames = 0;
acc = 0;
fpsEl.textContent = `FPS: ${fps}`;
infoEl.textContent = `Resolution: ${canvas.width}×${canvas.height} (dpr capped)`;
}
}
function frame(now) {
if (!running) return;
const dt = now - last;
last = now;
updateFPS(dt);
// Send uniforms
gl.uniform2f(uResolution, canvas.width, canvas.height);
gl.uniform1f(uTime, (now - startTime) * 0.001);
// Render full-screen quad (triangle strip of 4 verts)
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
requestAnimationFrame(frame);
}
// --- controls ---
stopBtn.addEventListener('click', () => stop(), { once: true });
function stop() {
running = false;
// remove elements and event listeners
window.removeEventListener('resize', resize);
try { gl.getExtension('WEBGL_lose_context')?.loseContext(); } catch (e) {}
if (canvas.parentNode) canvas.parentNode.removeChild(canvas);
if (ui.parentNode) ui.parentNode.removeChild(ui);
}
// Start loop
requestAnimationFrame((t) => {
last = t;
startTime = t;
requestAnimationFrame(frame);
});
// Expose stop to window for convenience
window.__gpuStressStop = stop;
// Friendly message to console
console.log('GPU stress demo started. Call window.__gpuStressStop() to stop.');
// return stop in case someone wants to capture it
return { stop };
})();
And this code for the computation running on top of it:
const N = 8000;
let i = 1;
let j = 1;
let total = 0;
function work(deadline) {
while (i <= N) {
total += i * j;
j++;
if (j > i) {
i++;
j = 1;
}
if (deadline.timeRemaining() < 1) {
requestIdleCallback(work);
return;
}
}
console.log("Done:", total);
}
requestIdleCallback(work);

Shouldn't
timeRemainingreturn something close to 50? See question Is there maximum value that timeRemaining() will return? Yes, it’s currently 50ms.