.Net’s time traveling StopWatch

posted by Craig Gidney on May 14, 2012

One of programming’s many ‘gotcha’ bugs is measuring elapsed time by checking the system time twice. It feels like the natural solution but creates an easy to exploit bug. The ‘current time’ on a computer is not guaranteed to be consistent over time. It may be updated by the OS (i.e. synchronization), the user, and even programs (probably malicious ones).

Example:

var startTime = DateTime.Now;
Thread.Sleep(10000); // sure hope the user doesn't change the system date during this 10s wait...
var elapsedTime = DateTime.Now - startTime;
// elapsedTime can be ANYTHING. It can even be negative!

Anyone can make this mistake. I’ve made this mistake. You’ve probably made this mistake. But more importantly… the .Net framework contains this mistake. In the System.Diagnostics.StopWatch class.

Normally StopWatch uses a high frequency timer to measure elapsed time but, when a high frequency timer is not available, it uses DateTime.UtcNow as a fallback. If your code uses StopWatch and one of your users has an old machine without a high frequency timer and they happen to change the date while a duration is being measured… Well, let’s just hope they didn’t change the year.

Update: Microsoft has marked this issue has Won’t Fix, at least for now.

If you happen to have an old machine without a high frequency timer, you can confirm this bug using the example code I included in the issue:

[System.Runtime.InteropServices.DllImport("kernel32.dll")]
private static extern bool QueryPerformanceCounter(out long freq);

static void Main(string[] args) {
    long x;
    var hasHighRes = QueryPerformanceCounter(out x);
    Console.WriteLine("High res timer available: " + hasHighRes);
    if (hasHighRes) Console.WriteLine("Issue only appears WITHOUT high res timer");

    var s = System.Diagnostics.Stopwatch.StartNew();
    var starttick = Environment.TickCount;
    var startdate = DateTime.UtcNow;
    while (true) {
        System.Threading.Thread.Sleep(1000);
        unchecked {
            var dtick = (uint)(Environment.TickCount - starttick);
            var ddate = DateTime.UtcNow - startdate;
            Console.WriteLine(String.Format(
                "stopwatch: {0:0.0}s, tickcount: {1:0.0}s, datedif: {2:0.0}s",
                s.Elapsed.TotalSeconds,
                dtick/1000.0,
                ddate.TotalSeconds));
        }
    }
}


Twisted Oak Studios offers consulting and development on high-tech interactive projects. Check out our portfolio, or Give us a shout if you have anything you think some really rad engineers should help you with.

Archive

More interesting posts (32 of 33 articles)

Or check out our Portfolio.