Navigation Menu

Skip to content

Commit

Permalink
Add debugger displays / type proxies for Timer (#49100)
Browse files Browse the repository at this point in the history
Adds a private Timer.AllTimers property that can be used in the debugger to get a list of all timers, debugger display strings for Timer / TimerQueueTimer that help to make sense of the state in a timer and when it'll fire next, and debugger type proxies for Timer/TimerQueueTimer/TimerQueue to help when navigating state.
  • Loading branch information
stephentoub committed Mar 10, 2021
1 parent 267cb42 commit 3973fc6
Show file tree
Hide file tree
Showing 4 changed files with 138 additions and 9 deletions.
Expand Up @@ -16,6 +16,9 @@
<method name="GetDelegateContinuationsForDebugger" />
<method name="SetNotificationForWaitCompletion" />
</type>
<type fullname="System.Threading.Timer">
<property name="AllTimers" />
</type>
<type fullname="System.Threading.ThreadPool">
<method name="GetQueuedWorkItemsForDebugger" />
<method name="GetGloballyQueuedWorkItemsForDebugger" />
Expand Down
Expand Up @@ -30,11 +30,9 @@ public class CancellationTokenSource : IDisposable
internal static readonly CancellationTokenSource s_neverCanceledSource = new CancellationTokenSource();

/// <summary>Delegate used with <see cref="Timer"/> to trigger cancellation of a <see cref="CancellationTokenSource"/>.</summary>
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()

/// <summary>The current state of the CancellationTokenSource.</summary>
private volatile int _state;
Expand Down
Expand Up @@ -5433,6 +5433,7 @@ public static Task Delay(int millisecondsDelay, CancellationToken cancellationTo
/// <summary>Task that also stores the completion closure and logic for Task.Delay implementation.</summary>
private class DelayPromise : Task
{
private static readonly TimerCallback s_timerCallback = TimerCallback;
private readonly TimerQueueTimer? _timer;

internal DelayPromise(uint millisecondsDelay)
Expand All @@ -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
Expand All @@ -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())
Expand Down
130 changes: 127 additions & 3 deletions 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;
Expand Down Expand Up @@ -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
/// <summary>Mapping from a tick count to a time to use when debugging to translate tick count values.</summary>
internal static readonly (long TickCount, DateTime Time) s_tickCountToTimeMap = (TickCount64, DateTime.UtcNow);

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

Expand All @@ -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<TimerQueueTimer> 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<TimerQueueTimer>(_queue.GetTimersForDebugger()).ToArray();
}

#endregion

#region interface to native timer
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -425,7 +478,6 @@ internal sealed partial class TimerQueueTimer : IThreadPoolWorkItem
private bool _canceled;
private object? _notifyWhenNoCallbacksRunning; // may be either WaitHandle or Task<bool>


internal TimerQueueTimer(TimerCallback timerCallback, object? state, uint dueTime, uint period, bool flowExecutionContext)
{
_timerCallback = timerCallback;
Expand All @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -860,5 +964,25 @@ public ValueTask DisposeAsync()
{
return _timer.CloseAsync();
}

private string DisplayString => _timer._timer.DisplayString;

/// <summary>Gets a list of all timers for debugging purposes.</summary>
private static IEnumerable<TimerQueueTimer> AllTimers // intended to be used by devs from debugger
{
get
{
var timers = new List<TimerQueueTimer>();

foreach (TimerQueue queue in TimerQueue.Instances)
{
timers.AddRange(queue.GetTimersForDebugger());
}

timers.Sort((t1, t2) => t1._dueTime.CompareTo(t2._dueTime));

return timers;
}
}
}
}

0 comments on commit 3973fc6

Please sign in to comment.