diff --git a/src/libraries/System.Private.CoreLib/src/ILLink/ILLink.Descriptors.Shared.xml b/src/libraries/System.Private.CoreLib/src/ILLink/ILLink.Descriptors.Shared.xml
index 3fe9dc5f861b..cc65319f28a2 100644
--- a/src/libraries/System.Private.CoreLib/src/ILLink/ILLink.Descriptors.Shared.xml
+++ b/src/libraries/System.Private.CoreLib/src/ILLink/ILLink.Descriptors.Shared.xml
@@ -16,6 +16,9 @@
+
+
+
diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/CancellationTokenSource.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/CancellationTokenSource.cs
index 4b03075d5a9b..ebd38dd1afa4 100644
--- a/src/libraries/System.Private.CoreLib/src/System/Threading/CancellationTokenSource.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/Threading/CancellationTokenSource.cs
@@ -30,11 +30,9 @@ public class CancellationTokenSource : IDisposable
internal static readonly CancellationTokenSource s_neverCanceledSource = new CancellationTokenSource();
/// Delegate used with to trigger cancellation of a .
- private static readonly TimerCallback s_timerCallback = obj =>
- {
- Debug.Assert(obj is CancellationTokenSource, $"Expected {typeof(CancellationTokenSource)}, got {obj}");
- ((CancellationTokenSource)obj).NotifyCancellation(throwOnFirstException: false); // skip ThrowIfDisposed() check in Cancel()
- };
+ private static readonly TimerCallback s_timerCallback = TimerCallback;
+ private static void TimerCallback(object? state) => // separated out into a named method to improve Timer diagnostics in a debugger
+ ((CancellationTokenSource)state!).NotifyCancellation(throwOnFirstException: false); // skip ThrowIfDisposed() check in Cancel()
/// The current state of the CancellationTokenSource.
private volatile int _state;
diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs
index 5006e6e92020..8f396bc7c805 100644
--- a/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs
@@ -5433,6 +5433,7 @@ public static Task Delay(int millisecondsDelay, CancellationToken cancellationTo
/// Task that also stores the completion closure and logic for Task.Delay implementation.
private class DelayPromise : Task
{
+ private static readonly TimerCallback s_timerCallback = TimerCallback;
private readonly TimerQueueTimer? _timer;
internal DelayPromise(uint millisecondsDelay)
@@ -5447,7 +5448,7 @@ internal DelayPromise(uint millisecondsDelay)
if (millisecondsDelay != Timeout.UnsignedInfinite) // no need to create the timer if it's an infinite timeout
{
- _timer = new TimerQueueTimer(state => ((DelayPromise)state!).CompleteTimedOut(), this, millisecondsDelay, Timeout.UnsignedInfinite, flowExecutionContext: false);
+ _timer = new TimerQueueTimer(s_timerCallback, this, millisecondsDelay, Timeout.UnsignedInfinite, flowExecutionContext: false);
if (IsCompleted)
{
// Handle rare race condition where the timer fires prior to our having stored it into the field, in which case
@@ -5458,6 +5459,9 @@ internal DelayPromise(uint millisecondsDelay)
}
}
+ // Separated out into a named method to improve Timer diagnostics in a debugger
+ private static void TimerCallback(object? state) => ((DelayPromise)state!).CompleteTimedOut();
+
private void CompleteTimedOut()
{
if (TrySetResult())
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 64184db16f3c..72ec6a7276a0 100644
--- a/src/libraries/System.Private.CoreLib/src/System/Threading/Timer.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/Threading/Timer.cs
@@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Tracing;
@@ -34,9 +35,13 @@ namespace System.Threading
// Note that all instance methods of this class require that the caller hold a lock on the TimerQueue instance.
// We partition the timers across multiple TimerQueues, each with its own lock and set of short/long lists,
// in order to minimize contention when lots of threads are concurrently creating and destroying timers often.
+ [DebuggerDisplay("Count = {CountForDebugger}")]
+ [DebuggerTypeProxy(typeof(TimerQueueDebuggerTypeProxy))]
internal 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);
public static TimerQueue[] Instances { get; } = CreateTimerQueues();
@@ -50,6 +55,52 @@ private static TimerQueue[] CreateTimerQueues()
return queues;
}
+ // This method is not thread-safe and should only be used from the debugger.
+ private int CountForDebugger
+ {
+ get
+ {
+ int count = 0;
+ foreach (TimerQueueTimer _ in GetTimersForDebugger())
+ {
+ count++;
+ }
+
+ return count;
+ }
+ }
+
+ // This method is not thread-safe and should only be used from the debugger.
+ internal IEnumerable GetTimersForDebugger()
+ {
+ // This should ideally take lock(this), but doing so can hang the debugger
+ // if another thread holds the lock. It could instead use Monitor.TryEnter,
+ // but doing so doesn't work while dump debugging. So, it doesn't take the
+ // lock at all; it's theoretically possible but very unlikely this could result
+ // in a circular list that causes the debugger to hang, too.
+
+ for (TimerQueueTimer? timer = _shortTimers; timer != null; timer = timer._next)
+ {
+ yield return timer;
+ }
+
+ for (TimerQueueTimer? timer = _longTimers; timer != null; timer = timer._next)
+ {
+ yield return timer;
+ }
+ }
+
+ private sealed class TimerQueueDebuggerTypeProxy
+ {
+ private readonly TimerQueue _queue;
+
+ public TimerQueueDebuggerTypeProxy(TimerQueue queue) =>
+ _queue = queue ?? throw new ArgumentNullException(nameof(queue));
+
+ [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
+ public TimerQueueTimer[] Items => new List(_queue.GetTimersForDebugger()).ToArray();
+ }
+
#endregion
#region interface to native timer
@@ -386,6 +437,8 @@ public void DeleteTimer(TimerQueueTimer timer)
}
// A timer in our TimerQueue.
+ [DebuggerDisplay("{DisplayString,nq}")]
+ [DebuggerTypeProxy(typeof(TimerDebuggerTypeProxy))]
internal sealed partial class TimerQueueTimer : IThreadPoolWorkItem
{
// The associated timer queue.
@@ -425,7 +478,6 @@ internal sealed partial class TimerQueueTimer : IThreadPoolWorkItem
private bool _canceled;
private object? _notifyWhenNoCallbacksRunning; // may be either WaitHandle or Task
-
internal TimerQueueTimer(TimerCallback timerCallback, object? state, uint dueTime, uint period, bool flowExecutionContext)
{
_timerCallback = timerCallback;
@@ -444,6 +496,23 @@ internal TimerQueueTimer(TimerCallback timerCallback, object? state, uint dueTim
Change(dueTime, period);
}
+ internal string DisplayString
+ {
+ get
+ {
+ string? typeName = _timerCallback.Method.DeclaringType?.FullName;
+ if (typeName is not null)
+ {
+ typeName += ".";
+ }
+
+ return
+ "DueTime = " + (_dueTime == Timeout.UnsignedInfinite ? "(not set)" : TimeSpan.FromMilliseconds(_dueTime)) + ", " +
+ "Period = " + (_period == Timeout.UnsignedInfinite ? "(not set)" : TimeSpan.FromMilliseconds(_period)) + ", " +
+ typeName + _timerCallback.Method.Name + "(" + (_state?.ToString() ?? "null") + ")";
+ }
+ }
+
internal bool Change(uint dueTime, uint period)
{
bool success;
@@ -646,6 +715,40 @@ internal void CallCallback(bool isThreadPool)
var t = (TimerQueueTimer)state;
t._timerCallback(t._state);
};
+
+ internal sealed class TimerDebuggerTypeProxy
+ {
+ private readonly TimerQueueTimer _timer;
+
+ public TimerDebuggerTypeProxy(Timer timer) => _timer = timer._timer._timer;
+ public TimerDebuggerTypeProxy(TimerQueueTimer timer) => _timer = timer;
+
+ public DateTime? EstimatedNextTimeUtc
+ {
+ get
+ {
+ if (_timer._dueTime != Timeout.UnsignedInfinite)
+ {
+ // In TimerQueue's static ctor, we snap a tick count and the current time, as a way of being
+ // able to translate from tick counts to times. This is only approximate, for a variety of
+ // reasons (e.g. drift, clock changes, etc.), but when dump debugging we are unable to use
+ // TickCount in a meaningful way, so this at least provides a reasonable approximation.
+ long msOffset = _timer._startTicks - TimerQueue.s_tickCountToTimeMap.TickCount + _timer._dueTime;
+ return (TimerQueue.s_tickCountToTimeMap.Time + TimeSpan.FromMilliseconds(msOffset));
+ }
+
+ return null;
+ }
+ }
+
+ public TimeSpan? DueTime => _timer._dueTime == Timeout.UnsignedInfinite ? null : TimeSpan.FromMilliseconds(_timer._dueTime);
+
+ public TimeSpan? Period => _timer._period == Timeout.UnsignedInfinite ? null : TimeSpan.FromMilliseconds(_timer._period);
+
+ public TimerCallback Callback => _timer._timerCallback;
+
+ public object? State => _timer._state;
+ }
}
// TimerHolder serves as an intermediary between Timer and TimerQueueTimer, releasing the TimerQueueTimer
@@ -691,12 +794,13 @@ public ValueTask CloseAsync()
}
}
-
+ [DebuggerDisplay("{DisplayString,nq}")]
+ [DebuggerTypeProxy(typeof(TimerQueueTimer.TimerDebuggerTypeProxy))]
public sealed class Timer : MarshalByRefObject, IDisposable, IAsyncDisposable
{
internal const uint MaxSupportedTimeout = 0xfffffffe;
- private TimerHolder _timer;
+ internal TimerHolder _timer;
public Timer(TimerCallback callback,
object? state,
@@ -860,5 +964,25 @@ public ValueTask DisposeAsync()
{
return _timer.CloseAsync();
}
+
+ private string DisplayString => _timer._timer.DisplayString;
+
+ /// Gets a list of all timers for debugging purposes.
+ private static IEnumerable AllTimers // intended to be used by devs from debugger
+ {
+ get
+ {
+ var timers = new List();
+
+ foreach (TimerQueue queue in TimerQueue.Instances)
+ {
+ timers.AddRange(queue.GetTimersForDebugger());
+ }
+
+ timers.Sort((t1, t2) => t1._dueTime.CompareTo(t2._dueTime));
+
+ return timers;
+ }
+ }
}
}