RequestAnimationFrame speeds up/slows down periodically
Asked Answered
P

1

13

From my understanding, requestAnimationFrame should run as close as possible to the browser's frame rate, which is approximately 60fps. To ensure that this is indeed taking place, I have been logging timestamps for each requestAnimationFrame invocation like so:

function animate(now){
    console.log(now);
    window.requestAnimationFrame(animate);
}
window.requestAnimationFrame(animate);

Console.log data shows that invocations are consistently taking place approximately 0.016674 milliseconds apart, thus indicating that the frame rate is ≈ 60fps (59.9736116108912fps to be exact).

Console.logs (sample data):

Timestamp   FPS               (Current - previous) timestamp
------------------------------------------------------------
100.226     59.97361161       0.016674
116.9       59.97361161       0.016674
133.574     59.97361161       0.016674
   .             .               .
   .             .               .
150.248     59.97361161       0.016674
166.922     59.97361161       0.016674
183.596     59.97361161       0.016674
200.27      59.97361161       0.016674

Up to this point, requestAnimationFrame invocations are occurring at consistent time intervals, and no invocation lags behind/executes too fast.

However, modifying the contents of the animate() function to execute a relatively more complex function, results in requestAnimationFrame invocations which are not as consistent.

Console.logs (sample data):

Timestamp       FPS               (Current - previous) timestamp
------------------------------------------------------------
7042.73         59.97361161       0.016674
7066.278        42.4664515        0.023548
7082.952        59.97361161       0.016674
7099.626        59.97361161       0.016674
   .                 .                .
   .                 .                .
17104.026       59.97361161       0.016674
17112.84        113.4558657       0.008814
17129.514       59.97361161       0.016674
17146.188       59.97361161       0.016674

As can be seen in the above data sample, timestamp differences and frame rates are no longer steady, and sometimes occur too soon/too late, resulting in inconsistent frame rates. Had requestAnimationFrame been consistently invoked late, I would have assumed that due to JavaScript's single-threaded nature, complex code residing in the animate() function is taking too long to execute, and thus results in a delayed requestAnimationFrame invocation. However, since requestAnimationFrame is occasionally being invoked too early, this does not seem to be the case.

My code (skeleton):

for (var counter = 0; counter < elements.length; counter ++) //10 elements
{
  //some other code

  animate(element);
}

function animate(element)
{
   // function invocation => performs some complex calculations and animates the element passed in as a parameter

   window.requestAnimationFrame(function() { animate(element) } );
}

As can be seen in the above code snippet, requestAnimationFrame is being invoked multiple times for each element, within the initial for loop. RequestAnimationFrame invocations are also intended to go on infinitely, for each of the elements. Since the animation to be performed is highly time-critical (animation timing is very important in this scenario and should be as accurate as possible), it is essential that requestAnimationFrame invocations occur at consistent time intervals throughout. These time intervals should ideally be as close as possible to 0.016674 (≈ 60fps).

Some detail regarding animation to be performed (on each canvas element):

I have a very specific situation, for which I am required to draw a blinking/flashing animation as accurately as possible with respect to time, i.e. canvas colour will have to change at a consistent rate, for the specified time interval. Therefore, for instance, canvas colour needs to stay red for exactly 0.025 seconds, followed by another 0.025 seconds where canvas colour is set to blue, which are then followed by another 0.025s where canvas is red and so on...(animation should go on infinitely, for each of the elements). My current approach involves keeping track of the number of frames which have elapsed within the animation loop itself (thus, each requestAnimationFrame invocation corresponds to a single frame).

Since on a 60Hz monitor an exact frame length of 0.025 seconds cannot be achieved, each red/blue canvas cycle should be "approximated". So, taking into consideration a 60Hz monitor, creating a complete cycle, where the canvas is initially red, followed by blue, a total of 3 frames would be required (1 sec/60 = 0.01666667 seconds * 3 frames = 0.05 seconds => the desired duration for a single, complete red/blue cycle). Dividing 0.05 seconds by 2 would give the desired frame length (which is 0.025 seconds), however since this cannot be achieved on a 60Hz monitor, the cycle is approximated by presenting 2 red canvas frames, followed by a single blue frame (thus forming the entire 3-frame cycle). Unfortunately, even when taking the monitor's refresh rate into consideration, the timing tends to fluctuate, resulting in undesirable inaccuracies.

Final questions:

  1. Would you be able to clarify what is causing this inconsistent requestAnimationFrame behaviour?

  2. Can any optimisations be applied to ensure that requestAnimationFrame invocations are executed at consistent time intervals?

  3. Can better timing accuracy be achieved if I use some other kind of functionality (say, web workers in combination with the setInterval() function)?

Pantagruel answered 27/8, 2019 at 20:8 Comment(15)
You should be doing your logic within a setTimeout and visual updates within requestAnimationFrameGoalie
@GetOffMyLawn However isn't setTimeout highly inaccurate? From tests performed, as well as a number of online sources it appears to be so. The same applies for setInterval.Pantagruel
The interval starts x milliseconds after the callback finishes executing, so tasks that vary in complication can delay or speed up when the callback gets called again.Goalie
In short: You need to check the time inside Your loop by using High precision timingJanuaryjanuisz
@Januaryjanuisz I understand how this might be useful in cases whenever requestAnimationFrame is called too early, as whenever this happens, one can wait for the desired time to elapse, and then execute the function. However, how will this be helpful whenever requestAnimationFrame is invoked too late? More time than is necessary has already elapsed, thus whenever requestAnimationFrame is invoked at a later stage, I am not sure how high precision timing will aid to guarantee consistent timing.Pantagruel
@GetOffMyLawn I have performed a simple test to try this out. Unfortunately, this has not resulted in the desired performance improvement.Pantagruel
What browser were you using to produce this 113FPS frame? Can't repro this. Otherwise 1 if anything blocks the browser, it won't be able to maintain a consistent frame rate. 2, depends on what you are doing / what is causing the bottleneck. Some operations could be offloaded to a WebWorker, but not all. 3 depends what you are trying to do exactly. Now, you should not assume any frame rate, browsers may (and actually should) adapt it to the monitor's frame rate. So two users may have completely different results using two different monitors (even on the same browser+computer).Hanukkah
@Hanukkah Hi Kaiido, I am currently using Google Chrome. Yes, I am aware of both1 & 2. However, what is surprising is that at some points it executes early & results in a spike which corresponds to approximately 113fps. Well, to give you a better idea of what I'm doing in the animation loop, my intentions are to consistently check the number of frames which have elapsed for each of the canvas elements & to perform a specific function for each if the desired number of frames have elapsed (each canvas has a different requirement as to how many frames should elapse before performing the function).Pantagruel
@Hanukkah So within the request animation frame loop, we have some code (if-else statements) which on each requestAnimationFrame() invocation check the number of frames which have elapsed for each of the canvas elements (this is done by means of a counter). This logic assumes that on each invocation of requestAnimationFrame() a single frame has elapsed.Pantagruel
you can control the fps with this package npmjs.com/package/loopzWistrup
Kaiido's comment was the answer to the questions at the end of the original post. I wouldn't worry too much about one callback firing earlier and focus on whether your code is causing jank. Did you check the Performance tab in DevTools already?Myocarditis
Yes, there is most definitely jank, as sometimes the animation will lag/stop at unexpected intervals (these points are associated with those instances when a sudden increase/decrease in fps occurs). Yes, I have been using DevTools to monitor performance, to determine which web page areas are being constantly repainted, the current fps, etc. @Nickolay.Pantagruel
My understanding is that you can look at the frame that took too long to render in DevTools and see why it took so long.Myocarditis
When you have such short drops in framerate and you don't know why, is GC always a good guess.Dicotyledon
Yes, it's also possible @Thomas. Thanks for this, I will look into this further.Pantagruel
W
5

requestAnimationFrame will "do its best" to run at a "consistent" frame rate. That doesn't guarantee a 60fps; it just states that it will animate as fast as it can.

The method in a nutshell allows you to execute code on the next available screen repaint, taking the guess work out of getting in sync with the user's browser and hardware readiness to make changes to the screen.

We enter a callback function containing the code we wish to run, and requestAnimationFrame() will run it when the screen is ready to accept the next screen repaint.

In order to keep constant your animation you have to calculate the data in the callback recomputing values against the actual delta, not presuming a constant FPS.

Eg:

function moveit(timestamp, el, dist, duration){
    //if browser doesn't support requestAnimationFrame, generate our own timestamp using Date:
    var timestamp = timestamp || new Date().getTime()
    var runtime = timestamp - starttime
    var progress = runtime / duration
    progress = Math.min(progress, 1)
    el.style.left = (dist * progress).toFixed(2) + 'px'
    if (runtime < duration){ // if duration not met yet
        requestAnimationFrame(function(timestamp){ // call requestAnimationFrame again with parameters
            moveit(timestamp, el, dist, duration)
        })
    }
}

requestAnimationFrame(function(timestamp){
    starttime = timestamp || new Date().getTime() //if browser doesn't support requestAnimationFrame, generate our own timestamp using Date
    moveit(timestamp, adiv, 400, 2000) // 400px over 1 second
})
Weaner answered 3/9, 2019 at 11:14 Comment(9)
Hi, thanks for this, it definitely makes sense for the specific scenario you provided in your example. However, I have a very specific situation, for which I am required to draw a blinking animation as accurately as possible with respect to time, i.e. canvas colour will have to change at a consistent rate, for the specified time interval. Therefore, for instance, canvas colour needs to stay red for exactly 0.025 seconds, followed by another 0.025 seconds where canvas colour is set to blue, which are then followed by another 0.025s where canvas is red and so on...(goes on infinitely).Pantagruel
So, for this specific scenario, I am currently unaware of how your example can be adapted to satisfy these requirements.Pantagruel
@Pantagruel there might be a way that the pixel buffer of the canvas is the color you want at approximately the time you want (+- a few µs), but there is no way that it gets painted exactly at this rate. The monitor's screen refresh rate will ultimately conflic with whatever your code askes, there is no way around that. If we take the common 60Hz rate, your canvas can only be painted either 0.016ms, or 0.032ms nothing in between.Hanukkah
As @Hanukkah says you can skip frames, delay frame rendering, but blinking at PERFECT timing without considering what happens on the CPU and refresh rate quantum it's impossibleExtrajudicial
I agree with that, the refresh rate will most definitely interfere. However, I intend to take this into consideration when performing my calculations. Sorry for not mentioning this in my previous comment/question. More details provided in the following comment.Pantagruel
Say, for a 60Hz monitor, in order to create a complete cycle, where the canvas is red, followed by blue, a total of 3 frames would be required (1 sec/60 = 0.01666667 seconds * 3 frames = 0.05 seconds => the desired duration for a single, complete red/blue cycle). Dividing 0.05 seconds by 2 would give me the desired frame length (0.025 seconds), however I am aware that this cannot be achieved on a 60Hz monitor. So I intend to approximate the cycle by presenting 2 red canvas frames, followed by a single blue frame (thus forming the entire 3-frame cycle).Pantagruel
However, I am still getting a timing issue when approximating the cycle in this manner.Pantagruel
There are two many factors here to achieve what the op is requesting your doing this is a browser where you have no control over the hardware or software outside the limited scope of the browser window. You cannot guarantee there are no other processes taking resource from the browser, the window is going to the active tab, the processing speed of the machine its being run on, so even if you managed to make this work on the machine your developing on it would not perform the same on any other machine due to configuration variations software, hardware and current processing load.Mornings
@Mornings Yes, I agree, hardware setups & different configurations will definitely play an important role in the performance achieved. For example, a PC which has a powerful graphics card, will more likely perform better in such a situation.Pantagruel

© 2022 - 2024 — McMap. All rights reserved.