| File: src\roslyn\src\Dependencies\PooledObjects\PoolTracker.cs | Web Access |
| Project: src\roslyn\src\RoslynAnalyzers\Microsoft.CodeAnalysis.Analyzers\Core\Microsoft.CodeAnalysis.Analyzers.csproj (Microsoft.CodeAnalysis.Analyzers) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. #nullable enable using System.Diagnostics; #if DEBUG using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Threading; #endif namespace Microsoft.CodeAnalysis.PooledObjects; #if DEBUG /// <summary> /// Tracks outstanding pooled object allocations, enabling per-test leak detection. /// When tracking is active (via <see cref="StartTracking"/>), every <see cref="ObjectPool{T}.Allocate"/> /// registers the instance and every free/forget removes it. After the test completes, /// <see cref="PoolTrackingContext.HasLeaks"/> reveals whether any pooled objects were not returned. /// All tracking is DEBUG-only and compiles out in Release builds. /// </summary> #endif internal static class PoolTracker { #if DEBUG // AsyncLocal so tracking flows through async test methods and their continuations. private static readonly AsyncLocal<PoolTrackingContext?> s_currentContext = new AsyncLocal<PoolTrackingContext?>(); // Fast check to avoid reading AsyncLocal when no tracking session is active. private static int s_activeTrackers; /// <summary> /// Begins tracking pooled object allocations on the current async flow. /// Returns the context that should later be inspected for leaks. /// </summary> /// <param name="traceLeaks">When true, allocation stack traces are captured for diagnostics.</param> internal static void StartTracking(out PoolTrackingContext context, bool traceLeaks = false) { context = new PoolTrackingContext(traceLeaks); s_currentContext.Value = context; Interlocked.Increment(ref s_activeTrackers); } /// <summary> /// Stops tracking pooled object allocations on the current async flow. /// </summary> internal static void StopTracking() { s_currentContext.Value = null; Interlocked.Decrement(ref s_activeTrackers); } #endif /// <summary> /// Records that a pooled object has been allocated. /// </summary> [Conditional("DEBUG")] internal static void OnAllocate(object obj, string? poolName = null, string filePath = "", int lineNumber = 0) { #if DEBUG if (s_activeTrackers > 0) { s_currentContext.Value?.OnAllocate(obj, poolName, filePath, lineNumber); } #endif } /// <summary> /// Records that a pooled object has been freed / forgotten. /// </summary> [Conditional("DEBUG")] internal static void OnFree(object obj) { #if DEBUG if (s_activeTrackers > 0) { s_currentContext.Value?.OnFree(obj); } #endif } /// <summary> /// Forgives all currently outstanding pooled object allocations, treating them as non-leaks. /// Use this when an exception path is known to abandon pooled objects and that is acceptable /// (e.g., MissingPredefinedMember exception unwinding through intermediate lowering methods). /// </summary> [Conditional("DEBUG")] internal static void ForgiveLeaks() { #if DEBUG if (s_activeTrackers > 0) { s_currentContext.Value?.ForgiveLeaks(); } #endif } } #if DEBUG /// <summary> /// Holds the set of outstanding pooled object allocations for a single tracking session (typically one test). /// </summary> internal sealed class PoolTrackingContext { private readonly ConcurrentDictionary<object, AllocationInfo> _outstanding = new ConcurrentDictionary<object, AllocationInfo>(ReferenceEqualityComparer.Instance); private readonly bool _traceLeaks; internal PoolTrackingContext(bool traceLeaks) { _traceLeaks = traceLeaks; } internal void OnAllocate(object obj, string? poolName, string filePath, int lineNumber) { _outstanding.TryAdd(obj, new AllocationInfo(obj.GetType(), poolName, filePath, lineNumber, _traceLeaks ? Environment.StackTrace : null)); } internal void OnFree(object obj) { _outstanding.TryRemove(obj, out _); } /// <summary> /// Returns true if there are pooled objects that were allocated but never freed. /// </summary> internal bool HasLeaks => !_outstanding.IsEmpty; /// <summary> /// Clears all outstanding allocations, forgiving any current leaks. /// </summary> internal void ForgiveLeaks() { _outstanding.Clear(); } internal bool WaitForOutstandingObjectsToBeFreed(TimeSpan timeout) { if (!HasLeaks) return true; var stopwatch = Stopwatch.StartNew(); while (stopwatch.Elapsed < timeout) { Thread.Sleep(10); if (!HasLeaks) return true; } return !HasLeaks; } /// <summary> /// Returns a human-readable summary of leaked pooled objects, grouped by type with counts /// and allocation stack traces. /// </summary> internal string GetLeakSummary() { var sb = new StringBuilder(); sb.AppendLine("Pool leak detected! The following pooled objects were not returned:"); foreach (var group in _outstanding.Values.GroupBy(v => (v.Type, v.PoolName, v.FilePath, v.LineNumber)).OrderByDescending(g => g.Count())) { var poolInfo = group.Key.PoolName is not null ? $" (from {group.Key.PoolName})" : ""; var allocSite = !string.IsNullOrEmpty(group.Key.FilePath) ? $" (allocated at {Path.GetFileName(group.Key.FilePath)}:{group.Key.LineNumber})" : ""; sb.AppendLine($" {group.Key.Type}{poolInfo}{allocSite}: {group.Count()}"); foreach (var info in group) { if (info.StackTrace != null) { sb.AppendLine($" Allocation stack trace:"); sb.AppendLine($" {info.StackTrace}"); } } } return sb.ToString(); } private readonly struct AllocationInfo(Type type, string? poolName, string filePath, int lineNumber, string? stackTrace) { public readonly Type Type = type; public readonly string? PoolName = poolName; public readonly string FilePath = filePath; public readonly int LineNumber = lineNumber; public readonly string? StackTrace = stackTrace; } } #endif