Promises, async functions, and requestAnimationFrame, together.

# /#!/JoePea (9 years ago)

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?

# Salvador de la Puente González (9 years ago)

AFAIK, that should execute drawSomething() once per frame. Given a frame is each time the animatinFrame() promise resolves.

# Boris Zbarsky (9 years ago)

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...

# /#!/JoePea (9 years ago)

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.

# /#!/JoePea (9 years ago)

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

# Matthew Robb (9 years ago)

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
# /#!/JoePea (8 years ago)

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.