13

I have a little project that I'm working on that consumes the twitter streaming API and makes a little canvas animation from it. Given that the twitter streaming API doesn't conclude, the animation could go on indefinitely.

Therein lies the problem. requestAnimationFrame appears to operate through recursion, and we don't get proper tail calls until ES6, meaning that I think this grows the call stack for every frame.

The question is, am I right that this'll eventually raise an error for exceeding the maximum call stack size, or does the browser play a trick to avoid the limit? Is requestAnimationFrame really doing something strange that I don't understand (perhaps along the lines of a setInterval not being recursive)?

In chrome 36.0.1985.32 beta (which has a call stack size of 20834), I am testing with:

function test(t) {
    requestAnimationFrame(test);
}

test(performance.now());

And have seen no issues. I would expect an RangeError to be thrown ~6 minutes assuming 60fps.

Another misleading information is shown in the Call Stack section of the chrome developer tools window, where it is shown the requestAnimationFrame Call stack as it would fill up the stack, as show in the following image:

enter image description here

Zac
  • 4,510
  • 3
  • 36
  • 44
qubyte
  • 17,558
  • 3
  • 30
  • 32
  • 1
    Did you start by reading the [**documentation**](https://developer.mozilla.org/en-US/docs/Web/API/window.requestAnimationFrame), it explains this rather well. – adeneo Jun 01 '14 at 11:27
  • 6
    The documentation doesn't clearly state an answer to the above question. – qubyte Jun 01 '14 at 11:29
  • The closest it gets (as far as I can see) is `This will request that your animation function be called before the browser performs the next repaint`, which implies that it's non-recursive. I'd like something more definitive than that. – qubyte Jun 01 '14 at 11:31
  • Yup. Was being stupid. It's not recursing synchronously. – qubyte Jun 01 '14 at 11:36
  • Not sure what you mean with "non-recursive", the function itself is of course non-recursive, but placing it inside a function that is called recursively makes it recursive, and it calls the callback approximately 60 times per second, depending on browsers, and it's all in the documentation. As it's async and calls the callback before the next repaint, there will be no call stack errors. – adeneo Jun 01 '14 at 11:36
  • `test` calls `requestAnimationFrame` calls `test` calls `requestAnimationFrame`... Looks pretty recursive to me. In any case, as I mentioned above, I was being dumb. This is not recursing synchronously so of course the call stack isn't an issue. – qubyte Jun 01 '14 at 11:38
  • The posted code where the function calls itself over and over is of course recursive, that should be rather obvious, but `requestAnimationFrame` is not really recursive, it only calls the callback once and that's it, and when you're reading the documentation and claim that "which implies that it's non-recursive", you are of course right, `requestAnimationFrame` is not recursive, the `test()` function however is. – adeneo Jun 01 '14 at 11:41

3 Answers3

6

RAF will launch the function "on the next drawn frame". That's means that it will be executed in another stack of action, and you won't have any maximum call stack error.

ArnaudMolo
  • 76
  • 2
  • Argh. What a brain fart. Yeah, async functions of any kind don't have limits because each function is in a different event loop iteration... – qubyte Jun 01 '14 at 11:35
2

Yes, requestAnimationFrame() is asynchronously recursive, and that prevents any actual stack overflow. But don't forget that the stack still unwinds at the end. There is no issue if you're running a single animation. But if you are running a series of animations in sequence, you might do something like this:

function animateFirst(timeStamp) {
    let r = functionReturnValue();
    if (r == "complete") {
        frame = requestAnimationFrame(animateNext);
        return; // this is necessary
    }
    frame = requestAnimationFrame(animateFirst);
}

Or you must structure it this way:

function animateFirst(timeStamp) {
    let r = functionReturnValue();
    if (r == "complete") {
        frame = requestAnimationFrame(animateNext);
    }
    else {
        frame = requestAnimationFrame(animateFirst);
    }
}

These examples are oversimplifications. In an actual animation function there might be more complex logic. The point is that if you leave out the return statement in the first example, animateFirst() will run again after animateNext() has completed and unwound its async stack. Depending on your code, it might run once, or it might start a whole new animation loop.

Sideways S
  • 601
  • 7
  • 12
0

If requestAnimationFrame calls test, then indeed there will be an infinite call stack. It is clear that test do call requestAnimationFrame. One needs to verify whether requestAnimationFrame calls test.

The following code will find out:

function testSnitch(t) {
    var caller = arguments.callee.caller || 'NULL';
    console.log(caller.toString());
    requestAnimationFrame(test);
}

testSnitch(performance.now());
zjk
  • 2,043
  • 3
  • 28
  • 45