3 min read

TiL: Wall clock vs monotonic clock

TiL: Wall clock vs monotonic clock

I've recently stumbled upon an article describing how NOT to measure the time in Node.js applications. I was once asked by a colleague at Netguru, why we should use the performance.now() rather than Date.now(), back then I didn't know how to answer. Here is the original if you want to read it.

In essence there are several sources for where the time values can come from in every system. One can easily be a subject to change - leap seconds, synchronisation with NTP, custom user operation all those may lead your system astray if you are using the time-of-day clock/wall clock for duration measurements, the second clock doesn't take any of the above into account and is therefore safer to use for programs where duration precision is important. If you are using the wall time to measure the duration of the process, don't be surprised if you occasionally get a negative number in your results!

The main difference between a wall clock and a monotonic clock seems to be that one is adjustable and can be rewinded, while the other allows the time to only flow forwards. The latter is also more precise.

Of course I immediately wanted to know how go deals with this problem - weirdly I have never before run into those issues or see them mentioned in any codebase I've worked with. I quickly found a bunch of resources - read through a stack overflow thread, GitHub issue discussion, the official docs and an extra article to consolidate what I've found.

To the point though - go apparently handles both at the same time - at least in some scenarios, then there are the ones you simply have to watch out for as they "reset" the monotonic clock for the operation. Beware of the "wall clock computation" methods (AddDate, Round, Truncate) and methods that are used on wall time values (t.In, t.Local, t.UTC) - all those return wall time values, which strip away the monotonic clock. Those are still comparable, but may skip some of the detailed information

package main

import (
	"fmt"
	"time"
)

func main() {
	t1 := time.Now()
	t2 := time.Now()
	fmt.Println("t1:", t1)
	fmt.Println("t2:", t2)
	fmt.Println("difference:", t2.Sub(t1))
}

The results is calculated based on the monotonic clock results:

71793-71626 = 167

But when one of the values doesn't have a value it could use it defaults to the wall clock reading for this computation. After the docs:

If either t or u contains no monotonic clock reading, these operations fall back to using the wall clock readings.

So the following may be interesting - as the value of the monotonic clock will be different than the result of the difference

package main

import (
	"fmt"
	"time"
)

func main() {
	t1 := time.Now().Round(0)
	time.Sleep(10 * time.Millisecond)
	t2 := time.Now()
	fmt.Println("t1:", t1)
	fmt.Println("t2:", t2)
	fmt.Println("difference:", t2.Sub(t1))
}

Sample result:

t1: 2022-03-24 19:41:24.961237 +0100 CET
t2: 2022-03-24 19:41:24.972363 +0100 CET m=+0.011195042
difference: 11.126ms

which is not the same as the result of

11195042 - 0 = 11195042

(0 used for simplicity as the default value for monotonic clock that is missing on the first Date type)

Important note - watch out for comparisons. The "==" comparison checks the monotonic value as well, so any discrepancies will cause a potential false negative on such a comparison. After the docs:

In general, prefer t.Equal(u) to t == u, since t.Equal uses the most accurate comparison available and correctly handles the case when only one of its arguments has a monotonic clock reading.
package main

import (
	"fmt"
	"time"
)

func main() {
	t1 := time.Now()
	t2 := t1.Round(0)
	fmt.Println("equality", t1 == t2) // false
	fmt.Println("t.Equal", t1.Equal(t2)) // true
}

It's amazing that I could find three quality pieces that are the antithesis of what you often get there, meaning the SO thread is actually kind and helpful, the GH issue shows how to disagree in a productive manner and the medium article doesn't give in the clickbaity title / images fad.