Mono High Resolution Timer (On Linux)

C# Timer resolution: Linux (mono, dotnet core) vs Windows

Unfortunately you cannot rely on timers in the .NET framework. The best one has 15 ms frequency even if you want to trigger it in every millisecond. But you can implement a high-resolution timer with microsec precision, too.

Note: This works only when Stopwatch.IsHighResolution returns true. In Windows this is true starting with Windows XP; however, I did not test other frameworks.

public class HiResTimer
{
// The number of ticks per one millisecond.
private static readonly float tickFrequency = 1000f / Stopwatch.Frequency;

public event EventHandler<HiResTimerElapsedEventArgs> Elapsed;

private volatile float interval;
private volatile bool isRunning;

public HiResTimer() : this(1f)
{
}

public HiResTimer(float interval)
{
if (interval < 0f || Single.IsNaN(interval))
throw new ArgumentOutOfRangeException(nameof(interval));
this.interval = interval;
}

// The interval in milliseconds. Fractions are allowed so 0.001 is one microsecond.
public float Interval
{
get { return interval; }
set
{
if (value < 0f || Single.IsNaN(value))
throw new ArgumentOutOfRangeException(nameof(value));
interval = value;
}
}

public bool Enabled
{
set
{
if (value)
Start();
else
Stop();
}
get { return isRunning; }
}

public void Start()
{
if (isRunning)
return;

isRunning = true;
Thread thread = new Thread(ExecuteTimer);
thread.Priority = ThreadPriority.Highest;
thread.Start();
}

public void Stop()
{
isRunning = false;
}

private void ExecuteTimer()
{
float nextTrigger = 0f;

Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();

while (isRunning)
{
float intervalLocal = interval;
nextTrigger += intervalLocal;
float elapsed;

while (true)
{
elapsed = ElapsedHiRes(stopwatch);
float diff = nextTrigger - elapsed;
if (diff <= 0f)
break;

if (diff < 1f)
Thread.SpinWait(10);
else if (diff < 10f)
Thread.SpinWait(100);
else
{
// By default Sleep(1) lasts about 15.5 ms (if not configured otherwise for the application by WinMM, for example)
// so not allowing sleeping under 16 ms. Not sleeping for more than 50 ms so interval changes/stopping can be detected.
if (diff >= 16f)
Thread.Sleep(diff >= 100f ? 50 : 1);
else
{
Thread.SpinWait(1000);
Thread.Sleep(0);
}

// if we have a larger time to wait, we check if the interval has been changed in the meantime
float newInterval = interval;

if (intervalLocal != newInterval)
{
nextTrigger += newInterval - intervalLocal;
intervalLocal = newInterval;
}
}

if (!isRunning)
return;
}


float delay = elapsed - nextTrigger;
if (delay >= ignoreElapsedThreshold)
{
fallouts += 1;
continue;
}

Elapsed?.Invoke(this, new HiResTimerElapsedEventArgs(delay, fallouts));
fallouts = 0;

// restarting the timer in every hour to prevent precision problems
if (stopwatch.Elapsed.TotalHours >= 1d)
{
stopwatch.Restart();
nextTrigger = 0f;
}
}

stopwatch.Stop();
}

private static float ElapsedHiRes(Stopwatch stopwatch)
{
return stopwatch.ElapsedTicks * tickFrequency;
}
}

public class HiResTimerElapsedEventArgs : EventArgs
{
public float Delay { get; }

internal HiResTimerElapsedEventArgs(float delay)
{
Delay = delay;
}
}

Edit 2021: Using the latest version that does not have the issue @hankd mentions in the comments.

Precise and reliable step timing in C# .NET/Mono

Meanwhile, I was able to largely solve the issue.

First off, I stand corrected on the CPU usage of the timers I referenced in the question. CPU usage was due to my own code, where I used a tight while loop.

Having found that, I was able to solve the issue by using two timers, and check for the type of environment during runtime, to decide which one to actually use. To check the environment, I use:

private static readonly bool IsPosixEnvironment = Path.DirectorySeparatorChar == '/';

which is typically true under Linux.

Now, it is possible to use two different timers, for example this one for Windows, and this one for linux, as follows:

if (IsPosixEnvironment)
{
_linTimer = new PosixHiPrecTimer();
_linTimer.Tick += LinTimerElapsed;
_linTimer.Interval = _stepsize;
_linTimer.Enabled = true;
}
else
{
_winTimer = new WinHiPrecTimer();
_winTimer.Elapsed += WinTimerElapsed;
_winTimer.Interval = _stepsize;
_winTimer.Resolution = 25;
_winTimer.Start();
}

So far, this has given me good results; the step size is ussually in the 99-101 ms range, with the interval set to 100 ms. Also, and even more importantly for my purposes, there are no more longer intervals.

On a slower system (Raspberry Pi 1st gen model B), I still got occasional longer intervals, but I'd have to check overall effeciency first before drawing a conclusion there.

There is also this timer, which works out of the box under both operating systems. In a test program, compared to the one linked previously, this one caused a higher CPU load under Linux with Mono.

High resolution timer

I found a solution to this problem in the following blog:
http://web.archive.org/web/20110910100053/http://www.indigo79.net/archives/27#comment-255

It tells you how to use the multimedia timer to have a timer with high frequency. It is working just fine for me!!!

High resolution timer in C#

There is nothing built into the .NET framework that I am aware of. Windows has a mechanism for high resolution timer events via the Multimedia Timer API. Below is a quick example I whipped up which seems to do the job. There are also seems to be a good example here.

I will note that this API changes system wide settings that can degrade system performance, so buyer beware. For testing purposes, I would recommend keeping track of how often the timer is firing to verify the timing is similar to the device you are trying to simulate. Since windows is not a real-time OS, the load on your system may cause the MM timer be delayed resulting in gaps of 100 ms that contain 100 events in quick succession, rather than 100 events spaced 1 ms apart. Some additional reading on MM timers.

class Program
{
static void Main(string[] args)
{
TestThreadingTimer();
TestMultimediaTimer();
}

private static void TestMultimediaTimer()
{
Stopwatch s = new Stopwatch();
using (var timer = new MultimediaTimer() { Interval = 1 })
{
timer.Elapsed += (o, e) => Console.WriteLine(s.ElapsedMilliseconds);
s.Start();
timer.Start();
Console.ReadKey();
timer.Stop();
}
}

private static void TestThreadingTimer()
{
Stopwatch s = new Stopwatch();
using (var timer = new Timer(o => Console.WriteLine(s.ElapsedMilliseconds), null, 0, 1))
{
s.Start();
Console.ReadKey();
}
}

}

public class MultimediaTimer : IDisposable
{
private bool disposed = false;
private int interval, resolution;
private UInt32 timerId;

// Hold the timer callback to prevent garbage collection.
private readonly MultimediaTimerCallback Callback;

public MultimediaTimer()
{
Callback = new MultimediaTimerCallback(TimerCallbackMethod);
Resolution = 5;
Interval = 10;
}

~MultimediaTimer()
{
Dispose(false);
}

public int Interval
{
get
{
return interval;
}
set
{
CheckDisposed();

if (value < 0)
throw new ArgumentOutOfRangeException("value");

interval = value;
if (Resolution > Interval)
Resolution = value;
}
}

// Note minimum resolution is 0, meaning highest possible resolution.
public int Resolution
{
get
{
return resolution;
}
set
{
CheckDisposed();

if (value < 0)
throw new ArgumentOutOfRangeException("value");

resolution = value;
}
}

public bool IsRunning
{
get { return timerId != 0; }
}

public void Start()
{
CheckDisposed();

if (IsRunning)
throw new InvalidOperationException("Timer is already running");

// Event type = 0, one off event
// Event type = 1, periodic event
UInt32 userCtx = 0;
timerId = NativeMethods.TimeSetEvent((uint)Interval, (uint)Resolution, Callback, ref userCtx, 1);
if (timerId == 0)
{
int error = Marshal.GetLastWin32Error();
throw new Win32Exception(error);
}
}

public void Stop()
{
CheckDisposed();

if (!IsRunning)
throw new InvalidOperationException("Timer has not been started");

StopInternal();
}

private void StopInternal()
{
NativeMethods.TimeKillEvent(timerId);
timerId = 0;
}

public event EventHandler Elapsed;

public void Dispose()
{
Dispose(true);
}

private void TimerCallbackMethod(uint id, uint msg, ref uint userCtx, uint rsv1, uint rsv2)
{
var handler = Elapsed;
if (handler != null)
{
handler(this, EventArgs.Empty);
}
}

private void CheckDisposed()
{
if (disposed)
throw new ObjectDisposedException("MultimediaTimer");
}

private void Dispose(bool disposing)
{
if (disposed)
return;

disposed = true;
if (IsRunning)
{
StopInternal();
}

if (disposing)
{
Elapsed = null;
GC.SuppressFinalize(this);
}
}
}

internal delegate void MultimediaTimerCallback(UInt32 id, UInt32 msg, ref UInt32 userCtx, UInt32 rsv1, UInt32 rsv2);

internal static class NativeMethods
{
[DllImport("winmm.dll", SetLastError = true, EntryPoint = "timeSetEvent")]
internal static extern UInt32 TimeSetEvent(UInt32 msDelay, UInt32 msResolution, MultimediaTimerCallback callback, ref UInt32 userCtx, UInt32 eventType);

[DllImport("winmm.dll", SetLastError = true, EntryPoint = "timeKillEvent")]
internal static extern void TimeKillEvent(UInt32 uTimerId);
}

Difference between CLOCK_REALTIME and CLOCK_MONOTONIC?

CLOCK_REALTIME represents the machine's best-guess as to the current wall-clock, time-of-day time. As Ignacio and MarkR say, this means that CLOCK_REALTIME can jump forwards and backwards as the system time-of-day clock is changed, including by NTP.

CLOCK_MONOTONIC represents the absolute elapsed wall-clock time since some arbitrary, fixed point in the past. It isn't affected by changes in the system time-of-day clock.

If you want to compute the elapsed time between two events observed on the one machine without an intervening reboot, CLOCK_MONOTONIC is the best option.

Note that on Linux, CLOCK_MONOTONIC does not measure time spent in suspend, although by the POSIX definition it should. You can use the Linux-specific CLOCK_BOOTTIME for a monotonic clock that keeps running during suspend.

Linux alternative to Windows high-resolution performance counter API

See clock_gettime() with CLOCK_MONOTONIC_RAW flag, and clock_getres().

Here is also an example of how to use it:

  • stopwatch.h
  • stopwatch.c
  • stopwatch_example.c


Related Topics



Leave a reply



Submit