Monotonic timestamps in C#
If you’ve ever written code which compares two DateTimes
, you might be living on borrowed time.
Most people assume that time ticks forwards at a rate of one second per second, and this assumption pervades a lot of our programming as well.
However this isn’t always the case: the user changing their clock, or leap seconds, can cause time to jump backwards and forwards, invalidating your calculations.
Even recently this caused serious issues for CloudFlare.
The solution is simple: use a monotonic time source: one that is independent of the system time, and always ticks forward at a rate of one second per second. Unfortunately .NET doesn’t expose such a time source directly. Here’s an easy way to make one.
There are a few monotonic time sources exposed by Windows.
QueryPerformanceCounter
is a relatively accurate one which is always available on XP and later, so we’ll use that.
An alternative is GetTickCount64()
.
When QueryPerformanceCounter
is available, the Stopwatch
class will act as a monotonic time source, however it will fall back to being non-monotonic if it isn’t available.
You can use Stopwatch
directly to act as a monotonic time source if you wish – however this is often ugly.
An alternative is to wrap calls to QueryPerformanceCounter
in a struct called MonotonicTimestamp
, which represents an instance in time.
However you cannot inspect a MonotonicTimestamp
directly: you can only compare it to another MonotonicTimestamp
to get the TimeSpan
which elapsed between them.
public struct MonotonicTimestamp
{
private static double tickFrequency;
private long timestamp;
static MonotonicTimestamp()
{
long frequency;
bool succeeded = NativeMethods.QueryPerformanceFrequency(out frequency);
if (!succeeded)
{
throw new PlatformNotSupportedException("Requires Windows XP or later");
}
tickFrequency = (double)TimeSpan.TicksPerSecond / frequency;
}
private MonotonicTimestamp(long timestamp)
{
this.timestamp = timestamp;
}
public static MonotonicTimestamp Now()
{
long value;
NativeMethods.QueryPerformanceCounter(out value);
return new MonotonicTimestamp(value);
}
public static TimeSpan operator -(MonotonicTimestamp to, MonotonicTimestamp from)
{
if (to.timestamp == 0)
throw new ArgumentException("Must be created using MonotonicTimestamp.Now(), not default(MonotonicTimestamp)", nameof(to));
if (from.timestamp == 0)
throw new ArgumentException("Must be created using MonotonicTimestamp.Now(), not default(MonotonicTimestamp)", nameof(from));
long ticks = unchecked((long)((to.timestamp - from.timestamp) * tickFrequency));
return new TimeSpan(ticks);
}
private static class NativeMethods
{
[DllImport("kernel32.dll")]
public static extern bool QueryPerformanceCounter(out long value);
[DllImport("kernel32.dll")]
public static extern bool QueryPerformanceFrequency(out long value);
}
}
Use it like this:
```csharp var start = MonotonicTimestamp.Now();
// .. stuff .. TimeSpan elapsed = MonotonicTimestamp.Now() - start;