timestamp of requestAnimationFrame is not reliable
Asked Answered
B

1

1

I think the timestamp argument passed by requestAnimationFrame is computed wrongly (tested in Chrome and Firefox).

In the snippet below, I have a loop which takes approx. 300ms (you may have to tweak the number of loop iterations). The calculated delta should always be larger than the printed 'duration' of the loop. The weird thing is, sometimes it is slower sometimes not. Why?

let timeElapsed = 0;
let animationID;


const loop = timestamp => {
  const delta = timestamp - timeElapsed;
  timeElapsed = timestamp;

  console.log('delta', delta);
  
  // some heavy load for the frame
  const start = performance.now();
  let sum = 0;
  for (let i = 0; i < 10000000; i++) {
    sum += i ** i;
  }
  console.warn('duration', performance.now() - start);

  animationID = requestAnimationFrame(loop)
}

animationID = requestAnimationFrame(loop);

setTimeout(() => {
    cancelAnimationFrame(animationID);
}, 2000);

jsFiddle: https://jsfiddle.net/Kritten/ohd1ysmg/53/

Please not that the snippet stops after two second.

Beltz answered 2/10, 2020 at 19:41 Comment(0)
D
5

At least in Blink and Gecko, the timestamp passed to rAF callback is the one of the last VSync pulse.

In the snippet, the CPU and the event-loop are locked for about 300ms, but the monitor still does emit its VSync pulse at the same rate, in parallel.

When the browser is done doing this 300ms computation, it has to schedule a new animation frame.
At the next event-loop iteration it will check if the monitor has sent a new VSync pulse and since it did (about 18 times on a 60Hz), it will execute the new rAF callbacks almost instantly.

The timestamp passed to rAF callback may thus indeed be the one of a time prior to when your last callback ended, because the event-loop got freed after the last VSync pulse.

One way to force this is to make your computation last just a bit more than a frame's duration, for instance on a 60Hz monitor VSync pulses will happen every 16.67ms, so if we lock the event-loop for 16.7ms we are quite sure to have a timestamp delta lesser than the actual computation time:

let stopped = false;
let perf_elapsed = performance.now();
let timestamp_elapsed = 0;
let computation_time = 0;
let raf_id;

const loop = timestamp => {

  const perf_now = performance.now();

  const timestamp_delta = +(timestamp - timestamp_elapsed).toFixed(2);
  timestamp_elapsed = timestamp;

  const perf_delta = +(perf_now - perf_elapsed).toFixed(2);
  perf_elapsed = perf_now;

  const ERROR = timestamp_delta < computation_time;
  if (computation_time) {
    console.log({
      computation_time,
      timestamp_delta,
      perf_delta,
      ERROR
    });
  }

  // some heavy load for the frame
  const computation_start = performance.now();
  const frame_duration = 1000 / frequency.value;
  const computation_duration = (Math.ceil(frame_duration * 10) + 1) / 10; // add 0.1 ms 
  while (performance.now() - computation_start < computation_duration) {}
  computation_time = performance.now() - computation_start;

  
  raf_id = requestAnimationFrame(loop)
  
}

frequency.oninput = evt => {
  cancelAnimationFrame( raf_id );
  console.clear();
  raf_id = requestAnimationFrame(loop);
  setTimeout(() => {
    cancelAnimationFrame( raf_id );
  }, 2000);
};
frequency.oninput();
In case your monitor has a different frame-rate than th common 60Hz, you can insert it here:

<input type="number" id="frequency" value="60" steps="0.1">

So what to use between this timestamp and performance.now() is your call I guess, the timestamp tells you when the frame began, performance.now() will tell you when your code executes, you could use both if needed. Even without such a big computation spanning over frames, you can very well have an other task scheduled before yours that took a few ms to complete or even a big CSS composition that should get performed after, and you have no real way to know.

Davinadavine answered 10/10, 2020 at 3:25 Comment(4)
I'm from https://mcmap.net/q/1328989/-performance-now-called-before-requestanimationframe-performance-now-has-a-larger-t. I'd like you to check if the reason that the timestamp(let's say A) from performance.now() that is called before requestAnimationFrame is larger than the timestamp(let's say B) passed by requesteAnimationFrame() is the same as what I understood.Malaya
A is larger than B when it's like senario I'm showing you here. 1. Display refreshment is done when it's 0ms (from time origin). 2. performance.now() is done when it's 5ms. 3. The callback of requestAnimationFrame() is called when it's 10ms.Malaya
Because step 3 is done before next display refreshment, B is 0ms which has been set at step 1, right?Malaya
@ChangdaePark First, as I told you there, the question you linked to is actually completely different, there they just misunderstood the difference between being inside the callback and outside of it. Regarding your question, yes that's about it. Though, normally the call to rAF should schedule our callback to the next animation frame, so it should be A is zero, B is 5ms, call to rAF is made at B, A is now 16.67ms, our rAF callback is finally called. But Chrome is buggy:https://mcmap.net/q/1195525/-exact-time-of-display-requestanimationframe-usage-and-timeline#57549862Davinadavine

© 2022 - 2024 — McMap. All rights reserved.