Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Observable.Timer(): How to avoid timer drift?

In a C# (.NET 4.0) Application, I use the Reactive Extensions (2.0.20823.0) to generate time boundaries for grouping events into aggregate values. To simplify queries to the resulting database, these boundaries need to be aligned on full hours (or seconds in the example below).

Using Observable.Timer():

var time = DefaultScheduler.Instance;

var start = new DateTimeOffset(time.Now.DateTime, time.Now.Offset);

var span = TimeSpan.FromSeconds(1);

start -= TimeSpan.FromTicks(start.Ticks % 10000000);
start += span;

var boundary = Observable.Timer(start, span, time);

boundary.Select(i => start + TimeSpan.FromSeconds(i * span.TotalSeconds))
    .Subscribe(t => Console.WriteLine("ideal: " + t.ToString("HH:mm:ss.fff")));

boundary.Select(i => time.Now)
    .Subscribe(t => Console.WriteLine("actual: " + t.ToString("HH:mm:ss.fff")));

You can see that the intended and the actual time of the timer ticks drift apart quite heavily:

ideal: 10:06:40.000
actual: 10:06:40.034
actual: 10:06:41.048
ideal: 10:06:41.000
actual: 10:06:42.055
ideal: 10:06:42.000
ideal: 10:06:43.000
actual: 10:06:43.067
actual: 10:06:44.081
ideal: 10:06:44.000
ideal: 10:06:45.000
actual: 10:06:45.095
actual: 10:06:46.109
ideal: 10:06:46.000
ideal: 10:06:47.000
actual: 10:06:47.123
actual: 10:06:48.137
ideal: 10:06:48.000
...

I also make use of a HistoricalScheduler and of course I have no problems there. I can tolerate slight inaccuracies and I do not need to care about system clock changes. There are no heavyweight operations triggered by those Observables.

Also, I know there is a lengthy discussion of RX timer drift problems in this blog post, but I don´t seem to be able to wrap my head around it.

What would be the right way to periodically schedule an Observable without systematic timer drift?

like image 334
bastian schmick Avatar asked Dec 12 '12 10:12

bastian schmick


2 Answers

The default Windows clock interrupt rate on most machines is 64 interrupts per second. Rounded by the CLR to 15.6 milliseconds. That's not a happy number if you ask for an interval of 1000 milliseconds, there is no integral divisor. Closest matches are 64 x 15.6 = 998 (too short) and 65 x 15.6 = 1014 milliseconds.

Which is exactly what you are seeing, 41.048 - 40.034 = 1.014. 44.081 - 43.067 = 1.014, etcetera.

You can actually change the interrupt rate, you can pinvoke timeBeginPeriod() and ask for a 1 millisecond interval. You'll need timeEndPeriod() at program termination to reset it. This is not exactly a very reasonable thing to do, it has system-wide side-effects and is very detrimental to power consumption. But will solve your problem.

A more sane approach is to just acknowledge that you can never keep time accurately by adding up intervals. The 15.6 msec that the CLR uses is already an approximation. Always recalibrate with the absolute clock. Get closer by asking for 998 msec instead of 1000. Etcetera.

like image 172
Hans Passant Avatar answered Oct 23 '22 08:10

Hans Passant


You could use Observable.Generate:

var boundary = Observable.Generate(
    0, _ => true, // start condition
    i => ++i,     // iterate
    i => i,       // result selector
    i => start + TimeSpan.FromSeconds(i * span.TotalSeconds),
    time);

This will reschedule based on absolute time every iteration.

Here's some sample output:

actual: 01:00:44.003
ideal: 01:00:44.000
actual: 01:00:44.999
ideal: 01:00:45.000
actual: 01:00:46.012
ideal: 01:00:46.000
actual: 01:00:47.011
ideal: 01:00:47.000
actual: 01:00:48.011
ideal: 01:00:48.000
actual: 01:00:49.007
ideal: 01:00:49.000
actual: 01:00:50.009
ideal: 01:00:50.000
actual: 01:00:51.006
ideal: 01:00:51.000

It doesn't match exactly, I imagine due to reasons explained by Hans, but there's no drift.

EDIT:

Here's some comments from the RxSource

// BREAKING CHANGE v2 > v1.x - No more correction for time drift based on absolute time. This
//                             didn't work for large period values anyway; the fractional
//                             error exceeded corrections. Also complicated dealing with system
//                             clock change conditions and caused numerous bugs.
//
// - For more precise scheduling, use a custom scheduler that measures TimeSpan values in a
//   better way, e.g. spinning to make up for the last part of the period. Whether or not the
//   values of the TimeSpan period match NT time or wall clock time is up to the scheduler.
//
// - For more accurate scheduling wrt the system clock, use Generate with DateTimeOffset time
//   selectors. When the system clock changes, intervals will not be the same as diffs between
//   consecutive absolute time values. The precision will be low (1s range by default).
like image 12
Matthew Finlay Avatar answered Oct 23 '22 07:10

Matthew Finlay