Precise synchronization of scalable clocks in different browsing contexts

Let’s start with a clock running in real time in the current browsing context. (Its display resolution is 10 milliseconds, but that was an arbitrary choice.)

00:00:00.0.0

Now, let’s open another clock instance in a different browsing context (e.g. a new window (tab or popup), an iframe, or a WebWorker). For this blog post, we’ll choose a popup window. (your browser may block this popup, in which case tell it allow it).

The next steps will work correctly only after opening the popup window.

The clocks are now updating themselves as fast as the browser permits. But, to be able to get more meaningful time readings, let’s stop the automatic updates and let’s update the clocks only when manually requested by pushing the button. Note that the time still progresses normally in the background (you can confirm this by repeatedly updating). You also can start the again.

The clocks internally do not use any kind of loop or setInterval() or setTimeout() or something equivalent. The time is simply calculated from values from the performance browser API whenever the clock is updated (e.g. manually). In other words, if the clock does not update, no calculation at all takes place.

After a manual update, the two clocks show the same time.

This is possible because the latency between pushing the “Manual Redraw” button and the reception of this message in the popup window is less than the display resolution (it is approx. 1 millisecond on a desktop browser). However, this can still cause a one-higher number in the popup when the time value wraps around to the next 10 ms unit. But this only because of the message latency, and not because the clocks are desynchronized. The clocks are always synchronized because performance.now() is a monotonic clock with a fixed absolute time offset per browsing context.

So far, there is nothing special or difficult about this example. The MDN documentation of performance.timeOrigin shows example code to achieve this.

However, if we want to synchronize the clocks including a varying clock scale factor (e.g. double speed, half speed, etc. while the clocks are running), the implementation becomes more difficult (but still straightforward to solve with a bit of maths).

To demonstrate, select one of the following clock scale factors:

The two clocks remain exactly synchronized even after varying the scale factor.

One application of this technique is in real-time, time-discrete simulations running in different browser threads for performance reasons, that need to remain synchronous even when scaling the flow of time, e.g. in a scenario of slow motion or time lapse.

This technique remains precise even with the jitter of setInterval() or requestAnimationFrame which can be in the order of milliseconds. This is demonstrated by the fact that, as already mentioned, no calculation at all takes place when the clock is not updated; the clock updates may thus arrive in any, not even periodic, interval.

If you think you found a mistake in this blog post, or would like to suggest an improvement to this blog post, you can write me an e-mail to the address public dot michael at franzl dot name; as subject please use the prefix "Comment to blog post" and append the post title.
 
Copyright © 2023 Michael Franzl