May 31, 2020

Time Does Not Flow Evenly

You are working on some system and you need some kind of timestamp in milli- or nanoseconds. Nothing easier than that. In Unix you might use clock_gettime and in Java System.currentTimeMillis() and of your go.

// Forgive me my C, I probably got things wrong =)
#include <sys/time.h>
#include <stdint.h>

int64_t time_stamp_nanos(){
    struct timespec current;
    clock_gettime(CLOCK_REALTIME, &current);
    return current.tv_sec * 1000000000 + current.tv_nsec;
}
public static long timeStamp(){
    return System.currentTimeMillis()
}

Now you are tempted to use that timestamp to order events, protocol packets or something else. NOOOOOO, STOP, you need more thought here. Dealing with time is tricky even at this low level.

Time May Jump Backwards

Yes, time can move backwards. This can happen in many ways. On a laptop, the user might change the time or switches time zone. In a server environment, the machine might correct its time via the network. (NTP or something alike). Or the actual clock on the hardware has an issue. You might think you know your OS and setup, but that OS might run on virtualized hardware on top another OS, which can have the same issues itself, fiddling with the clock outside of your control.

You might think that this is a rare event. We debugged once strange reoccurring disconnects in a 3rd party protocol. After weeks of debugging and second-guessing it came down to a protocol that used the clock to sequence events, and cloud machines which clocks periodically jumped back in time causing the protocol to get confused. So, the clock jumps happened enough often to cause noticeable issues. Link to the issue, it’s fixed now =)

Time Traveling
Figure 1. Time Traveling ;)

What should you do if you want the time to increase for sure? You should use a monotonically increasing clock which guarantees to never jump back. In Unix there is the CLOCK_MONOTONIC option and in Java System.nanoTime(). These two clocks won’t jump back. Note that Java’s System.nanoTime() does not guarantee that that value corresponds to an absolute time, for example, it could start a zero or any number. It only measures time passing as you invoke and compare values.

int64_t time_stamp_nanos(){
    struct timespec current;
    clock_gettime(CLOCK_MONOTONIC, &current);
    return current.tv_sec * 1000000000 + current.tv_nsec;
}
public static long timeStamp(){
    return System.currentTimeMillis()
}

Time Has Limited Resolution

Time sources have limited resolution and computers are fast. So, if you keep asking for the time, you might get the same time back, since the resolution of the clock is too low. I saw crashes due to this because code expected time to progress. For example something like this:

var summaryMap = new HashMap<Long, String>();
summaryMap.put(System.currentTimeMillis(), "Some stuff");
var operationResult = doSomeOperation();
// Might overwrite previous entry because time didn't progress
summaryMap.put(System.currentTimeMillis(), operationResult);
var moreResult = moreResultOperation();
// Might overwrite previous entry because time didn't progress
summaryMap.put(System.currentTimeMillis(), moreResult);
// Summary map is maybe missing entries. If your lucky you get a crash, if your unlucky you get wrong report
printSummary(summaryMap);

In a small test on my laptop, it easily can write multiple small files in 1 milliseconds:

var random = new Random();
var writesInSameTime = 0;
var time = System.currentTimeMillis();
long nextTime;
do {
    // Do some work, including IO
    var data = new byte[8 * 1024];
    random.nextBytes(data);
    Files.write(p.resolve("time-" + time + "i" + writesInSameTime),
            data);

    writesInSameTime++;
    nextTime = System.currentTimeMillis();
} while (time == nextTime);

System.out.println("Iterations " + writesInSameTime + " time resolution " + (nextTime - time));

Some result lines running this multiple times:

Iterations 1 in 1ms
Iterations 1 in 1ms
Iterations 2 in 1ms
Iterations 13 in 1ms
Iterations 8 in 1ms
Iterations 11 in 1ms
Iterations 11 in 1ms
Iterations 15 in 1ms
Iterations 12 in 1ms
Iterations 9 in 1ms
Iterations 13 in 1ms
Iterations 14 in 1ms
Iterations 7 in 1ms
Iterations 16 in 1ms

So, avoid relying on clock progress in your code. The clock might doesn’t have enough resolution to show progress.

Time Standing Still
Figure 2. Time Standing Still

Prefer Sequence Numbers

In general, prefer sequence numbers to order events, packets in protocols etc. A sequence number is not tangled with the pitfalls of time. You can still include time as an extra piece of information for the human reader/debugger.

If you only need one timestamp, you could 'turn' the time into a timestamp-sequence-hybrid that always progresses:

private final static AtomicLong lastTime = new AtomicLong(System.nanoTime());

public static long timeStamp() {
    return lastTime.updateAndGet(oldTime -> {
        var time = System.nanoTime();
        if (time > oldTime)
            return time;
        else
            return oldTime + 1;
    });
}

Summary

Be aware the time isn’t guaranteed to pass evenly, it might jump back into the past. Be aware that the time sources have a limited resolution. Check out your API documentation to choose the right time source. Prefer sequence numbers to order things like events, protocol packets etc.

Tags: Unix Time-Handling Java Development C/C++