Promises, async functions, and requestAnimationFrame, together.
AFAIK, that should execute drawSomething()
once per frame. Given a frame
is each time the animatinFrame() promise resolves.
On 4/23/16 4:09 AM, Salvador de la Puente González wrote:
AFAIK, that should execute
drawSomething()
once per frame. Given a frame is each time the animatinFrame() promise resolves.
What's not obvious to me is whether it will execute it before the actual paint for the frame (i.e. before or right after the loop that's notifying the animation frame callbacks completes) or whether it will do drawSomething() after the paint...
Alright, I did an experiment, and I'm really surprised at the results! Apparently, the logic (what would be drawSomething() in my previous example) is fired within the frame!!
So, let me show you my original method for starting an animation loop. I'm working on a 3D project at infamous.io. The Scene class (infamous/infamous/blob/master/src/motor/Scene.js) has a method for starting an animation loop the standard way:
async _startAnimationLoopWhenMounted() {
this._animationLoopStarted = true
if (!this._mounted) await this.mountPromise
// So now we can render after the scene is mounted.
const loop = timestamp => {
this._inFrame = true
this._runRenderTasks(timestamp)
this._renderNodes(timestamp)
// If any tasks are left to run, continue the animation loop.
if (this._allRenderTasks.length)
this._rAF = requestAnimationFrame(loop)
else {
this._rAF = null
this._animationLoopStarted = false
}
this._inFrame = false
}
this._rAF = requestAnimationFrame(loop)
}
Here's what the Chrome timeline shows for the logic that is fired inside the loop: cloud.githubusercontent.com/assets/297678/14764236/8eb72d4a-0965-11e6-9bb9-5db02cc23520.png
Now, I went ahead and modified my Scene class so the method now looks like this:
function animationFrame() {
let resolve = null
const promise = new Promise(r => resolve = r)
window.requestAnimationFrame(resolve)
return promise
}
// ...
async _startAnimationLoopWhenMounted() {
this._animationLoopStarted = true
if (!this._mounted) await this.mountPromise
this._rAF = true
let timestamp = null
while (this._rAF) {
timestamp = await animationFrame()
this._inFrame = true
this._runRenderTasks(timestamp)
this._renderNodes(timestamp)
// If any tasks are left to run, continue the animation loop.
if (!this._allRenderTasks.length) {
this._rAF = null
this._animationLoopStarted = false
}
this._inFrame = false
}
}
And the timeline results are surprising! As you can see in the following screenshot, all of the logic happens within the frame (though you can see there's extra overhead from what I assume are the extra function calls due to the fact that I'm using Facebook Regenerator for the async functions): cloud.githubusercontent.com/assets/297678/14764237/8eb71ce2-0965-11e6-942a-3c556c48b9a0.png
Near the bottom right of the screen shot, you can see the tooltip as
I'm hovering on the call to animationFrame
which returns the promise
that I am awaiting in the loop.
Although this behavior seems to be exactly what I was hoping for, it seems like there is something wrong. Could there be a bug in regenerator that is failing to defer my loop code to a following tick? Or is my code deferred to a following tick that somehow the animation frame knows to execute within the same tick? Maybe there's something I'm missing about the Promise API that allows for .then() of a promise (which I assume is what Regenerator is using) to be executed in the same tick? What's going on here?
I was expecting to see the code of my loop execute outside of the "Animation Frame Fired" section.
Just to show a little more detail, here's a screenshot that shows that the logic of the while-loop version of my animation loop fires inside each animation frame. I've zoomed out and we can see there's nothing fired between the frames:
cloud.githubusercontent.com/assets/297678/14764323/c28e83cc-0968-11e6-8771-8e726158aa52.png
This is definitely interesting stuff. Have you considered rewriting this so that it only uses generators? If you did then you could test natively in Chrome and see if you get the same results.
- Matthew Robb
Ah, I think the reason that this animation loop using promises works is
because promise handlers resolve in the next microtask after the current
macrotask. I believe that the animation frame fires in what is essentially
a macrotask, then immediately after this macrotask the resolution of the
animationFrame()
promise happens in the following microtask. At some
point later, the browser renders stuff, which I think might the next
macrotask after the animation frame macrotask.
Is it possible?
I thought maybe something like this:
function animationFrame() { let resolve = null const promise = new Promise(r => resolve = r) window.requestAnimationFrame(resolve) return promise } async function game() { // the game loop while (true) { await animationFrame() drawSomething() } } game()
But, I'm not sure what to expect: does the code that follows the await statement execute in the frame, sometimes in the frame, or never in the frame? What might we expect from the various browsers?