From 3b42f2e30731147afab0e3c9786ac157d54d56ba Mon Sep 17 00:00:00 2001 From: Tanner Gooding Date: Mon, 22 Dec 2025 11:25:49 -0800 Subject: [PATCH 1/3] Use QueryUnbiasedInterruptTime instead of GetTickCount64 to allow more responsive Windows apps when they opt-in --- src/coreclr/gc/windows/gcenv.windows.cpp | 16 ++++++++++- .../Interop.QueryUnbiasedInterruptTime.cs | 12 +++++++-- .../src/System/Environment.Windows.cs | 23 +++++++++++++++- .../System/Threading/TimerQueue.Windows.cs | 27 +------------------ src/native/minipal/time.c | 14 +++++++++- 5 files changed, 61 insertions(+), 31 deletions(-) diff --git a/src/coreclr/gc/windows/gcenv.windows.cpp b/src/coreclr/gc/windows/gcenv.windows.cpp index 8285e71d354aaf..bc95e6648deb8e 100644 --- a/src/coreclr/gc/windows/gcenv.windows.cpp +++ b/src/coreclr/gc/windows/gcenv.windows.cpp @@ -1091,7 +1091,21 @@ int64_t GCToOSInterface::QueryPerformanceFrequency() // Time stamp in milliseconds uint64_t GCToOSInterface::GetLowPrecisionTimeStamp() { - return ::GetTickCount64(); + // GetTickCount64 uses fixed resolution of 10-16ms for backward compatibility. Use + // QueryUnbiasedInterruptTime instead which becomes more accurate if the underlying system + // resolution is improved. This helps responsiveness in the case an app is trying to opt + // into things like multimedia scenarios and additionally does not include "bias" from time + // the system is spent asleep or in hibernation. + + const ULONGLONG TicksPerMillisecond = 10000; + + ULONGLONG unbiasedTime; + if (!::QueryUnbiasedInterruptTime(&unbiasedTime)) + { + assert(false && "Failed to query unbiased interrupt time"); + } + + return (uint64_t)(unbiasedTime / TicksPerMillisecond); } // Gets the total number of processors on the machine, not taking diff --git a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.QueryUnbiasedInterruptTime.cs b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.QueryUnbiasedInterruptTime.cs index fd2cb681088b12..7bb8cc959230ee 100644 --- a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.QueryUnbiasedInterruptTime.cs +++ b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.QueryUnbiasedInterruptTime.cs @@ -7,8 +7,16 @@ internal static partial class Interop { internal static partial class Kernel32 { + // The actual native signature is: + // BOOL WINAPI QueryUnbiasedInterruptTime( + // _Out_ PULONGLONG UnbiasedTime + // ); + // + // We take a long* (rather than a out long) to avoid the pinning overhead. + // We don't set last error since we don't need the extended error info. + [LibraryImport(Libraries.Kernel32)] - [return: MarshalAs(UnmanagedType.Bool)] - internal static partial bool QueryUnbiasedInterruptTime(out ulong UnbiasedTime); + [SuppressGCTransition] + internal static unsafe partial BOOL QueryUnbiasedInterruptTime(ulong* unbiasedTime); } } diff --git a/src/libraries/System.Private.CoreLib/src/System/Environment.Windows.cs b/src/libraries/System.Private.CoreLib/src/System/Environment.Windows.cs index a84d9218acb1d6..2f251a11a2970e 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Environment.Windows.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Environment.Windows.cs @@ -377,6 +377,27 @@ public static ProcessCpuUsage CpuUsage /// Gets the number of milliseconds elapsed since the system started. /// A 64-bit signed integer containing the amount of time in milliseconds that has passed since the last time the computer was started. - public static long TickCount64 => (long)Interop.Kernel32.GetTickCount64(); + public static long TickCount64 + { + get + { + unsafe + { + // GetTickCount64 uses fixed resolution of 10-16ms for backward compatibility. Use + // QueryUnbiasedInterruptTime instead which becomes more accurate if the underlying system + // resolution is improved. This helps responsiveness in the case an app is trying to opt + // into things like multimedia scenarios and additionally does not include "bias" from time + // the system is spent asleep or in hibernation. + + ulong unbiasedTime; + + Interop.BOOL result = Interop.Kernel32.QueryUnbiasedInterruptTime(&unbiasedTime); + // The P/Invoke is documented to only fail if a null-ptr is passed in + Debug.Assert(result != Interop.BOOL.FALSE); + + return (long)(unbiasedTime / TimeSpan.TicksPerMillisecond); + } + } + } } } diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/TimerQueue.Windows.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/TimerQueue.Windows.cs index 566c0cbc10dc1a..ccd5978158f7fc 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/TimerQueue.Windows.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/TimerQueue.Windows.cs @@ -12,32 +12,7 @@ private TimerQueue(int id) _id = id; } - public static long TickCount64 - { - get - { - // We need to keep our notion of time synchronized with the calls to SleepEx that drive - // the underlying native timer. In Win8, SleepEx does not count the time the machine spends - // sleeping/hibernating. Environment.TickCount (GetTickCount) *does* count that time, - // so we will get out of sync with SleepEx if we use that method. - // - // So, on Win8, we use QueryUnbiasedInterruptTime instead; this does not count time spent - // in sleep/hibernate mode. - if (Environment.IsWindows8OrAbove) - { - // Based on its documentation the QueryUnbiasedInterruptTime() function validates - // the argument is non-null. In this case we are always supplying an argument, - // so will skip return value validation. - bool success = Interop.Kernel32.QueryUnbiasedInterruptTime(out ulong time100ns); - Debug.Assert(success); - return (long)(time100ns / 10_000); // convert from 100ns to milliseconds - } - else - { - return Environment.TickCount64; - } - } - } + public static long TickCount64 => Environment.TickCount64; private bool SetTimer(uint actualDuration) => ThreadPool.UseWindowsThreadPool ? diff --git a/src/native/minipal/time.c b/src/native/minipal/time.c index 4b3a989fd495b0..26ee0a7613cd9c 100644 --- a/src/native/minipal/time.c +++ b/src/native/minipal/time.c @@ -29,7 +29,19 @@ int64_t minipal_hires_tick_frequency() int64_t minipal_lowres_ticks() { - return GetTickCount64(); + // GetTickCount64 uses fixed resolution of 10-16ms for backward compatibility. Use + // QueryUnbiasedInterruptTime instead which becomes more accurate if the underlying system + // resolution is improved. This helps responsiveness in the case an app is trying to opt + // into things like multimedia scenarios and additionally does not include "bias" from time + // the system is spent asleep or in hibernation. + + const ULONGLONG TicksPerMillisecond = 10000; + + ULONGLONG unbiasedTime; + BOOL ret; + ret = QueryUnbiasedInterruptTime(&unbiasedTime); + assert(ret); // The function is documented to only fail if a null-ptr is passed in + return (int64_t)(unbiasedTime / TicksPerMillisecond); } uint64_t minipal_get_system_time() From 0a9fcad9a89dc145b478cf8eb83fc77f99da6aa2 Mon Sep 17 00:00:00 2001 From: Jan Kotas Date: Tue, 23 Dec 2025 00:46:57 -0800 Subject: [PATCH 2/3] Update src/libraries/Common/src/Interop/Windows/Kernel32/Interop.QueryUnbiasedInterruptTime.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../Windows/Kernel32/Interop.QueryUnbiasedInterruptTime.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.QueryUnbiasedInterruptTime.cs b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.QueryUnbiasedInterruptTime.cs index 7bb8cc959230ee..df9927d35f5b89 100644 --- a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.QueryUnbiasedInterruptTime.cs +++ b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.QueryUnbiasedInterruptTime.cs @@ -12,7 +12,7 @@ internal static partial class Kernel32 // _Out_ PULONGLONG UnbiasedTime // ); // - // We take a long* (rather than a out long) to avoid the pinning overhead. + // We take a ulong* (rather than a out ulong) to avoid the pinning overhead. // We don't set last error since we don't need the extended error info. [LibraryImport(Libraries.Kernel32)] From 3eb20f786698b5086808ce28490f38e29e7ee8b4 Mon Sep 17 00:00:00 2001 From: Tanner Gooding Date: Tue, 23 Dec 2025 07:13:06 -0800 Subject: [PATCH 3/3] Use Environment.TickCount64 directly from System.Threading.Timer and TimerQueue --- .../src/System/Threading/Timer.cs | 12 ++++++------ .../src/System/Threading/TimerQueue.Browser.cs | 5 ++--- .../src/System/Threading/TimerQueue.Portable.cs | 4 ++-- .../src/System/Threading/TimerQueue.Unix.cs | 2 -- .../src/System/Threading/TimerQueue.Wasi.cs | 5 ++--- .../src/System/Threading/TimerQueue.Windows.cs | 2 -- 6 files changed, 12 insertions(+), 18 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/Timer.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/Timer.cs index c9747d2cbdec55..19d152d6153bd4 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/Timer.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/Timer.cs @@ -42,7 +42,7 @@ internal sealed partial class TimerQueue { #region Shared TimerQueue instances /// Mapping from a tick count to a time to use when debugging to translate tick count values. - internal static readonly (long TickCount, DateTime Time) s_tickCountToTimeMap = (TickCount64, DateTime.UtcNow); + internal static readonly (long TickCount, DateTime Time) s_tickCountToTimeMap = (Environment.TickCount64, DateTime.UtcNow); public static TimerQueue[] Instances { get; } = CreateTimerQueues(); @@ -126,7 +126,7 @@ private bool EnsureTimerFiresBy(uint requestedDuration) if (_isTimerScheduled) { - long elapsed = TickCount64 - _currentTimerStartTicks; + long elapsed = Environment.TickCount64 - _currentTimerStartTicks; if (elapsed >= _currentTimerDuration) return true; // the timer's about to fire @@ -138,7 +138,7 @@ private bool EnsureTimerFiresBy(uint requestedDuration) if (SetTimer(actualDuration)) { _isTimerScheduled = true; - _currentTimerStartTicks = TickCount64; + _currentTimerStartTicks = Environment.TickCount64; _currentTimerDuration = actualDuration; return true; } @@ -161,7 +161,7 @@ private bool EnsureTimerFiresBy(uint requestedDuration) // The current threshold, an absolute time where any timers scheduled to go off at or // before this time must be queued to the short list. - private long _currentAbsoluteThreshold = TickCount64 + ShortTimersThresholdMilliseconds; + private long _currentAbsoluteThreshold = Environment.TickCount64 + ShortTimersThresholdMilliseconds; // Default threshold that separates which timers target _shortTimers vs _longTimers. The threshold // is chosen to balance the number of timers in the small list against the frequency with which @@ -191,7 +191,7 @@ private void FireNextTimers() bool haveTimerToSchedule = false; uint nextTimerDuration = uint.MaxValue; - long nowTicks = TickCount64; + long nowTicks = Environment.TickCount64; // Sweep through the "short" timers. If the current tick count is greater than // the current threshold, also sweep through the "long" timers. Finally, as part @@ -342,7 +342,7 @@ private void FireNextTimers() public bool UpdateTimer(TimerQueueTimer timer, uint dueTime, uint period) { - long nowTicks = TickCount64; + long nowTicks = Environment.TickCount64; // The timer can be put onto the short list if it's next absolute firing time // is <= the current absolute threshold. diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/TimerQueue.Browser.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/TimerQueue.Browser.cs index db03ddf5731934..4593a39441f261 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/TimerQueue.Browser.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/TimerQueue.Browser.cs @@ -19,7 +19,6 @@ namespace System.Threading // internal partial class TimerQueue { - private static long TickCount64 => Environment.TickCount64; private static List? s_scheduledTimers; private static List? s_scheduledTimersToFire; private static long s_shortestDueTimeMs = long.MaxValue; @@ -49,7 +48,7 @@ private static void TimerHandler() // always only have one scheduled at a time s_shortestDueTimeMs = long.MaxValue; - long currentTimeMs = TickCount64; + long currentTimeMs = Environment.TickCount64; ReplaceNextTimer(PumpTimerQueue(currentTimeMs), currentTimeMs); } catch (Exception e) @@ -62,7 +61,7 @@ private static void TimerHandler() private bool SetTimer(uint actualDuration) { Debug.Assert((int)actualDuration >= 0); - long currentTimeMs = TickCount64; + long currentTimeMs = Environment.TickCount64; if (!_isScheduled) { s_scheduledTimers ??= new List(Instances.Length); diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/TimerQueue.Portable.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/TimerQueue.Portable.cs index f125dcb4b4de3d..4d2195b2096df7 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/TimerQueue.Portable.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/TimerQueue.Portable.cs @@ -50,7 +50,7 @@ private static List InitializeScheduledTimerManager_Locked() private bool SetTimerPortable(uint actualDuration) { Debug.Assert((int)actualDuration >= 0); - long dueTimeMs = TickCount64 + (int)actualDuration; + long dueTimeMs = Environment.TickCount64 + (int)actualDuration; AutoResetEvent timerEvent = s_timerEvent; Lock timerEventLock = s_timerEventLock; @@ -92,7 +92,7 @@ private static void TimerThread() { timerEvent.WaitOne(shortestWaitDurationMs); - long currentTimeMs = TickCount64; + long currentTimeMs = Environment.TickCount64; shortestWaitDurationMs = int.MaxValue; lock (timerEventLock) { diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/TimerQueue.Unix.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/TimerQueue.Unix.cs index 1f8dadaae8df29..886a8bf535a9bf 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/TimerQueue.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/TimerQueue.Unix.cs @@ -5,8 +5,6 @@ namespace System.Threading { internal sealed partial class TimerQueue { - public static long TickCount64 => Environment.TickCount64; - #pragma warning disable IDE0060 private TimerQueue(int id) { diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/TimerQueue.Wasi.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/TimerQueue.Wasi.cs index d5af01ca588caf..1517d9b11d45ea 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/TimerQueue.Wasi.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/TimerQueue.Wasi.cs @@ -18,7 +18,6 @@ namespace System.Threading // internal sealed partial class TimerQueue { - private static long TickCount64 => Environment.TickCount64; private static List? s_scheduledTimers; private static List? s_scheduledTimersToFire; private static long s_shortestDueTimeMs = long.MaxValue; @@ -36,7 +35,7 @@ private static void TimerHandler(object _) { s_shortestDueTimeMs = long.MaxValue; - long currentTimeMs = TickCount64; + long currentTimeMs = Environment.TickCount64; SetNextTimer(PumpTimerQueue(currentTimeMs), currentTimeMs); } catch (Exception e) @@ -49,7 +48,7 @@ private static void TimerHandler(object _) private bool SetTimer(uint actualDuration) { Debug.Assert((int)actualDuration >= 0); - long currentTimeMs = TickCount64; + long currentTimeMs = Environment.TickCount64; if (!_isScheduled) { s_scheduledTimers ??= new List(Instances.Length); diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/TimerQueue.Windows.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/TimerQueue.Windows.cs index ccd5978158f7fc..7030da7a0cbd58 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/TimerQueue.Windows.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/TimerQueue.Windows.cs @@ -12,8 +12,6 @@ private TimerQueue(int id) _id = id; } - public static long TickCount64 => Environment.TickCount64; - private bool SetTimer(uint actualDuration) => ThreadPool.UseWindowsThreadPool ? SetTimerWindowsThreadPool(actualDuration) :