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; + } + } } }