in src/Microsoft.Diagnostics.Runtime/src/Common/GCRoot.cs [83:171]
public IEnumerable<GCRootPath> EnumerateGCRoots(ulong target, bool returnOnlyFullyUniquePaths, int maxDegreeOfParallelism, IEnumerable<IClrRoot> roots, CancellationToken cancellationToken = default)
{
if (roots is null)
throw new ArgumentNullException(nameof(roots));
bool parallel = Heap.Runtime.IsThreadSafe && maxDegreeOfParallelism > 1;
Dictionary<ulong, LinkedListNode<ClrObject>> knownEndPoints = new Dictionary<ulong, LinkedListNode<ClrObject>>()
{
{ target, new LinkedListNode<ClrObject>(Heap.GetObject(target)) }
};
if (!parallel)
{
int count = 0;
ObjectSet processedObjects = new ObjectSet(Heap);
foreach (IClrRoot root in roots)
{
LinkedList<ClrObject>? path = PathsTo(processedObjects, knownEndPoints, root.Object, target, returnOnlyFullyUniquePaths, cancellationToken).FirstOrDefault();
if (path != null)
yield return new GCRootPath(root, path.ToImmutableArray());
if (count != processedObjects.Count)
{
count = processedObjects.Count;
ProgressUpdated?.Invoke(this, count);
}
}
}
else
{
ParallelObjectSet processedObjects = new ParallelObjectSet(Heap);
ConcurrentQueue<GCRootPath> results = new ConcurrentQueue<GCRootPath>();
using BlockingCollection<IClrRoot?> queue = new BlockingCollection<IClrRoot?>();
Thread[] threads = new Thread[Math.Min(maxDegreeOfParallelism, Environment.ProcessorCount)];
for (int i = 0; i < threads.Length; i++)
{
threads[i] = new Thread(() => WorkerThread(queue, results, processedObjects, knownEndPoints, target, all: true, returnOnlyFullyUniquePaths, cancellationToken)) { Name = "GCRoot Worker Thread" };
threads[i].Start();
}
#pragma warning disable CA2016 // Forward the 'CancellationToken' parameter to methods that take one (BlockingCollection is unbounded so Add will not block)
foreach (IClrRoot root in roots)
queue.Add(root);
// Add one sentinel value for every thread
for (int i = 0; i < threads.Length; i++)
queue.Add(null);
#pragma warning restore CA2016 // Forward the 'CancellationToken' parameter to methods that take one
queue.CompleteAdding();
int count = 0;
// Worker threads end when they have run out of roots to process. While we are waiting for them to exit, yield return
// any results they've found. We'll use a 100 msec timeout because processing roots is slooooow and finding a root is
// rare. There's no reason to check these results super quickly and starve worker threads.
for (int i = 0; i < threads.Length; i++)
{
while (!threads[i].Join(100))
{
if (cancellationToken.IsCancellationRequested)
yield break;
while (results.TryDequeue(out GCRootPath result))
yield return result;
}
if (count != processedObjects.Count)
{
count = processedObjects.Count;
ProgressUpdated?.Invoke(this, count);
}
}
// We could have raced to put an object in the results queue while joining the last thread, so we need to drain the
// results queue one last time.
while (results.TryDequeue(out GCRootPath result))
yield return result;
if (count != processedObjects.Count)
{
count = processedObjects.Count;
ProgressUpdated?.Invoke(this, count);
}
}
}