Windows Timer Queue Timer continues after .NET breakpoint

Spacy

I developed a .NET application that has to poll a pressure device with 120 Hz. The standard .NET timers only seem to achieve up to 60 Hz, so I decided to use the Win32 CreateTimerQueueTimer API via PInvoke. It works nicely, but the debugging experience is very bad because the timers are even fired when I'm stepping through the program while the program is on hold. I wrote a minimal example in C and in C# and the undesired behavior only occurs on C#. The C program does not create the timer callback threads while the debugger paused the program. Can anyone tell me, what I can do to achieve the same debugging behavior in C#?

C code:

#include <stdio.h>
#include <assert.h>
#include <Windows.h>

int counter = 0;

VOID NTAPI callback(PVOID lpParameter, BOOLEAN TimerOrWaitFired)
{
    printf("Just in time %i\n", counter++);
}

int main()
{
    HANDLE timer;
    BOOL success = CreateTimerQueueTimer(&timer, NULL, callback, NULL, 0, 1000, WT_EXECUTEDEFAULT);
    assert(FALSE != success); // set breakpoint at this line and wait 10 seconds
    Sleep(1000);
    success = DeleteTimerQueueTimer(NULL, timer, NULL); // step to this line
    assert(FALSE != success);
    return 0;
}

C result

Equivalent C# code:

using System;
using System.Runtime.InteropServices;

class TimerQueue
{
    delegate void WAITORTIMERCALLBACK(IntPtr lpParameter, bool TimerOrWaitFired);

    [DllImport("kernel32.dll", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    static extern bool CreateTimerQueueTimer(
        out IntPtr phNewTimer,
        IntPtr TimerQueue,
        WAITORTIMERCALLBACK Callback,
        IntPtr Parameter,
        uint DueTime,
        uint Period,
        uint Flags);

    [DllImport("kernel32.dll", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    static extern bool DeleteTimerQueueTimer(
        IntPtr TimerQueue,
        IntPtr Timer,
        IntPtr CompletionEvent);

    static int counter = 0;

    static void Callback(IntPtr lpParameter, bool TimerOrWaitFired)
    {
        Console.WriteLine("Just in time {0}", counter++);
    }

    static void Main(string[] args)
    {
        WAITORTIMERCALLBACK callbackWrapper = Callback;
        IntPtr timer;
        bool success = CreateTimerQueueTimer(out timer, IntPtr.Zero, callbackWrapper, IntPtr.Zero, 0, 1000, 0);
        System.Diagnostics.Debug.Assert(false != success); // set breakpoint at this line and wait 10 seconds
        System.Threading.Thread.Sleep(1000);
        success = DeleteTimerQueueTimer(IntPtr.Zero, timer, IntPtr.Zero); // step to this line
        System.Diagnostics.Debug.Assert(false != success);
    }
}

C# result

By the way, I know there is a race condition when using the unprotected counter variable from multiple threads, that's not important right now.

The sleep for one second is meant independent from waiting after the breakpoint is hit and seems to be necessary because the callbacks are not queued immediately on the process even when stepping the program in a debugger but only after a short delay.

The call to DeleteTimerQueueTimer is not really necessary to show my problem because it occurs before this line is executed.

Spacy

I see I asked this question 4 years ago and I think I know the answer now: The timer classes in .NET internally only use one-shot timers. So when you pause the program at a breakpoint, the code that re-sets the one-shot timer does not run so there will be no callbacks piling up until you let the program/debugger continue.

Several months ago I wrote my own Timer class that can reach callback frequencies up to 1000 Hz. It uses a combination of setting one-shot timers repeatedly and busy waiting. Busy waiting burns CPU cycles but helps to reduce jitter introduced by the Windows thread scheduler.

Here is the code:

using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading;

/// <summary>
/// Executes a function periodically.
/// Timer expirations are not queued during debug breaks or
/// if the function takes longer than the period.
/// </summary>
/// <remarks>
/// Uses a Windows one-shot timer together
/// with busy waiting to achieve good accuracy
/// at reasonable CPU usage.
/// Works even on Windows 10 Version 2004.
/// </remarks>
public sealed class Timer : IDisposable
{
    /// <summary>
    /// Statistics:
    /// How much time was 'wasted' for busy waiting.
    /// Does not include time passed by using a system timer.
    /// </summary>
    public TimeSpan TimeSpentWithBusyWaiting => new TimeSpan(Interlocked.Read(ref timeSpentWithBusyWaiting));
    private long timeSpentWithBusyWaiting = 0;

    private readonly TimeCaps timeCaps;
    private readonly Func<bool> function;
    private readonly TimeSpan period;
    /// <summary>
    /// We must keep a reference to this callback so that it does not get garbage collected.
    /// </summary>
    private readonly TIMECALLBACK callback;
    private readonly Stopwatch stopwatch = new Stopwatch();
    private volatile bool stopTimer = false;
    private ManualResetEvent timerStopped = new ManualResetEvent(false);
    private TimeSpan desiredNextTime;

    /// <summary>
    /// Immediately starts the timer.
    /// </summary>
    /// <param name="function">
    /// What to do after each <paramref name="period"/>.
    /// The timer will stop if the function returns <see langword="false"/>.
    /// </param>
    /// <param name="period">
    /// How long to wait between executing each <paramref name="function"/>.
    /// </param>
    public Timer(Func<bool> function, TimeSpan period)
    {
        uint? timerDelay = TimeSpanToPositiveMillisecondsWithRoundingDown(period);
        if (timerDelay == null)
        {
            throw new ArgumentOutOfRangeException(nameof(period));
        }

        uint timeGetDevCapsErrorCode = TimeGetDevCaps(out timeCaps, (uint)Marshal.SizeOf(typeof(TimeCaps)));
        if (timeGetDevCapsErrorCode != 0)
        {
            throw new Exception($"{nameof(TimeGetDevCaps)} returned error code {timeGetDevCapsErrorCode}");
        }
        Debug.Assert(timeCaps.wPeriodMin >= 1);

        this.function = function ?? throw new ArgumentNullException(nameof(function));
        this.period = period;
        callback = new TIMECALLBACK(OnTimerExpired);

        TimeBeginPeriod(timeCaps.wPeriodMin);

        stopwatch.Start();

        Schedule(desiredNextTime = period, forceTimer: true);
    }

    /// <summary>
    /// Does not cancel the timer and instead
    /// blocks until the function passed to
    /// the constructor returns <see langword="false"/>.
    /// </summary>
    public void WaitUntilFinished()
    {
        if (timerStopped != null)
        {
            timerStopped.WaitOne();
            timerStopped.Dispose();
            timerStopped = null;
            TimeEndPeriod(timeCaps.wPeriodMin);
        }
    }

    /// <summary>
    /// Stops timer and blocks until the
    /// last invocation of the function has finished.
    /// </summary>
    public void Dispose()
    {
        stopTimer = true;
        WaitUntilFinished();
    }

    private void OnTimerExpired(
        uint uTimerID,
        uint uMsg,
        UIntPtr dwUser,
        UIntPtr dw1,
        UIntPtr dw2)
    {
        while (!stopTimer)
        {
            TimeSpan startOfBusyWaiting = stopwatch.Elapsed;
            TimeSpan endOfBusyWaiting = desiredNextTime;
            TimeSpan timeThatWillBeSpentWithBusyWaiting = endOfBusyWaiting - startOfBusyWaiting;
            if (timeThatWillBeSpentWithBusyWaiting > TimeSpan.Zero)
            {
                Interlocked.Add(ref timeSpentWithBusyWaiting, timeThatWillBeSpentWithBusyWaiting.Ticks);
            }

            if (desiredNextTime > stopwatch.Elapsed)
            {
                while (desiredNextTime > stopwatch.Elapsed)
                {
                    // busy waiting until time has arrived
                }
                desiredNextTime += period;
            }
            else
            {
                // we are too slow
                desiredNextTime = stopwatch.Elapsed + period;
            }

            bool continueTimer = function();
            if (continueTimer)
            {
                if (Schedule(desiredNextTime, forceTimer: false))
                {
                    return;
                }
            }
            else
            {
                stopTimer = true;
            }
        }
        timerStopped.Set();
    }

    /// <param name="desiredNextTime">
    /// Desired absolute time for next execution of function.
    /// </param>
    /// <param name="forceTimer">
    /// If <see langword="true"/>, a one-shot timer will be used even if
    /// <paramref name="desiredNextTime"/> is in the past or too close to
    /// the system timer resolution.
    /// </param>
    /// <returns>
    /// <see langword="true"/> if timer was set or <paramref name="forceTimer"/> was <see langword="true"/>.
    /// <see langword="false"/> if <paramref name="desiredNextTime"/> was in the past.
    /// </returns>
    /// <remarks>
    /// Increases accuracy by scheduling the timer a little earlier and
    /// then do busy waiting outside of this function.
    /// </remarks>
    private bool Schedule(TimeSpan desiredNextTime, bool forceTimer)
    {
        TimeSpan currentTime = stopwatch.Elapsed;
        TimeSpan remainingTimeUntilNextExecution = desiredNextTime - currentTime;
        uint? timerDelay = TimeSpanToPositiveMillisecondsWithRoundingDown(remainingTimeUntilNextExecution);
        timerDelay =
            timerDelay < timeCaps.wPeriodMin ? timeCaps.wPeriodMin :
            timerDelay > timeCaps.wPeriodMax ? timeCaps.wPeriodMax :
            timerDelay;
        if (forceTimer && timerDelay == null)
        {
            timerDelay = timeCaps.wPeriodMin;
        }
        if (forceTimer || timerDelay >= timeCaps.wPeriodMin)
        {
            // wait until next execution using a one-shot timer
            uint timerHandle = TimeSetEvent(
                timerDelay.Value,
                timeCaps.wPeriodMin,
                callback,
                UIntPtr.Zero,
                0);
            if (timerHandle == 0)
            {
                throw new Exception($"{nameof(TimeSetEvent)} failed");
            }
            return true;
        }
        else // use busy waiting
        {
            return false;
        }
    }

    /// <returns><see langword="null"/> if <paramref name="timeSpan"/> is negative</returns>
    private static uint? TimeSpanToPositiveMillisecondsWithRoundingDown(TimeSpan timeSpan)
    {
        if (timeSpan.Ticks >= 0)
        {
            long milliseconds = timeSpan.Ticks / TimeSpan.TicksPerMillisecond;
            if (milliseconds <= uint.MaxValue)
            {
                return unchecked((uint)milliseconds);
            }
        }
        return null;
    }

    private delegate void TIMECALLBACK(
        uint uTimerID,
        uint uMsg,
        UIntPtr dwUser,
        UIntPtr dw1,
        UIntPtr dw2);

    // https://docs.microsoft.com/en-us/previous-versions//dd757634(v=vs.85)
    //
    // This is the only timer API that seems to work for frequencies
    // higher than 60 Hz on Windows 10 Version 2004.
    //
    // The uResolution parameter has the same effect as
    // using the timeBeginPeriod API, so it can be observed
    // by entering `powercfg.exe /energy /duration 1` into
    // a command prompt with administrator privileges.
    [DllImport("winmm", EntryPoint = "timeSetEvent")]
    private static extern uint TimeSetEvent(
        uint uDelay,
        uint uResolution,
        TIMECALLBACK lpTimeProc,
        UIntPtr dwUser,
        uint fuEvent);

    [DllImport("winmm", EntryPoint = "timeBeginPeriod")]
    private static extern uint TimeBeginPeriod(
        uint uPeriod);

    [DllImport("winmm", EntryPoint = "timeEndPeriod")]
    private static extern uint TimeEndPeriod(
        uint uPeriod);

    [DllImport("winmm", EntryPoint = "timeGetDevCaps")]
    private static extern uint TimeGetDevCaps(
        out TimeCaps ptc,
        uint cbtc);

    [StructLayout(LayoutKind.Sequential)]
    private struct TimeCaps
    {
        public uint wPeriodMin;
        public uint wPeriodMax;
    }
}

Collected from the Internet

Please contact [email protected] to delete if infringement.

edited at
0

Comments

0 comments
Login to comment

Related