/// <summary> /// Converts the specified object set. /// </summary> /// <param name="objectSet">The object set.</param> /// <returns>IObjectSet.</returns> public IObjectSet Convert(ClrMd.ObjectSet objectSet) { if (objectSet == null) { return(null); } var item = new ObjectSetAdapter(this, objectSet); return(Cache.GetOrAdd <IObjectSet>(objectSet, () => item, () => item.Setup())); }
/// <summary> /// Enumerates GCRoots of a given object. Similar to !gcroot. /// </summary> /// <param name="target">The target object to search for GC rooting.</param> /// <param name="unique">Whether to only return fully unique paths.</param> /// <param name="cancelToken">A cancellation token to stop enumeration.</param> /// <returns>An enumeration of all GC roots found for target.</returns> public IEnumerable <RootPath> EnumerateGCRoots(ulong target, bool unique, CancellationToken cancelToken) { _heap.BuildDependentHandleMap(cancelToken); long totalObjects = _heap.TotalObjects; long lastObjectReported = 0; bool parallel = AllowParallelSearch && IsFullyCached && _maxTasks > 0; Task <Tuple <LinkedList <ClrObject>, ClrRoot> >[] tasks; ObjectSet processedObjects; Dictionary <ulong, LinkedListNode <ClrObject> > knownEndPoints = new Dictionary <ulong, LinkedListNode <ClrObject> >(); if (parallel) { processedObjects = new ParallelObjectSet(_heap); } else { processedObjects = new ObjectSet(_heap); } int initial = 0; tasks = new Task <Tuple <LinkedList <ClrObject>, ClrRoot> > [_maxTasks]; foreach (ClrHandle handle in _heap.EnumerateStrongHandles()) { Debug.Assert(handle.HandleType != HandleType.Dependent); Debug.Assert(handle.Object != 0); if (processedObjects.Contains(handle.Object)) { continue; } Debug.Assert(_heap.GetObjectType(handle.Object) == handle.Type); if (parallel) { var task = PathToParallel(processedObjects, knownEndPoints, handle, target, unique, cancelToken); if (initial < tasks.Length) { tasks[initial++] = task; } else { int i = Task.WaitAny(tasks); var completed = tasks[i]; tasks[i] = task; if (completed.Result.Item1 != null) { yield return new RootPath() { Root = completed.Result.Item2, Path = completed.Result.Item1.ToArray() } } ; } } else { var path = PathTo(processedObjects, knownEndPoints, new ClrObject(handle.Object, handle.Type), target, unique, false, cancelToken).FirstOrDefault(); if (path != null) { yield return new RootPath() { Root = GetHandleRoot(handle), Path = path.ToArray() } } ; } ReportObjectCount(processedObjects.Count, ref lastObjectReported, totalObjects); } foreach (ClrRoot root in _heap.EnumerateStackRoots()) { if (!processedObjects.Contains(root.Object)) { Debug.Assert(_heap.GetObjectType(root.Object) == root.Type); if (parallel) { var task = PathToParallel(processedObjects, knownEndPoints, root, target, unique, cancelToken); if (initial < tasks.Length) { tasks[initial++] = task; } else { int i = Task.WaitAny(tasks); var completed = tasks[i]; tasks[i] = task; if (completed.Result.Item1 != null) { yield return new RootPath() { Root = completed.Result.Item2, Path = completed.Result.Item1.ToArray() } } ; } } else { var path = PathTo(processedObjects, knownEndPoints, new ClrObject(root.Object, root.Type), target, unique, false, cancelToken).FirstOrDefault(); if (path != null) { yield return new RootPath() { Root = root, Path = path.ToArray() } } ; } ReportObjectCount(processedObjects.Count, ref lastObjectReported, totalObjects); } } foreach (Tuple <LinkedList <ClrObject>, ClrRoot> result in WhenEach(tasks)) { ReportObjectCount(processedObjects.Count, ref lastObjectReported, totalObjects); yield return(new RootPath() { Root = result.Item2, Path = result.Item1.ToArray() }); } ReportObjectCount(totalObjects, ref lastObjectReported, totalObjects); }
/// <summary> /// Enumerates GCRoots of a given object. Similar to !gcroot. /// </summary> /// <param name="target">The target object to search for GC rooting.</param> /// <param name="unique">Whether to only return fully unique paths.</param> /// <param name="maxDegreeOfParallelism">The number of threads this class is allowed to use to calculate the result. /// Setting this to 1 will cause the algorithm to run on the current thread.</param> /// <param name="roots">The roots to consider. You can pass ClrMD.</param> /// <param name="cancellationToken">A cancellation token to stop enumeration.</param> /// <returns>An enumeration of all GC roots found for target.</returns> public IEnumerable <GCRootPath> EnumerateGCRoots(ulong target, bool unique, 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, unique, 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, unique, cancellationToken)) { Name = "GCRoot Worker Thread" }; threads[i].Start(); } foreach (IClrRoot root in roots) { queue.Add(root); } // Add one sentinal value for every thread for (int i = 0; i < threads.Length; i++) { queue.Add(null); } 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)) { 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); } } }
private IEnumerable <LinkedList <ClrObject> > PathsTo( ObjectSet deadEnds, Dictionary <ulong, LinkedListNode <ClrObject> >?knownEndPoints, ClrObject source, ulong target, bool unique, CancellationToken cancellationToken) { /* knownEndPoints: A set of objects that are known to point to the target, once we see one of * these, we can end the search early and return the current path plus the * nodes there. * * deadEnds When unique == false, these are Objects we've fully processed and know doesn't * point to the target object. When unique == true, deadEnds are updated to include * objects that other threads are processing, ensuring we don't yield duplicate * roots. Note that we must be careful to ONLY add objects to this list when we are * sure we've exhausted all potential paths because parallel threads use this same * set. * * path: The current path we are considering. As we walk through objects in the rooting chain, * we will add a new node to this list with the object in question and create a stack of * all objects it points to. This is a depth-first search, so we will search as deeply * as we can, and pop off objects from path as soon as the stack of references is empty. * If we find that the reference stack is empty and we didn't find the target/known end * point, then we'll mark the object in deadEnds. * * processing: The list of objects we are currently considering on this thread. Since objects * can contain circular references, we need to know we shouldn't re-consider objects * we encounter while walking objects. * * It's important to note that multiple threads may race and consider the same path at the same * time. This is expected (and similar to how the GC operates). The thing we have to make sure * of is that we only add objects to knownEndPoints/deadEnds when we are 100% sure of the result * because it will directly affect other threads walking these paths. */ HashSet <ulong> processing = new HashSet <ulong>() { source.Address }; LinkedList <PathEntry> path = new LinkedList <PathEntry>(); if (knownEndPoints != null) { lock (knownEndPoints) { if (knownEndPoints.TryGetValue(source.Address, out LinkedListNode <ClrObject>?ending)) { if (!unique || ending.Value.Address == target) { yield return(GetResult(ending)); } yield break; } } } if (unique && !deadEnds.Add(source.Address)) { yield break; } if (source.Type is null) { yield break; } if (source.Address == target) { path.AddLast(new PathEntry { Object = source }); yield return(GetResult()); yield break; } path.AddLast( new PathEntry { Object = source, Todo = GetRefs(source, out bool foundTarget, out LinkedListNode <ClrObject>?foundEnding) });