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..df9927d35f5b89 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 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)]
- [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/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 566c0cbc10dc1a..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,33 +12,6 @@ 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;
- }
- }
- }
-
private bool SetTimer(uint actualDuration) =>
ThreadPool.UseWindowsThreadPool ?
SetTimerWindowsThreadPool(actualDuration) :
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()