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();
}
}
}