2

First, let me say that I understand that Windows is not a real-time OS. I do not expect any given timer interval to be accurate to better than 15 ms. However, it is important to me that the sum of my timer ticks be accurate in the long term, which is not possible with the stock timers or the stopwatch.

If a normal System.Timers.Timer (or, worse yet, a Windows.Forms.Timer) is configured to run every 100 ms, it ticks at somewhere between 100 ms and about 115 ms. This means that if I need to do the event for one day, 10 * 60 * 60 * 24 = 864,000 times, I might end up finishing over three hours late! This is not acceptable for my application.

How can I best construct a timer for which the average duration is accurate? I have built one, but I think it could be smoother, more effective, or...there just has to be a better way. Why would .NET provide a low-accuracy DateTime.Now, a low-accuracy Timer, and a high-accuracy Diagnostics.Stopwatch, but no high-accuracy timer?

I wrote the following timer update routine:

var CorrectiveStopwatch = new System.Diagnostics.Stopwatch();
var CorrectedTimer = new System.Timers.Timer()
{
    Interval = targetInterval,
    AutoReset = true,
};
CorrectedTimer.Elapsed += (o, e) =>
{
    var actualMilliseconds = ; 

    // Adjust the next tick so that it's accurate
    // EG: Stopwatch says we're at 2015 ms, we should be at 2000 ms
    // 2000 + 100 - 2015 = 85 and should trigger at the right time
    var newInterval = StopwatchCorrectedElapsedMilliseconds + 
                      targetInterval -
                       CorrectiveStopwatch.ElapsedMilliseconds;

    // If we're over 1 target interval too slow, trigger ASAP!
    if (newInterval <= 0)
    {
        newInterval = 1; 
    }

    CorrectedTimer.Interval = newInterval;

    StopwatchCorrectedElapsedMilliseconds += targetInterval;
};

As you can see, it's not likely to be very smooth. And, in testing, it isn't very smooth at all! I get the following values for one example run:

100
87
91
103
88
94
102
93
103
88

This sequence has an average value of I understand that network time and other tools smoothly coerce the current time to get an accurate timer. Perhaps doing some sort of PID loop to get the target interval to targetInterval - 15 / 2 or so could require smaller updates, or perhaps smoothing could reduce the difference in timing between short events and long events. How can I integrate this with my approach? Are there existing libraries which do this?

I compared it to a regular stopwatch, and it seems to be accurate. After 5 minutes, I measured the following millisecond counter values:

DateTime elapsed:  300119
Stopwatch elapsed: 300131
Timer sum:         274800
Corrected sum:     300200

and after 30 minutes, I measured:

DateTime elapsed:  1800208.2664
Stopwatch elapsed: 1800217
Timer sum:         1648500
Corrected sum:     1800300

The stock timers are slower than the stopwatch by 151.8 seconds, or 8.4% of the total. That seems remarkably close to an average tick duration being between 100 and 115 ms! Also, the corrected sum is still within 100 ms of the measured values (and these are more accurate than I can find error with my watch), so this works.

My full test program follows. It runs in Windows Forms and requires a text box on "Form1", or delete the textBox1.Text = line and run it in a command-line project.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace StopwatchTimerTest
{
    public partial class Form1 : Form
    {
        private System.Diagnostics.Stopwatch ElapsedTimeStopwatch;
        private double TimerSumElapsedMilliseconds;
        private double StopwatchCorrectedElapsedMilliseconds;
        private DateTime StartTime;

        public Form1()
        {
            InitializeComponent();
            Run();
        }

        private void Run()
        {
            var targetInterval = 100;
            // A stopwatch running in normal mode provides the correct elapsed time to the best abilities of the system
            // (save, for example, a network time server or GPS peripheral)
            ElapsedTimeStopwatch = new System.Diagnostics.Stopwatch();

            // A normal System.Timers.Timer ticks somewhere between 100 ms and 115ms, quickly becoming inaccurate.
            var NormalTimer = new System.Timers.Timer()
            {
                Interval = targetInterval,
                AutoReset = true,
            };
            NormalTimer.Elapsed += (o, e) =>
            {
                TimerSumElapsedMilliseconds += NormalTimer.Interval;
            };

            // This is my "quick and dirty" corrective stopwatch
            var CorrectiveStopwatch = new System.Diagnostics.Stopwatch();
            var CorrectedTimer = new System.Timers.Timer()
            {
                Interval = targetInterval,
                AutoReset = true,
            };
            CorrectedTimer.Elapsed += (o, e) =>
            {
                var actualMilliseconds = CorrectiveStopwatch.ElapsedMilliseconds; // Drop this into a variable to avoid confusion in the debugger

                // Adjust the next tick so that it's accurate
                // EG: Stopwatch says we're at 2015 ms, we should be at 2000 ms, 2000 + 100 - 2015 = 85 and should trigger at the right time
                var newInterval = StopwatchCorrectedElapsedMilliseconds + targetInterval - actualMilliseconds;

                if (newInterval <= 0)
                {
                    newInterval = 1; // Basically trigger instantly in an attempt to catch up; could be smoother...
                }
                CorrectedTimer.Interval = newInterval;

                // Note: could be more accurate with actual interval, but actual app uses sum of tick events for many things
                StopwatchCorrectedElapsedMilliseconds += targetInterval;
            };

            var updateTimer = new System.Windows.Forms.Timer()
            {
                Interval = 1000,
                Enabled = true,
            };
            updateTimer.Tick += (o, e) =>
            {
                var result = "DateTime elapsed:  " + (DateTime.Now - StartTime).TotalMilliseconds.ToString() + Environment.NewLine + 
                             "Stopwatch elapsed: " + ElapsedTimeStopwatch.ElapsedMilliseconds.ToString() + Environment.NewLine +
                             "Timer sum:         " + TimerSumElapsedMilliseconds.ToString() + Environment.NewLine +
                             "Corrected sum:     " + StopwatchCorrectedElapsedMilliseconds.ToString();
                Console.WriteLine(result + Environment.NewLine);
                textBox1.Text = result;
            };

            // Start everything simultaneously
            StartTime = DateTime.Now;
            ElapsedTimeStopwatch.Start();
            updateTimer.Start();
            NormalTimer.Start();
            CorrectedTimer.Start();
            CorrectiveStopwatch.Start();
        }
    }
}
Peter Duniho
  • 68,759
  • 7
  • 102
  • 136
kvermeer
  • 458
  • 3
  • 12
  • This is a bit too dense for me to analyse now (kind of tired and without too much time :)), but I can write a couple of quick remarks. Firstly, your statement "it ticks at somewhere between 100 ms and about 115 ms... I might end up finishing over three hours late" is quite misleading. By assuming that this information is right, it would mean that each tick might have a maximum delay of 15 ms; but these delays might either not happen or compensate among them (faster than 100 ms?!). Thus after hours or days, the average delay of all the ticks might be even zero (very unlikely though)... – varocarbas Oct 07 '15 at 19:30
  • ... On the other hand, when dealing with so small intervals everything counts (the hardware/software of your computer, what is being run on your computer, the way in which your application is being executed, etc.); even the order in which the elapsed times of the different variables are being measured might affect the results. In any case, thanks for sharing your thoughts on this matter; I look forward to taking a closer look at your code at other point. – varocarbas Oct 07 '15 at 19:31
  • @varocarbas - I also assumed initially that the average delay would be zero, and that the result would either be approximately correct, or lie in a distribution about the correct value. However, this is incorrect, the Timer class guarantees that the time between calls will be *greater than or equal to the target interval.* In practice, as you can see above, it's about 8.4 ms per tick slow all the time! – kvermeer Oct 07 '15 at 19:37
  • As said, I will take a proper look at all this at other point (= tomorrow) and will share my impressions then – varocarbas Oct 07 '15 at 19:42
  • 1
    You may want to take a look at Reactive Extensions and the timers it provides (`Observable.Timer`). A lot of clever code went into this to ensure the timers are useful in this kind of scenario (firing on absolute times or with reliable intervals). See, for example, http://stackoverflow.com/questions/13838113/observable-timer-how-to-avoid-timer-drift. (Rx has quite a steep learning curve if you've never seen it before, though.) – Jeroen Mostert Oct 07 '15 at 19:43
  • There are a lot of _possible_ questions in your post. Could you please identify what the _actual_ question is? I'm having a hard time understanding what it is you actually want help with. As you already know, Windows won't give you precise timings. You've already implemented a suitable work-around for ensuring accurate _average_ timer behavior. Given that thread scheduling can be off as much as ~50ms, that you get only 15ms of jitter seems pretty reasonable. If you want more accurate timing, you need to use Windows' multimedia timers, which are not exposed by .NET (i.e. you need p/invoke). – Peter Duniho Oct 08 '15 at 03:59
  • @Peter - Sorry, there is a lot to digest. My end goal is stated in the title, so if there's another way to get long term accuracy with smooth intervals, that would be great. At the same time, I've got a long-term accurate solution, but it's not smooth - help improving the algorithm is what I'm really looking for. – kvermeer Oct 08 '15 at 12:10
  • @Jeroen - Reactive Extensions looks great! I've never seen it before, but I'll see if I can wrap my head around it. Thanks for the link and Googleable name! – kvermeer Oct 08 '15 at 12:13
  • I have done some tests with your code and even tried different alternatives; and have to recognise that wasn't expecting to see so big (and constantly-increasing) differences. In any case, the proposed conditions represent an extreme case which can even be considered outside the truly-speaking scope of application of timers. In summary: the surprising (at least, to me) results should be as the logical consequence of bringing something to its extreme limits; that is: something to be avoided no matter what. – varocarbas Oct 08 '15 at 13:46
  • @varocarbas - thanks for trying it out! I'm not sure that I understand what you're saying - did you see the stopwatch-compensated timer showing differences in the elapsed time? Or were you surprised at the inaccuracy of the stock timer? – kvermeer Oct 08 '15 at 13:48
  • I was surprised by the accumulated delay in the tick events. I tried different alternatives and varied the conditions; but the differences (between the actual timer tick and the real elapsed time) did remain surprisingly high. But as said... ticking a timer each 100 ms for too long is not the way in which it is supposed to be used (= that's why its not too reliable behaviour). Regarding your correction, it seems fine. Perhaps you should account for some of the Hans Passant's suggestions too. – varocarbas Oct 08 '15 at 13:50
  • _"if there's another way to get long term accuracy with smooth intervals"_ -- you state at the outset that you understand Windows is not a real-time OS. Well, it's not. And the fact that it's not precludes the possibility of "smooth intervals". The best answer you could hope for is to use the multimedia timers, but a real answer actually providing full details on that topic would be too broad. You should research that yourself and ask for help with _specific_ issues if they arise. – Peter Duniho Oct 08 '15 at 15:33
  • 1
    @varocarbas: _"wasn't expecting to see so big (and constantly-increasing) differences"_ -- that's completely expected, given the promise any of the timers provide: to be signaled **no sooner than** the stated interval, but without a guarantee of being signaled precisely **on** the interval. I.e. the timer can only ever be later, and so all errors are in the positive direction, and so over time the timer will only ever become later and later. – Peter Duniho Oct 08 '15 at 15:35
  • @PeterDuniho As explained above, I never thought too much about this (equivalently to what I do with any other situation which might be considered as not-practical enough/beyond the application scope of the given object), neither I think that this is relevant in any way as far as these conditions shouldn't ever be attempted (timers weren't created to be used in this way). Timers have always been on time (under my expectations) when I have used them; and that's why so big delays (and always delays, with no compensation) were intuitively weird to me. That was the whole point of my comment. – varocarbas Oct 08 '15 at 16:13

1 Answers1

2

Windows is not a real-time OS

More accurately, code that runs in ring 3 on a protected mode demand-paged virtual memory operating system cannot assume it can provide any execution guarantees. Something as simple as a page fault can greatly delay code execution. Plenty more like that, a gen #2 garbage collection is always around to ruin your expectations. Ring 0 code (drivers) generally has excellent interrupt response times, modulo a crappy video driver that cheats on its benchmarks.

So no, hoping that code activated by a Timer will run instantly is idle hope in general. There will always be a delay. A DispatcherTimer or Winforms Timer can only tick when the UI thread is idle, an asynchronous timer needs to compete with all the other threadpool requests as well as threads owned by other processes that compete for the processor.

So one thing you never want to do is increment an elapsed time counter in the event handler or callback. It will quickly diverge from elapsed wall clock time. In a completely unpredictable way, the heavier the machine is loaded the more it falls behind.

Workaround is simple, if you care about actual elapsed time then use the clock. Either DateTime.UtcNow or Environment.TickCount, there's no fundamental difference between the two. Store the value in variable when you start the timer, subtract it in the event or callback to know the actual elapsed time. It is highly precise over long periods, accuracy is limited by the clock update rate. Which by default is 64 times per second (15.6 msec), it can be jacked up by pinvoking timeBeginPeriod(). Do so conservatively, it is bad for power consumption.

Stopwatch has opposite characteristics, it is highly accurate but not precise. It is not calibrated to an atomic clock like DateTime.UtcNow and Environment.TickCount. And is entirely useless to generate events, burning core in a loop to detect elapsed time will get your thread put in the dog house for a while after it burned through its quantum. Only use it for ad hoc profiling.

Hans Passant
  • 922,412
  • 146
  • 1,693
  • 2,536
  • 1
    Your comment on 15.6 msec and using `timeBeginPeriod` is slightly out of date -- beginning with Windows 8, there's the `GetSystemTimePreciseAsFileTime` function, which gives high accuracy system times without the need for mucking about with the multimedia timer functions. NTP makes good use of this. (`DateTime` doesn't use it, however.) – Jeroen Mostert Oct 08 '15 at 12:02
  • @Hans - I don't have hope it will run instantly, I just want it to be eventually correct. I care about actual elapsed time, but I'm working with external hardware that needs to make sure each event in a very long sequence happens for as close to 100 ms as can be provided within the acknowledged limitations, and I can't skip events. And lastly, I'm not burning core in a loop, I'm using a timer WITH a stopwatch that doesn't seem to diverge from elapsed wall clock time, and looking for ways to make it better. – kvermeer Oct 08 '15 at 12:17
  • @Jeroen - That would be great, but I'm still on Windows 7. Looking at moving up to 10 soon, though! – kvermeer Oct 08 '15 at 12:19
  • 1
    In spite of Jeroen's comment, you really are asking for the multi-media timer. It makes an effort to "catch-up" when it falls behind. Within reasonable limits of course. You'll have to pinvoke timeSetEvent(). Realistically, "external hardware" ought to use a driver to get a service guarantee. – Hans Passant Oct 08 '15 at 12:25
  • My comment was a footnote on the precision (not accuracy, sorry about that) of the system time, not a refutation. Whether or not you're using a multimedia timer is orthogonal to that. The idea that you must use `timeBeginPeriod` to be able to obtain precise system time is outdated on newer Windows, that's all. Accuracy is a different kettle of fish. – Jeroen Mostert Oct 08 '15 at 13:03