2

I have some operations in my application which rely on short timers. Using the example code below I have timers firing every ~5ms as required.

On an Intel i5 10400H CPU the timings are observed to be off, and the callback occurs after ~15ms (or a multiple of 15). Using the ClockRes sysinternals tool shows that the machine has a system timer resolution of 15ms even when run after the call to timeBeginPeriod(1) made in the code below.

Using https://cms.lucashale.com/timer-resolution/ to set the resolution to the maximum supported value (0.5ms) does not change the behaviour of the example code.

From what I can see the machine is using the Invariant TSC acpi timer, and forcing it to use HPET (with bcdedit /set useplatformclock true and rebooting) did not change the behaviour.

I can't see anything in the CPU documentation or errata that would explain this.

I don't know where the problem lies and if it is something that is fixable on my end, any ideas?

Edit: Having this program (DPC Latency Checker) open results in the timer queue firing when expected, so it's solveable.

Example code:

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

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            using (new TimePeriod(1))
                RunTimer();
        }

        public static void RunTimer()
        {
            var completionEvent = new ManualResetEvent(false);
            var stopwatch = Stopwatch.StartNew();
            var i = 0;
            var previous = 0L;
            using var x = TimerQueue.Default.CreateTimer((s) =>
            {
                if (i > 100)
                    completionEvent.Set();
                i++;
                var now = stopwatch.ElapsedMilliseconds;
                var gap = now - previous;
                previous = now;
                Console.WriteLine($"Gap: {gap}ms");
            }, "", 10, 5);
            completionEvent.WaitOne();
        }
    }

    public class TimerQueueTimer : IDisposable
    {
        private TimerQueue MyQueue;
        private TimerCallback Callback;
        private object UserState;
        private IntPtr Handle;

        internal TimerQueueTimer(
            TimerQueue queue,
            TimerCallback cb,
            object state,
            uint dueTime,
            uint period,
            TimerQueueTimerFlags flags)
        {
            MyQueue = queue;
            Callback = cb;
            UserState = state;
            bool rslt = TQTimerWin32.CreateTimerQueueTimer(
                out Handle,
                MyQueue.Handle,
                TimerCallback,
                IntPtr.Zero,
                dueTime,
                period,
                flags);
            if (!rslt)
            {
                throw new Win32Exception(Marshal.GetLastWin32Error(), "Error creating timer.");
            }
        }
        ~TimerQueueTimer()
        {
            Dispose(false);
        }
        public void Change(uint dueTime, uint period)
        {
            bool rslt = TQTimerWin32.ChangeTimerQueueTimer(MyQueue.Handle, ref Handle, dueTime, period);
            if (!rslt)
            {
                throw new Win32Exception(Marshal.GetLastWin32Error(), "Error changing timer.");
            }
        }
        private void TimerCallback(IntPtr state, bool bExpired)
        {
            Callback.Invoke(UserState);
        }
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
        private IntPtr completionEventHandle = new IntPtr(-1);
        public void Dispose(WaitHandle completionEvent)
        {
            completionEventHandle = completionEvent.SafeWaitHandle.DangerousGetHandle();
            this.Dispose();
        }
        private bool disposed = false;
        protected virtual void Dispose(bool disposing)
        {
            if (!disposed)
            {
                bool rslt = TQTimerWin32.DeleteTimerQueueTimer(MyQueue.Handle,
                    Handle, completionEventHandle);
                if (!rslt)
                {
                    throw new Win32Exception(Marshal.GetLastWin32Error(), "Error deleting timer.");
                }
                disposed = true;
            }
        }
    }

    public class TimerQueue : IDisposable
    {
        public IntPtr Handle { get; private set; }
        public static TimerQueue Default { get; private set; }
        static TimerQueue()
        {
            Default = new TimerQueue(IntPtr.Zero);
        }
        private TimerQueue(IntPtr handle)
        {
            Handle = handle;
        }
        public TimerQueue()
        {
            Handle = TQTimerWin32.CreateTimerQueue();
            if (Handle == IntPtr.Zero)
            {
                throw new Win32Exception(Marshal.GetLastWin32Error(), "Error creating timer queue.");
            }
        }
        ~TimerQueue()
        {
            Dispose(false);
        }
        public TimerQueueTimer CreateTimer(
            TimerCallback callback,
            object state,
            uint dueTime,
            uint period)
        {
            return CreateTimer(callback, state, dueTime, period, TimerQueueTimerFlags.ExecuteInPersistentThread);
        }

        public TimerQueueTimer CreateTimer(
            TimerCallback callback,
            object state,
            uint dueTime,
            uint period,
            TimerQueueTimerFlags flags)
        {
            return new TimerQueueTimer(this, callback, state, dueTime, period, flags);
        }

        private IntPtr CompletionEventHandle = new IntPtr(-1);

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        public void Dispose(WaitHandle completionEvent)
        {
            CompletionEventHandle = completionEvent.SafeWaitHandle.DangerousGetHandle();
            Dispose();
        }

        private bool Disposed = false;

        protected virtual void Dispose(bool disposing)
        {
            if (!Disposed)
            {
                if (Handle != IntPtr.Zero)
                {
                    bool rslt = TQTimerWin32.DeleteTimerQueueEx(Handle, CompletionEventHandle);
                    if (!rslt)
                    {
                        int err = Marshal.GetLastWin32Error();
                        throw new Win32Exception(err, "Error disposing timer queue");
                    }
                }
                Disposed = true;
            }
        }
    }

    public enum TimerQueueTimerFlags : uint
    {
        ExecuteDefault = 0x0000,
        ExecuteInTimerThread = 0x0020,
        ExecuteInIoThread = 0x0001,
        ExecuteInPersistentThread = 0x0080,
        ExecuteLongFunction = 0x0010,
        ExecuteOnlyOnce = 0x0008,
        TransferImpersonation = 0x0100,
    }

    public delegate void Win32WaitOrTimerCallback(
        IntPtr lpParam,
        [MarshalAs(UnmanagedType.U1)] bool bTimedOut);

    static public class TQTimerWin32
    {
        [DllImport("kernel32.dll", SetLastError = true)]
        public extern static IntPtr CreateTimerQueue();

        [DllImport("kernel32.dll", SetLastError = true)]
        public extern static bool DeleteTimerQueue(IntPtr timerQueue);

        [DllImport("kernel32.dll", SetLastError = true)]
        public extern static bool DeleteTimerQueueEx(IntPtr timerQueue, IntPtr completionEvent);

        [DllImport("kernel32.dll", SetLastError = true)]
        public extern static bool CreateTimerQueueTimer(
            out IntPtr newTimer,
            IntPtr timerQueue,
            Win32WaitOrTimerCallback callback,
            IntPtr userState,
            uint dueTime,
            uint period,
            TimerQueueTimerFlags flags);

        [DllImport("kernel32.dll", SetLastError = true)]
        public extern static bool ChangeTimerQueueTimer(
            IntPtr timerQueue,
            ref IntPtr timer,
            uint dueTime,
            uint period);

        [DllImport("kernel32.dll", SetLastError = true)]
        public extern static bool DeleteTimerQueueTimer(
            IntPtr timerQueue,
            IntPtr timer,
            IntPtr completionEvent);
    }

    public sealed class TimePeriod : IDisposable
    {
        private const string WINMM = "winmm.dll";

        private static TIMECAPS timeCapabilities;

        private static int inTimePeriod;

        private readonly int period;

        private int disposed;

        [DllImport(WINMM, ExactSpelling = true)]
        private static extern int timeGetDevCaps(ref TIMECAPS ptc, int cbtc);

        [DllImport(WINMM, ExactSpelling = true)]
        private static extern int timeBeginPeriod(int uPeriod);

        [DllImport(WINMM, ExactSpelling = true)]
        private static extern int timeEndPeriod(int uPeriod);

        static TimePeriod()
        {
            int result = timeGetDevCaps(ref timeCapabilities, Marshal.SizeOf(typeof(TIMECAPS)));
            if (result != 0)
            {
                throw new InvalidOperationException("The request to get time capabilities was not completed because an unexpected error with code " + result + " occured.");
            }
        }

        internal TimePeriod(int period)
        {
            if (Interlocked.Increment(ref inTimePeriod) != 1)
            {
                Interlocked.Decrement(ref inTimePeriod);
                throw new NotSupportedException("The process is already within a time period. Nested time periods are not supported.");
            }

            if (period < timeCapabilities.wPeriodMin || period > timeCapabilities.wPeriodMax)
            {
                throw new ArgumentOutOfRangeException("period", "The request to begin a time period was not completed because the resolution specified is out of range.");
            }

            int result = timeBeginPeriod(period);
            if (result != 0)
            {
                throw new InvalidOperationException("The request to begin a time period was not completed because an unexpected error with code " + result + " occured.");
            }

            this.period = period;
        }

        internal static int MinimumPeriod
        {
            get
            {
                return timeCapabilities.wPeriodMin;
            }
        }

        internal static int MaximumPeriod
        {
            get
            {
                return timeCapabilities.wPeriodMax;
            }
        }

        internal int Period
        {
            get
            {
                if (this.disposed > 0)
                {
                    throw new ObjectDisposedException("The time period instance has been disposed.");
                }

                return this.period;
            }
        }

        public void Dispose()
        {
            if (Interlocked.Increment(ref this.disposed) == 1)
            {
                timeEndPeriod(this.period);
                Interlocked.Decrement(ref inTimePeriod);
            }
            else
            {
                Interlocked.Decrement(ref this.disposed);
            }
        }

        [StructLayout(LayoutKind.Sequential)]
        private struct TIMECAPS
        {
            internal int wPeriodMin;

            internal int wPeriodMax;
        }
    }
}

John
  • 67
  • 1
  • 9
  • It is probably due to the motherboard driver. Check from vendor of you have latest. See following : https://forums.tomshardware.com/threads/windows-timer-resolution-got-variable-now.3540488/ – jdweng Jul 13 '20 at 10:31
  • 5ms is pretty tight for a multi-process O/S. On projects with similar requirements I've worked on before (industrial control etc), we had to specify the exact MB/Proc to be used and tested many combinations that had to be rejected. – Neil Jul 13 '20 at 10:35
  • The machine with issues is a Dell Latitude 5511 laptop, the Dell update software claims that all drivers are up to date. Windows has all updates There are quite a few different models of machine currently in use and no other one has issues with the timing when not being used for other tasks (which is SOP). – John Jul 13 '20 at 10:55

3 Answers3

3

This seem to be an issue with windows 10 2004. I would guess that it has nothing to do with the processor/motherboard.

A possible workaround might be to use a stopwatch and spinwait on a thread. This would be inadvisable for regular consumer applications since it would consume a full thread, but might be feasible if you have full control of the system.

JonasH
  • 28,608
  • 2
  • 10
  • 23
1

I encountered the exact same problem under Windows 10 2004. Previous versions did not seem to exhibit the same behavior. CreateTimerQueueTimer does not seem to honor timeBeginPeriod anymore and its minimum period seems to be 15ms (good old 15 ms...).

There are a few people complaining about this problem around, but not a lot. (see this forum entry for example. I do not know if this is a bug introduced in v2004, or a power-saving "feature" that got sneaked past us. That being said, official documentation never linked TimerQueueTimers and timeBeginPeriod, so if might have been a bug to begin with that they honored the timeBeginPeriod setting.

In any case, I ended up re-implementing a TimerQueue on top of timeBeginPeriod/timeSetEvent to achieve the required timer frequency.

Denis Troller
  • 7,411
  • 1
  • 23
  • 36
  • Isn't `timeSetEvent` suffering from the same issue? – Acorn Oct 06 '20 at 09:21
  • 1
    No it is not. This is why I think this is a "correction". If you look at the documentation for TimerQueues, it does not indicate any linke to the time* APIs, especially not timeBeginPeriod/timeEndPeriod. It might be that TimerQueues being affected was a by-product of implementation, and it has been "corrected" in 2004 for power consumption purposes. – Denis Troller Oct 07 '20 at 11:40
  • Thanks, that is very useful to know. So they are still waking up the kernel to honor `timeSetEvent` but they now avoid to reschedule many of the processes that previously were. Hopefully they document the change somewhere, even if informally. – Acorn Oct 07 '20 at 18:15
  • Again, this is just a gut feeling. It might just be a plain bug, god knows we've had our share of these with Windows 10... But I ave not seen any acknowledgment of the problem one way or another... – Denis Troller Oct 08 '20 at 14:16
0

Running into the same problem, I'm using CreateTimerQueueTimer. What still works is timeSetEvent. You'll loose some precision as it's in whole milliseconds, but it's better than nothing.

The Cookies Dog
  • 1,915
  • 2
  • 22
  • 38