11

Assumptions: rAF now time is calculated at the time the set of callbacks are all triggered. Therefore any blocking that happens before the first callback of that frame is called doesn't affect the rAF now and it's accurate--at least for that first callback.

Any performance.now() measurements made before a rAF set is triggered should be earlier than rAF now.

Test: Record before (a baseline time before anything happens). Set the next rAF. Compare rAF now and actual performance.now() to before to see how different they are.

Expected results:

var before = performance.now(), frames = ["with blocking", "with no blocking"], calls = 0;
requestAnimationFrame(function frame(rAFnow) {
  var actual = performance.now();
  console.log("frame " + (calls + 1) + " " + frames[calls] + ":");
  console.log("before frame -> rAF now: " + (rAFnow - before));
  console.log("before frame -> rAF actual: " + (actual - before));

  if (++calls < frames.length) { before = actual; requestAnimationFrame(frame); }
});

// blocking
for (var i = 0, l = 0; i < 10000000; i++) {
    l += i;
}

Observations: When there is blocking before the frame starts, the rAF now time is at times incorrect, even for that first frame. Sometimes the first frame's now is actually an earlier time than the recorded before time.

Whether there is blocking happening before the frame or not, every so often the in-frame time rAFnow will be earlier than the pre-frame time before--even when I setup the rAF after I take my first measurement. This can also happen without any blocking whatsoever, though this is rarer.

(I get a timing error on the first blocking frame most of the time. Getting an issue on the others is rarer, but does happen occasionally if you try running it a few times.)

With more extensive testing, I've found bad times with blocking prior to callback: 1% from 100 frames, no blocking: 0.21645021645021645% from ~400 frames, seemingly caused by opening a window or some other potentially CPU-intensive action by the user.

So it's fairly rare, but the problem is this shouldn't happen at all. If you want to do useful things with them, simulating time, animation, etc., then you need those times to make sense.

I've taken into account what people have said, but maybe I am still not understanding how things work. If this is all per-spec, I'd love some psuedo-code to solidify it in my mind.

And more importantly, if anyone has any suggestions for how I could get around these issues, that would be awesome. The only thing I can think of is taking my own performance.now() measurement every frame and using that--but it seems a bit wasteful, having it effectively run twice every frame, on top of any triggered events and so on.

Whothehellisthat
  • 1,956
  • 1
  • 13
  • 13

2 Answers2

8

The timestamp passed in to the requestAnimationFrame() callback is the time of the beginning of the animation frame. Multiple callbacks being invoked during the same frame all receive the same timestamp. Thus, it would be really weird if performance.now() returned a time before the parameter value, but not really weird for it to be after that.

Here's the relevant specification:

When the user agent is to run the animation frame callbacks for a Document document with a timestamp now, it must run the following steps:

  1. If the value returned by the document object’s hidden attribute is true, abort these steps. [PAGE-VISIBILITY]

  2. Let callbacks be a list of the entries in document’s list of animation frame callbacks, in the order in which they were added to the list.

  3. Set document’s list of animation frame callbacks to the empty list.

  4. For each entry in callbacks, in order: invoke the Web IDL callback function, passing now as the only argument, and if an exception is thrown, report the exception.

So you've registered a callback (let's say just one) for the next animation frame. Tick tick tick, BOOM, time for that animation frame to happen:

  1. The JavaScript runtime makes a note of the time and labels that now.
  2. The runtime makes a temporary copy of the list of registered animation frame callbacks, and clears the actual list (so that they're not accidentally invoked if things take so long that the next animation frame comes around).
  3. The list has just one thing in it: your callback. The system invokes that with now as the parameter.
  4. Your callback starts running. Maybe it's never been run before, so the JavaScript optimizer may need to do some work. Or maybe the operating system switches threads to some other system process, like starting up a disk buffer flush or handling some network traffic, or any of dozens of other things.
  5. Oh right, your callback. The browser gets the CPU again and your callback code starts running.
  6. Your code calls performance.now() and compares it to the now value passed in as a parameter.

Because a brief but non-ignorable amount of time may pass between step 1 and step 6, the return value from performance.now() may indicate that a couple of microseconds, or even more than a couple, have elapsed. That is perfectly normal behavior.

Community
  • 1
  • 1
Pointy
  • 389,373
  • 58
  • 564
  • 602
  • Are you saying that _any_ callback, even non-rAF ones will have the same performance.now() value? That doesn't seem to be the case. If you mean multiple rAF callbacks, I'm only using a single rAF. – Whothehellisthat Jul 14 '16 at 22:25
  • I've made edits to better explain what's happening. (I wasn't sure if you'd get notified?) – Whothehellisthat Jul 14 '16 at 22:30
  • @Whothehellisthat no no no. When the event occurs internal to the JavaScript runtime that triggers the processing of callbacks for an animation frame, the timestamp is noted **once** at the beginning of that process. Each callback registered by `requestAnimationFrame()` is passed exactly the same time value. It really has nothing to do with subsequent calls to `performance.now()`, except that they will obviously all happen *after* that initial time is collected. – Pointy Jul 14 '16 at 23:41
  • Depending on what's happening on your system (other processes, operating system work, etc), it's entirely possible for some microseconds to elapse between the animation frame internals collect the frame start timestamp and the time at which the code in your callback runs. In particular, the system **does not** pass the result of calling `performance.now()` to each callback. Thus, even if there's only one registered callback, it is not surprising that it's parameter is a smaller number than what's returned by `performance.now()`. – Pointy Jul 14 '16 at 23:43
  • @Whothehellisthat answer extended a little. – Pointy Jul 15 '16 at 00:22
  • That all sounds logical. The problem is, it's up to 16 milliseconds out--not a few microseconds, but a whole frame! And that's just the first thing is does in the rAF, before any processing is done. – Whothehellisthat Jul 16 '16 at 06:42
  • Now heavily editing the question to pare it down to the important parts. – Whothehellisthat Jul 16 '16 at 07:30
  • Edits done, basically rewritten from scratch to be much clearer and more focussed. – Whothehellisthat Jul 16 '16 at 08:32
  • Any thoughts? You've been very helpful to me on exploring what is actually going on here. – Whothehellisthat Jul 19 '16 at 18:16
  • Well, ultimately neither the browser nor the underlying operating system is a truly real-time platfurm. Real-time systems have all sorts of ways of controlling latency and preemption priority, but between the OS and the browser (and, for that matter, the JavaScript interpreter itself) you're far from that. And, of course, you may be encountering legit bugs in the browser(s) you're testing. – Pointy Jul 19 '16 at 19:55
  • Yeah, that's fair enough. But to me, that's the whole point of using Date.now or performance.now or rAF's now--you can make calculations based on the correct time at runtime. I'm guessing it probably is a bug, as you say. Otherwise what would be the point of rAF now. I'll just take my own performance.now measurement each frame, I guess. – Whothehellisthat Jul 21 '16 at 05:19
  • Question: can the timeStamp argument received by the callback ever be equal to zero? It seems that it could be zero for the first animation frame, but I have never seen that happen. Then again, I've have not tested the idea fully. – jamess Sep 27 '18 at 17:17
  • You said `it would be really weird if performance.now() returned a time before the parameter value`, but the whole problem is the initial `before` is larger than `rAFnow` passed by requestAnimationFrame, which is because `timestamp` passed by requestAnimationFrame is not made when people would think it would be. – Changdae Park Aug 08 '21 at 01:32
2

I encountered the same issue on chrome, where calls to performance.now () would return a higher value than the now value passed into subsequent callbacks made by window.requestAnimationFrame ()

My workaround was to set the before using the now passed to the callback in the first window.requestAnimationFrame () rather than performance.now (). It seems that using only one of the two functions to measure time guarantees progressing values.

I hope this helps anyone else suffering through this bug.