Skip to content

Commit 023ad3f

Browse files
tannergoodingjkotasCopilot
authored
Use QueryUnbiasedInterruptTime instead of GetTickCount64 to allow more responsive Windows apps when they opt-in (#122706)
Unix systems (Linux and MacOS) were already using timers like `CLOCK_MONOTONIC_COARSE` and `CLOCK_UPTIME_RAW` and so allowed for higher responsiveness. It was only Windows which was restricting itself to 10-16ms (typically 15.5ms) of responsiveness and which did not respect apps which set higher precision tick times. This was also then inconsistent with changes the OS itself made (in Win8+) to its various Wait APIs to no longer include sleep/hibernate time (bias) as part of their timeout checks. Linux will fallback to `CLOCK_MONOTONIC` if `CLOCK_MONOTONIC_COARSE` isn't available. The non-coarse version is closer to `QueryUnbiasedInterruptTimePrecise` which is an "exact" rather than cached query. MacOS doesn't try to use `CLOCK_UPTIME_RAW_APPROX` which is the "coarse" equivalent. --------- Co-authored-by: Jan Kotas <[email protected]> Co-authored-by: Copilot <[email protected]>
1 parent 44e1a20 commit 023ad3f

File tree

10 files changed

+72
-48
lines changed

10 files changed

+72
-48
lines changed

src/coreclr/gc/windows/gcenv.windows.cpp

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1091,7 +1091,21 @@ int64_t GCToOSInterface::QueryPerformanceFrequency()
10911091
// Time stamp in milliseconds
10921092
uint64_t GCToOSInterface::GetLowPrecisionTimeStamp()
10931093
{
1094-
return ::GetTickCount64();
1094+
// GetTickCount64 uses fixed resolution of 10-16ms for backward compatibility. Use
1095+
// QueryUnbiasedInterruptTime instead which becomes more accurate if the underlying system
1096+
// resolution is improved. This helps responsiveness in the case an app is trying to opt
1097+
// into things like multimedia scenarios and additionally does not include "bias" from time
1098+
// the system is spent asleep or in hibernation.
1099+
1100+
const ULONGLONG TicksPerMillisecond = 10000;
1101+
1102+
ULONGLONG unbiasedTime;
1103+
if (!::QueryUnbiasedInterruptTime(&unbiasedTime))
1104+
{
1105+
assert(false && "Failed to query unbiased interrupt time");
1106+
}
1107+
1108+
return (uint64_t)(unbiasedTime / TicksPerMillisecond);
10951109
}
10961110

10971111
// Gets the total number of processors on the machine, not taking

src/libraries/Common/src/Interop/Windows/Kernel32/Interop.QueryUnbiasedInterruptTime.cs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,16 @@ internal static partial class Interop
77
{
88
internal static partial class Kernel32
99
{
10+
// The actual native signature is:
11+
// BOOL WINAPI QueryUnbiasedInterruptTime(
12+
// _Out_ PULONGLONG UnbiasedTime
13+
// );
14+
//
15+
// We take a ulong* (rather than a out ulong) to avoid the pinning overhead.
16+
// We don't set last error since we don't need the extended error info.
17+
1018
[LibraryImport(Libraries.Kernel32)]
11-
[return: MarshalAs(UnmanagedType.Bool)]
12-
internal static partial bool QueryUnbiasedInterruptTime(out ulong UnbiasedTime);
19+
[SuppressGCTransition]
20+
internal static unsafe partial BOOL QueryUnbiasedInterruptTime(ulong* unbiasedTime);
1321
}
1422
}

src/libraries/System.Private.CoreLib/src/System/Environment.Windows.cs

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,27 @@ public static ProcessCpuUsage CpuUsage
377377

378378
/// <summary>Gets the number of milliseconds elapsed since the system started.</summary>
379379
/// <value>A 64-bit signed integer containing the amount of time in milliseconds that has passed since the last time the computer was started.</value>
380-
public static long TickCount64 => (long)Interop.Kernel32.GetTickCount64();
380+
public static long TickCount64
381+
{
382+
get
383+
{
384+
unsafe
385+
{
386+
// GetTickCount64 uses fixed resolution of 10-16ms for backward compatibility. Use
387+
// QueryUnbiasedInterruptTime instead which becomes more accurate if the underlying system
388+
// resolution is improved. This helps responsiveness in the case an app is trying to opt
389+
// into things like multimedia scenarios and additionally does not include "bias" from time
390+
// the system is spent asleep or in hibernation.
391+
392+
ulong unbiasedTime;
393+
394+
Interop.BOOL result = Interop.Kernel32.QueryUnbiasedInterruptTime(&unbiasedTime);
395+
// The P/Invoke is documented to only fail if a null-ptr is passed in
396+
Debug.Assert(result != Interop.BOOL.FALSE);
397+
398+
return (long)(unbiasedTime / TimeSpan.TicksPerMillisecond);
399+
}
400+
}
401+
}
381402
}
382403
}

src/libraries/System.Private.CoreLib/src/System/Threading/Timer.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ internal sealed partial class TimerQueue
4242
{
4343
#region Shared TimerQueue instances
4444
/// <summary>Mapping from a tick count to a time to use when debugging to translate tick count values.</summary>
45-
internal static readonly (long TickCount, DateTime Time) s_tickCountToTimeMap = (TickCount64, DateTime.UtcNow);
45+
internal static readonly (long TickCount, DateTime Time) s_tickCountToTimeMap = (Environment.TickCount64, DateTime.UtcNow);
4646

4747
public static TimerQueue[] Instances { get; } = CreateTimerQueues();
4848

@@ -126,7 +126,7 @@ private bool EnsureTimerFiresBy(uint requestedDuration)
126126

127127
if (_isTimerScheduled)
128128
{
129-
long elapsed = TickCount64 - _currentTimerStartTicks;
129+
long elapsed = Environment.TickCount64 - _currentTimerStartTicks;
130130
if (elapsed >= _currentTimerDuration)
131131
return true; // the timer's about to fire
132132

@@ -138,7 +138,7 @@ private bool EnsureTimerFiresBy(uint requestedDuration)
138138
if (SetTimer(actualDuration))
139139
{
140140
_isTimerScheduled = true;
141-
_currentTimerStartTicks = TickCount64;
141+
_currentTimerStartTicks = Environment.TickCount64;
142142
_currentTimerDuration = actualDuration;
143143
return true;
144144
}
@@ -161,7 +161,7 @@ private bool EnsureTimerFiresBy(uint requestedDuration)
161161

162162
// The current threshold, an absolute time where any timers scheduled to go off at or
163163
// before this time must be queued to the short list.
164-
private long _currentAbsoluteThreshold = TickCount64 + ShortTimersThresholdMilliseconds;
164+
private long _currentAbsoluteThreshold = Environment.TickCount64 + ShortTimersThresholdMilliseconds;
165165

166166
// Default threshold that separates which timers target _shortTimers vs _longTimers. The threshold
167167
// is chosen to balance the number of timers in the small list against the frequency with which
@@ -191,7 +191,7 @@ private void FireNextTimers()
191191
bool haveTimerToSchedule = false;
192192
uint nextTimerDuration = uint.MaxValue;
193193

194-
long nowTicks = TickCount64;
194+
long nowTicks = Environment.TickCount64;
195195

196196
// Sweep through the "short" timers. If the current tick count is greater than
197197
// the current threshold, also sweep through the "long" timers. Finally, as part
@@ -342,7 +342,7 @@ private void FireNextTimers()
342342

343343
public bool UpdateTimer(TimerQueueTimer timer, uint dueTime, uint period)
344344
{
345-
long nowTicks = TickCount64;
345+
long nowTicks = Environment.TickCount64;
346346

347347
// The timer can be put onto the short list if it's next absolute firing time
348348
// is <= the current absolute threshold.

src/libraries/System.Private.CoreLib/src/System/Threading/TimerQueue.Browser.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ namespace System.Threading
1919
//
2020
internal partial class TimerQueue
2121
{
22-
private static long TickCount64 => Environment.TickCount64;
2322
private static List<TimerQueue>? s_scheduledTimers;
2423
private static List<TimerQueue>? s_scheduledTimersToFire;
2524
private static long s_shortestDueTimeMs = long.MaxValue;
@@ -49,7 +48,7 @@ private static void TimerHandler()
4948
// always only have one scheduled at a time
5049
s_shortestDueTimeMs = long.MaxValue;
5150

52-
long currentTimeMs = TickCount64;
51+
long currentTimeMs = Environment.TickCount64;
5352
ReplaceNextTimer(PumpTimerQueue(currentTimeMs), currentTimeMs);
5453
}
5554
catch (Exception e)
@@ -62,7 +61,7 @@ private static void TimerHandler()
6261
private bool SetTimer(uint actualDuration)
6362
{
6463
Debug.Assert((int)actualDuration >= 0);
65-
long currentTimeMs = TickCount64;
64+
long currentTimeMs = Environment.TickCount64;
6665
if (!_isScheduled)
6766
{
6867
s_scheduledTimers ??= new List<TimerQueue>(Instances.Length);

src/libraries/System.Private.CoreLib/src/System/Threading/TimerQueue.Portable.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ private static List<TimerQueue> InitializeScheduledTimerManager_Locked()
5050
private bool SetTimerPortable(uint actualDuration)
5151
{
5252
Debug.Assert((int)actualDuration >= 0);
53-
long dueTimeMs = TickCount64 + (int)actualDuration;
53+
long dueTimeMs = Environment.TickCount64 + (int)actualDuration;
5454
AutoResetEvent timerEvent = s_timerEvent;
5555
Lock timerEventLock = s_timerEventLock;
5656

@@ -92,7 +92,7 @@ private static void TimerThread()
9292
{
9393
timerEvent.WaitOne(shortestWaitDurationMs);
9494

95-
long currentTimeMs = TickCount64;
95+
long currentTimeMs = Environment.TickCount64;
9696
shortestWaitDurationMs = int.MaxValue;
9797
lock (timerEventLock)
9898
{

src/libraries/System.Private.CoreLib/src/System/Threading/TimerQueue.Unix.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@ namespace System.Threading
55
{
66
internal sealed partial class TimerQueue
77
{
8-
public static long TickCount64 => Environment.TickCount64;
9-
108
#pragma warning disable IDE0060
119
private TimerQueue(int id)
1210
{

src/libraries/System.Private.CoreLib/src/System/Threading/TimerQueue.Wasi.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ namespace System.Threading
1818
//
1919
internal sealed partial class TimerQueue
2020
{
21-
private static long TickCount64 => Environment.TickCount64;
2221
private static List<TimerQueue>? s_scheduledTimers;
2322
private static List<TimerQueue>? s_scheduledTimersToFire;
2423
private static long s_shortestDueTimeMs = long.MaxValue;
@@ -36,7 +35,7 @@ private static void TimerHandler(object _)
3635
{
3736
s_shortestDueTimeMs = long.MaxValue;
3837

39-
long currentTimeMs = TickCount64;
38+
long currentTimeMs = Environment.TickCount64;
4039
SetNextTimer(PumpTimerQueue(currentTimeMs), currentTimeMs);
4140
}
4241
catch (Exception e)
@@ -49,7 +48,7 @@ private static void TimerHandler(object _)
4948
private bool SetTimer(uint actualDuration)
5049
{
5150
Debug.Assert((int)actualDuration >= 0);
52-
long currentTimeMs = TickCount64;
51+
long currentTimeMs = Environment.TickCount64;
5352
if (!_isScheduled)
5453
{
5554
s_scheduledTimers ??= new List<TimerQueue>(Instances.Length);

src/libraries/System.Private.CoreLib/src/System/Threading/TimerQueue.Windows.cs

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -12,33 +12,6 @@ private TimerQueue(int id)
1212
_id = id;
1313
}
1414

15-
public static long TickCount64
16-
{
17-
get
18-
{
19-
// We need to keep our notion of time synchronized with the calls to SleepEx that drive
20-
// the underlying native timer. In Win8, SleepEx does not count the time the machine spends
21-
// sleeping/hibernating. Environment.TickCount (GetTickCount) *does* count that time,
22-
// so we will get out of sync with SleepEx if we use that method.
23-
//
24-
// So, on Win8, we use QueryUnbiasedInterruptTime instead; this does not count time spent
25-
// in sleep/hibernate mode.
26-
if (Environment.IsWindows8OrAbove)
27-
{
28-
// Based on its documentation the QueryUnbiasedInterruptTime() function validates
29-
// the argument is non-null. In this case we are always supplying an argument,
30-
// so will skip return value validation.
31-
bool success = Interop.Kernel32.QueryUnbiasedInterruptTime(out ulong time100ns);
32-
Debug.Assert(success);
33-
return (long)(time100ns / 10_000); // convert from 100ns to milliseconds
34-
}
35-
else
36-
{
37-
return Environment.TickCount64;
38-
}
39-
}
40-
}
41-
4215
private bool SetTimer(uint actualDuration) =>
4316
ThreadPool.UseWindowsThreadPool ?
4417
SetTimerWindowsThreadPool(actualDuration) :

src/native/minipal/time.c

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,19 @@ int64_t minipal_hires_tick_frequency()
2929

3030
int64_t minipal_lowres_ticks()
3131
{
32-
return GetTickCount64();
32+
// GetTickCount64 uses fixed resolution of 10-16ms for backward compatibility. Use
33+
// QueryUnbiasedInterruptTime instead which becomes more accurate if the underlying system
34+
// resolution is improved. This helps responsiveness in the case an app is trying to opt
35+
// into things like multimedia scenarios and additionally does not include "bias" from time
36+
// the system is spent asleep or in hibernation.
37+
38+
const ULONGLONG TicksPerMillisecond = 10000;
39+
40+
ULONGLONG unbiasedTime;
41+
BOOL ret;
42+
ret = QueryUnbiasedInterruptTime(&unbiasedTime);
43+
assert(ret); // The function is documented to only fail if a null-ptr is passed in
44+
return (int64_t)(unbiasedTime / TicksPerMillisecond);
3345
}
3446

3547
uint64_t minipal_get_system_time()

0 commit comments

Comments
 (0)