DEV Community

Noam Rosenthal
Noam Rosenthal

Posted on • Updated on

When a millisecond is not a millisecond

The origin of time

performance.timeOrigin is a value that supposedly represents the time (in milliseconds from the epoch) when the navigation to the current document has started, or when a worker was created. This allows translating high-resolution time, used for performance measurements, into “real” time, AKA wall-clock time, the time we all know and love. Sounds simple. However…

A tale of two clocks

When measuring performance, we usually think of two clocks: the monotonic clock (e.g. performance.now()) and the wall clock (e.g. new Date()). We use the monotonic clock for high-resolution performance metrics, and the wall clock to display the time to the user, and to synchronize with times that are outside the scope of a single document.

This is the formula that seemingly represents the relationship between the clocks:

new Date().valueOf() === 
    performance.timeOrigin + 
    performance.now()
Enter fullscreen mode Exit fullscreen mode

The problem with this formula is that it’s correct sometimes. Apart from the difference in resolution, the problem is that this formula assumes that a millisecond is a millisecond.

What is a millisecond?

A millisecond is a measurement relative to the earth's rotation. It’s the time it takes the earth to rotate on its axis (aka a day), divided by 86,400,000 (24 hours * 60 minutes * 60 seconds * 1000). The problem is that the Earth’s rotation speed is not constant, and we don’t measure it on our computer and phones.

Globally, what we know as UTC is actually a standard based on a set of very accurate atomic clocks, periodically adjusted to match the Earth’s rotation.
What we do have is a real time clock (RTC), measuring time in a way similar to a quartz watch. performance.now() is really a count of oscillations or “ticks” of that real time clock, normalized to its known oscillation speed, and periodically adjusted to match UTC, using NTP.

To add some complexity, RTCs are not identical to each other, and the way they’re handled by operating systems also differs greatly.

In other words, there are no “accurate” clocks, and milliseconds means something different depending on how they are measured.

So why not use the wall clock and forget about the monotonic clock?

Because of how they measure time and how they’re synchronized, the two clocks available to us as web developers are both inaccurate, but in a different way. The monotonic clock (relying only on whatever RTC comes with our hardware and however the OS exposes monotonic time) is accurate enough for measuring short durations at high resolution, which is great for performance benchmarks. The wall clock is better at long durations and synchronizing across systems, because those systems all periodically sync with UTC (and thus with each other) using NTP.

What’s the problem then?

The root of the problem comes when using the monotonic clock, and scaling it over a long period of time. For example, calling performance.now() in a webpage that has been active for minutes, hours or days. The more time passes, and depending on the system the page is running on and whether it was sleeping at any point in the lifetime of the document, the formula above becomes less and less accurate, and performance.timeOrigin becomes less and less meaningful as a way to translate between the monotonic clock and the wall clock.

In those long running sessions, people were often surprised that their performance timeline entries, when adjusted by timeOrigin, did not match times on the server or other UTC-aligned times.

See this chromium bug report for example.
In short, when you use performance.timeOrigin you’re using a constant to represent a variable.

What’s the solution then?

There is no magical solution. As long as we don’t all have scientific-grade atomic clocks in our phones and as long as the earth speeds up or slows down, clock drift will remain a fact, and converting long monotonic durations back to UTC would bring surprising results.

What we can do is periodically measure the drift instead of relying on a constant timeOrigin.

The good news is that we don’t need a new API for this.

Variable Time Origin

Instead of relying on a constant to represent a variable, we can treat the time origin as the variable that it is, and measure it ourselves periodically:

// This is the inverse of the previous formula...
function variableTimeOrigin() {
 return new Date() - performance.now();
}
Enter fullscreen mode Exit fullscreen mode

We can call this once a minute, once an hour, once at the initialization of the document (if we want to mimic the behavior of performance.timeOrigin), once for every PerformanceEntry, or whenever suits our use case. Don’t call this at a high resolution though, as you’ll lose precision (large numbers with double-precision floats).

If you’re monitoring performance in the field, consider sending this value with your beacon, and adjust your dashboards in whatever way you see fit. There is no “correct” solution to how to superimpose timelines from different clocks, check what works for you.
Perhaps you’re doing this already.

Takeaways

Milliseconds vary based on the clock which measures them.
Therefore, don’t count on the monotonic clock to be synchronized with the wall clock over long periods of time.
If you need to correlate between the performance timeline and UTC, you can do so by occasionally measuring the variable time origin.

Have a great time!

Top comments (1)

Collapse
 
shifi profile image
Shifa Ur Rehman

This! Why doesn't anyone talk about this?
You have my thanks. My whole firm will bow to you eventually. You have my respects.