12

I have an application that uses a System.Timers.Timer object to raise events that are processed by the main form (Windows Forms, C#). My problem is that no matter how short I set the .Interval (even to 1 ms) I get a max of 64 times per second.

I know the Forms timer has a 55 ms accuracy limit, but this is the System.Timer variant, not the Forms one.

The application sits a 1% CPU, so it's definitely not CPU-bound. So all it's doing is:

  • Set the Timer to 1&nsp;ms
  • When the event fires, increment a _Count variable
  • Set it to 1&nsp;ms again and repeat

_Count gets incremented a maximum of 64 times a second even when there's no other work to do.

This is an "playback" application that has to replicate packets coming in with as little as 1-2 ms delay between them, so I need something that can reliably fire 1000 times a second or so (though I'd settle for 100 if I was CPU bound, I'm not).

Any thoughts?

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Dave
  • 1,521
  • 17
  • 31
  • BTW, I removed all communication between the event handler and the main form (just for testing) to make sure I wasn't stuck in some message queue. Made no difference. – Dave Nov 22 '12 at 23:54
  • try `System.Threading.Timer` and see if it works. – Serdalis Nov 22 '12 at 23:57
  • IMHO not the best approach. Try running at full speed and using delays to slow down. – Simon Nov 23 '12 at 09:37
  • 1
    55 msec was the olden MS-Dos clock interrupt rate. Windows ticks at 64 interrupts per second. timeBeginPeriod to change that. – Hans Passant Nov 23 '12 at 11:12
  • 1
    All .NET timers have this limitation - `System.Timers.Timer`, `System.Threading.Timer`, and `Windows.Forms.Timer`; see [this question](http://stackoverflow.com/questions/19571739/system-timers-timer-net-really-slow) – darda Oct 24 '13 at 17:36

3 Answers3

4

Try Multimedia Timers - they provide greatest accuracy possible for the hardware platform. These timers schedule events at a higher resolution than other timer services.

You will need following Win API functions to set timer resolution, start and stop timer:

[DllImport("winmm.dll")]
private static extern int timeGetDevCaps(ref TimerCaps caps, int sizeOfTimerCaps);

[DllImport("winmm.dll")]
private static extern int timeSetEvent(int delay, int resolution, TimeProc proc, int user, int mode);

[DllImport("winmm.dll")]
private static extern int timeKillEvent(int id);

You also need callback delegate:

delegate void TimeProc(int id, int msg, int user, int param1, int param2);

And timer capabilities structure

[StructLayout(LayoutKind.Sequential)]
public struct TimerCaps
{
    public int periodMin;
    public int periodMax;
}

Usage:

TimerCaps caps = new TimerCaps();
// provides min and max period 
timeGetDevCaps(ref caps, Marshal.SizeOf(caps));
int period = 1;
int resolution = 1;
int mode = 0; // 0 for periodic, 1 for single event
timeSetEvent(period, resolution, new TimeProc(TimerCallback), 0, mode);

And callback:

void TimerCallback(int id, int msg, int user, int param1, int param2)
{
    // occurs every 1 ms
}
Sergey Berezovskiy
  • 232,247
  • 41
  • 429
  • 459
  • 1
    The better granularity is not a feature of the timeSetEvent when compared to standard timer calls. The systems interrupt period will be changed to operate at 1 ms with the code here. However, when this is in use, all timer functions will have 1 ms granularity. Thus you just have to [set the timer resolution](http://msdn.microsoft.com/en-us/library/windows/desktop/dd743626%28v=vs.85%29.aspx) to maximum (minimum period) to let all timers operate at the level. – Arno Nov 23 '12 at 09:13
  • 1
    FYI the multimedia timers are not the most accurate - the High Performance Timer is. I have used multimedia timers ticking at 1 kHz and on occasion you'll have one that goes way off (bad for control loops). HPT is "dead stable" compared to multimedia timers. – darda Oct 24 '13 at 17:23
  • @HansPassant `Threading.Timer` does not care how you set timeBeginPeriod. [It continues to ticks at 15.6ms intervals even when timeBeginPeriod is set to 1ms](http://stackoverflow.com/q/16160179/709537). – Evgeniy Berezovsky Mar 31 '15 at 05:08
3

You can stick to your design. You only need to set the system interrupt frequency to run at its maximum frequency. In order to obtain this, you just have to execute the following code anywhere in your code:

#define TARGET_RESOLUTION 1         // 1-millisecond target resolution

TIMECAPS tc;
UINT     wTimerRes;

if (timeGetDevCaps(&tc, sizeof(TIMECAPS)) != TIMERR_NOERROR) 
{
    // Error; application can't continue.
}

wTimerRes = min(max(tc.wPeriodMin, TARGET_RESOLUTION), tc.wPeriodMax);
timeBeginPeriod(wTimerRes); 

This will force the systems interrupt period to run at maximum frequency. It is a system wide behavior, thus it may even be done in a separate process. Don't forget to use

MMRESULT timeEndPeriod(wTimerRes );

when done to release the resource and reset the interrupt period to default. See Multimedia Timers for details.

You must match each call to timeBeginPeriod with a call to timeEndPeriod, specifying the same minimum resolution in both calls. An application can make multiple timeBeginPeriod calls as long as each call is matched with a call to timeEndPeriod.

As a consequence, all timers (including your current design) will operate at higher frequency since the granularity of the timers will improve. A granularity of 1 ms can be obtained on most hardware.

Here is a list of interrupt periods obtained with various settings of wTimerRes for two different hardware setups (A+B):

ActualResolution (interrupt period) vs. setting of wTimerRes

It can easily be seen that the 1 ms is a theoretical value. ActualResolution is given in 100 ns units. 9,766 represents 0.9766 ms which is 1024 interrupts per second. (In fact it should be 0.9765625 which would be 9,7656.25 100 ns units, but that accuracy obviously does not fit into an integer and is therefore rounded by the system.)

It also becomes obvious that i.g. platform A does not really support all the range of periods returned by timeGetDevCaps (values ranging between wPeriodMinand wPeriodMin).

Summary: The multimedia timer interface can be used to modify the interrupt frequency system wide. As a consequence all timers will change their granularity. Also the system time update will change accordingly, it will increment more often and in smaller steps. But: The actual behavior depends on the underlaying hardware. This hardware dependency has gotten a lot smaller since the introduction of Windows 7 and Windows 8 since newer schemes for timing have been introduced.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Arno
  • 4,994
  • 3
  • 39
  • 63
  • Just hard-code timeBeginPeriod(1), there isn't any Windows machine left that doesn't support it. – Hans Passant Nov 23 '12 at 11:05
  • @HansPassant: Could try that but told the full story for the sake of perfectionism. But the `timeEndPeriod` shall not be forgotten about. – Arno Nov 23 '12 at 11:12
  • I added the code and single-stepped to verify that I am now calling timeBeginPeriod with a value of 1, and that it returns TIMERR_NOERROR. However, I am still seeing 64 interrupts per sec. To be clear, does timeBeginPeriod affect System.Timers.Timer? For whatever reason, it seems to succeed but have no effect. – Dave Nov 23 '12 at 20:18
  • @Dave: Interesting result. Did `timeGetDevCaps` return a `.PeriodMin` of 1? Please go forward an check whether the interrupt period has changed. Simply test the `system file time` increment by using `GetSystemTimeAsFileTime(...)` in a loop. See [this](http://stackoverflow.com/a/11743614/1504523) SO answer to get the code and the description. A system file time increment would be 15.625 ms when only 64 interrupts per second occur. – Arno Nov 24 '12 at 16:57
  • System interrupt frequency? Doesn't that require administrative rights to change? – Peter Mortensen Feb 28 '14 at 00:33
  • @PeterMortensen: The [timeBeginPeriod function](http://msdn.microsoft.com/en-us/library/windows/desktop/dd757624%28v=vs.85%29.aspx) does it, no admin rights needed for this. See [Timer Resolution](http://msdn.microsoft.com/en-us/library/windows/desktop/dd757633%28v=vs.85%29.aspx) for more details. – Arno Feb 28 '14 at 08:13
0

Based on the other solutions and comments, I put together this VB.NET code. Can be pasted into a project with a form. I understood @HansPassant's comments as saying that as long as timeBeginPeriod is called, the "regular timers get accurate as well". This doesn't seem to be the case in my code.

My code creates a multimedia timer, a System.Threading.Timer, a System.Timers.Timer, and a Windows.Forms.Timer after using timeBeginPeriod to set the timer resolution to the minimum. The multimedia timer runs at 1 kHz as required, but the others still are stuck at 64 Hz. So either I'm doing something wrong, or there's no way to change the resolution of the built-in .NET timers.

EDIT; changed the code to use the StopWatch class for timing.

Imports System.Runtime.InteropServices
Public Class Form1

    'From http://www.pinvoke.net/default.aspx/winmm/MMRESULT.html
    Private Enum MMRESULT
        MMSYSERR_NOERROR = 0
        MMSYSERR_ERROR = 1
        MMSYSERR_BADDEVICEID = 2
        MMSYSERR_NOTENABLED = 3
        MMSYSERR_ALLOCATED = 4
        MMSYSERR_INVALHANDLE = 5
        MMSYSERR_NODRIVER = 6
        MMSYSERR_NOMEM = 7
        MMSYSERR_NOTSUPPORTED = 8
        MMSYSERR_BADERRNUM = 9
        MMSYSERR_INVALFLAG = 10
        MMSYSERR_INVALPARAM = 11
        MMSYSERR_HANDLEBUSY = 12
        MMSYSERR_INVALIDALIAS = 13
        MMSYSERR_BADDB = 14
        MMSYSERR_KEYNOTFOUND = 15
        MMSYSERR_READERROR = 16
        MMSYSERR_WRITEERROR = 17
        MMSYSERR_DELETEERROR = 18
        MMSYSERR_VALNOTFOUND = 19
        MMSYSERR_NODRIVERCB = 20
        WAVERR_BADFORMAT = 32
        WAVERR_STILLPLAYING = 33
        WAVERR_UNPREPARED = 34
    End Enum

    'http://msdn.microsoft.com/en-us/library/windows/desktop/dd757625(v=vs.85).aspx
    <StructLayout(LayoutKind.Sequential)>
    Public Structure TIMECAPS
        Public periodMin As UInteger
        Public periodMax As UInteger
    End Structure

    'http://msdn.microsoft.com/en-us/library/windows/desktop/dd757627(v=vs.85).aspx
    <DllImport("winmm.dll")>
    Private Shared Function timeGetDevCaps(ByRef ptc As TIMECAPS, ByVal cbtc As UInteger) As MMRESULT
    End Function

    'http://msdn.microsoft.com/en-us/library/windows/desktop/dd757624(v=vs.85).aspx
    <DllImport("winmm.dll")>
    Private Shared Function timeBeginPeriod(ByVal uPeriod As UInteger) As MMRESULT
    End Function

    'http://msdn.microsoft.com/en-us/library/windows/desktop/dd757626(v=vs.85).aspx
    <DllImport("winmm.dll")>
    Private Shared Function timeEndPeriod(ByVal uPeriod As UInteger) As MMRESULT
    End Function

    'http://msdn.microsoft.com/en-us/library/windows/desktop/ff728861(v=vs.85).aspx
    Private Delegate Sub TIMECALLBACK(ByVal uTimerID As UInteger, _
                                  ByVal uMsg As UInteger, _
                                  ByVal dwUser As IntPtr, _
                                  ByVal dw1 As IntPtr, _
                                  ByVal dw2 As IntPtr)

    'Straight from C:\Program Files (x86)\Microsoft SDKs\Windows\v7.1A\Include\MMSystem.h
    'fuEvent below is a combination of these flags.
    Private Const TIME_ONESHOT As UInteger = 0
    Private Const TIME_PERIODIC As UInteger = 1
    Private Const TIME_CALLBACK_FUNCTION As UInteger = 0
    Private Const TIME_CALLBACK_EVENT_SET As UInteger = &H10
    Private Const TIME_CALLBACK_EVENT_PULSE As UInteger = &H20
    Private Const TIME_KILL_SYNCHRONOUS As UInteger = &H100

    'http://msdn.microsoft.com/en-us/library/windows/desktop/dd757634(v=vs.85).aspx
    'Documentation is self-contradicting. The return value is Uinteger, I'm guessing.
    '"Returns an identifier for the timer event if successful or an error otherwise. 
    'This function returns NULL if it fails and the timer event was not created."
    <DllImport("winmm.dll")>
    Private Shared Function timeSetEvent(ByVal uDelay As UInteger, _
                                         ByVal uResolution As UInteger, _
                                         ByVal TimeProc As TIMECALLBACK, _
                                         ByVal dwUser As IntPtr, _
                                         ByVal fuEvent As UInteger) As UInteger
    End Function

    'http://msdn.microsoft.com/en-us/library/windows/desktop/dd757630(v=vs.85).aspx
    <DllImport("winmm.dll")>
    Private Shared Function timeKillEvent(ByVal uTimerID As UInteger) As MMRESULT
    End Function

    Private lblRate As New Windows.Forms.Label
    Private WithEvents tmrUI As New Windows.Forms.Timer
    Private WithEvents tmrWorkThreading As New System.Threading.Timer(AddressOf TimerTick)
    Private WithEvents tmrWorkTimers As New System.Timers.Timer
    Private WithEvents tmrWorkForm As New Windows.Forms.Timer

    Public Sub New()
        lblRate.AutoSize = True
        Me.Controls.Add(lblRate)

        InitializeComponent()
    End Sub

    Private Capability As New TIMECAPS

    Private Sub Form1_FormClosing(sender As Object, e As System.Windows.Forms.FormClosingEventArgs) Handles Me.FormClosing
        timeKillEvent(dwUser)
        timeEndPeriod(Capability.periodMin)
    End Sub

    Private dwUser As UInteger = 0
    Private Clock As New System.Diagnostics.Stopwatch
    Private Sub Form1_Load(sender As System.Object, e As System.EventArgs) _
        Handles MyBase.Load

        Dim Result As MMRESULT

        'Get the min and max period
        Result = timeGetDevCaps(Capability, Marshal.SizeOf(Capability))
        If Result <> MMRESULT.MMSYSERR_NOERROR Then
            MsgBox("timeGetDevCaps returned " + Result.ToString)
            Exit Sub
        End If

        'Set to the minimum period.
        Result = timeBeginPeriod(Capability.periodMin)
        If Result <> MMRESULT.MMSYSERR_NOERROR Then
            MsgBox("timeBeginPeriod returned " + Result.ToString)
            Exit Sub
        End If

        Clock.Start()

        Dim uTimerID As UInteger
        uTimerID = timeSetEvent(Capability.periodMin, Capability.periodMin, _
                     New TIMECALLBACK(AddressOf MMCallBack), dwUser, _
                     TIME_PERIODIC Or TIME_CALLBACK_FUNCTION Or TIME_KILL_SYNCHRONOUS)
        If uTimerID = 0 Then
            MsgBox("timeSetEvent not successful.")
            Exit Sub
        End If

        tmrWorkThreading.Change(0, 1)

        tmrWorkTimers.Interval = 1
        tmrWorkTimers.Enabled = True

        tmrWorkForm.Interval = 1
        tmrWorkForm.Enabled = True

        tmrUI.Interval = 100
        tmrUI.Enabled = True
    End Sub

    Private CounterThreading As Integer = 0
    Private CounterTimers As Integer = 0
    Private CounterForms As Integer = 0
    Private CounterMM As Integer = 0

    Private ReadOnly TimersLock As New Object
    Private Sub tmrWorkTimers_Elapsed(sender As Object, e As System.Timers.ElapsedEventArgs) _
        Handles tmrWorkTimers.Elapsed
        SyncLock TimersLock
            CounterTimers += 1
        End SyncLock
    End Sub

    Private ReadOnly ThreadingLock As New Object
    Private Sub TimerTick()
        SyncLock ThreadingLock
            CounterThreading += 1
        End SyncLock
    End Sub

    Private ReadOnly MMLock As New Object
    Private Sub MMCallBack(ByVal uTimerID As UInteger, _
                                  ByVal uMsg As UInteger, _
                                  ByVal dwUser As IntPtr, _
                                  ByVal dw1 As IntPtr, _
                                  ByVal dw2 As IntPtr)
        SyncLock MMLock
            CounterMM += 1
        End SyncLock
    End Sub

    Private ReadOnly FormLock As New Object
    Private Sub tmrWorkForm_Tick(sender As Object, e As System.EventArgs) Handles tmrWorkForm.Tick
        SyncLock FormLock
            CounterForms += 1
        End SyncLock
    End Sub

    Private Sub tmrUI_Tick(sender As Object, e As System.EventArgs) _
    Handles tmrUI.Tick
        Dim Secs As Integer = Clock.Elapsed.TotalSeconds
        If Secs > 0 Then
            Dim TheText As String = ""
            TheText += "System.Threading.Timer " + (CounterThreading / Secs).ToString("#,##0.0") + "Hz" + vbCrLf
            TheText += "System.Timers.Timer " + (CounterTimers / Secs).ToString("#,##0.0") + "Hz" + vbCrLf
            TheText += "Windows.Forms.Timer " + (CounterForms / Secs).ToString("#,##0.0") + "Hz" + vbCrLf
            TheText += "Multimedia Timer " + (CounterMM / Secs).ToString("#,##0.0") + "Hz"
            lblRate.Text = TheText
        End If
    End Sub

End Class
darda
  • 3,597
  • 6
  • 36
  • 49