Search code examples
c#winapitimer

How to guarantee high precision timer on Windows 11, even when the App is fully minimized


The timers on Windows run at a rate of 15.6ms per tick. So, even if you set a timer to 1ms, it still takes 15.6ms to complete (this does depend on exactly when the timer was started, but this is the gist of it).

Using timeBeginPeriod(1) (from the Windows API), you can set this to a higher resolution of 1ms.

Unfortunately, on Windows 11, this resolution increase is not guaranteed if a window-owning process becomes minimized, according to the documentation:

Starting with Windows 11, if a window-owning process becomes fully occluded, minimized, or otherwise invisible or inaudible to the end user, Windows does not guarantee a higher resolution than the default system resolution. See SetProcessInformation for more information on this behavior.

Is there a way to guarantee this higher resolution of the timer on Windows 11 onwards, even when the window is minimized? Or is there an alternative to timeBeginPeriod() that can achieve this resolution?

The reason I am asking:

We want to start polling some high precision instruments at an interval of 1ms over a serial port and collect that data, even when the application that is polling is minimized.

Interesting notes:

  • It doesn't have to be exactly 1ms, 1.342ms is also fine, but it should be around there, and not the default timer resolution of 15.6ms.
  • We are using C# and WinUI for the interface
  • Polling is done off the main thread
  • Yes, we are using timeEndPeriod to stop it. :)

I have been looking around for any information on this, but have not found any more details that can shed light on this.


Solution

  • Well, as the timeBeginPeriod documentation now says, we need to use SetProcessInformation and explicitly disable PROCESS_POWER_THROTTLING_IGNORE_TIMER_RESOLUTION.

    To use it in .NET, we need to write some minimal wrappers. (I'm translating it on the fly from VB.NET in which I had to write and test it, so treat it as a basic illustration).

    using System.Runtime.InteropServices;
    using System.Diagnostics;
    
    public class WinNative
    {
        [DllImport("winmm.dll"]
        public static extern UInt32 timeBeginPeriod(UInt32 ms);
    
        [DllImport("kernel32.dll", SetLastError = true)]
        public static extern bool SetProcessInformation(
            IntPtr hProcess,
            int ProcessInformationClass,
            IntPtr ProcessInformation,
            UInt32 ProcessInformationSize
        );
    
        // From processthreadsapi.h (for Win11+); we define only those used
        private const int ProcessPowerThrottling = 4;
        private const UInt32 PROCESS_POWER_THROTTLING_IGNORE_TIMER_RESOLUTION = 4;
    
        [StructLayout(LayoutKind.Sequential)]
        private struct PROCESS_POWER_THROTTLING_STATE
        {
            public UInt32 Version;  // ULONG
            public UInt32 ControlMask;
            public UInt32 StateMask;
        }
    
        public static int DisableThrottling(IntPtr? hProcess = null)
        {
            var sz = Marshal.SizeOf(typeof(PROCESS_POWER_THROTTLING_STATE));
            var PwrInfo = new PROCESS_POWER_THROTTLING_STATE() {
                Version = 1,
                ControlMask = PROCESS_POWER_THROTTLING_IGNORE_TIMER_RESOLUTION,
                StateMask = 0 };  // disable that flag explicitly
            var PwrInfoPtr = Marshal.AllocHGlobal(sz);
            Marshal.StructureToPtr(PwrInfo, PwrInfoPtr, false);
            if (!hProcess.HasValue)
                hProcess = Process.GetCurrentProcess.Handle;
            var r = SetProcessInformation(hProcess.Value, ProcessPowerThrottling, PwrInfoPtr, sz);
            Marshal.FreeHGlobal(PwrInfoPtr);
            return r ? 0 : Marshal.GetLastWin32Error();
            // Note: prior to Win11, the above should return code 87 (invalid parameter).
        }
    
    }
    

    Now, when you need it, you call WinNative.timeBeginPeriod(1) as before, and also WinNative.DisableThrottling() (optionally with the required process handle - by default it will use the current process). Presumably, in any order. When done, just timeEndPeriod(1) should suffice.