From 933b8112207fadcb1051ba192e31bd6cd84ae22b Mon Sep 17 00:00:00 2001 From: Lifeng Lu Date: Sun, 8 Feb 2026 15:20:42 -0800 Subject: [PATCH 1/4] Handle JoinUntilEmpty in dumpasync Join to the task it waits on. --- src/SosThreadingTools/DumpAsyncCommand.cs | 76 +++++++++++++++++++---- 1 file changed, 64 insertions(+), 12 deletions(-) diff --git a/src/SosThreadingTools/DumpAsyncCommand.cs b/src/SosThreadingTools/DumpAsyncCommand.cs index de3686f8d..df93d98fb 100644 --- a/src/SosThreadingTools/DumpAsyncCommand.cs +++ b/src/SosThreadingTools/DumpAsyncCommand.cs @@ -164,6 +164,12 @@ private static void GetAllStateMachines(ClrHeap heap, List al } } } + + ClrObject currentObject = stateMachine.TryGetObjectField("<>4__this"); + if (!currentObject.IsNull && currentObject.Type is not null && string.Equals(currentObject.Type.Name, "Microsoft.VisualStudio.Threading.JoinableTaskCollection", StringComparison.Ordinal)) + { + asyncState.WaitingJoinableTasks = GetJoinableTasksFromCollection(currentObject); + } } } #pragma warning disable CA1031 // Do not catch general exception types @@ -183,6 +189,40 @@ private static void GetAllStateMachines(ClrHeap heap, List al } } + private static List GetJoinableTasksFromCollection(ClrObject joinableTaskCollection) + { + var joinableTasks = new List(); + ClrValueType? dependentData = joinableTaskCollection.TryGetValueClassField("dependentData"); + if (dependentData is not null) + { + ClrObject childDependentNodes = dependentData.TryGetObjectField("childDependentNodes"); + if (!childDependentNodes.IsNull && + childDependentNodes.TryReadField("count", out int count) && + childDependentNodes.TryReadField("freeCount", out int freeCount)) + { + count -= freeCount; + if (count > 0) + { + ClrObject entries = childDependentNodes.TryGetObjectField("entries"); + if (!entries.IsNull && entries.IsArray && entries.AsArray() is ClrArray entriesArray) + { + for (int i = 0; i < entriesArray.Length; i++) + { + ClrValueType? value = entriesArray.GetStructValue(i); + ClrObject key = value.TryGetObjectField("key"); + if (!key.IsNull) + { + joinableTasks.Add(key); + } + } + } + } + } + } + + return joinableTasks; + } + private static void ChainStateMachinesBasedOnTaskContinuations(Dictionary knownStateMachines) { foreach (AsyncStateMachine? stateMachine in knownStateMachines.Values) @@ -285,16 +325,13 @@ private static void ChainStateMachinesBasedOnJointableTasks(List4__this"); - ClrObject wrappedTask = joinableTask.TryGetObjectField("wrappedTask"); - if (!wrappedTask.IsNull) + FindWaitingTaskFromJoinableTask(joinableTask, stateMachine); + + if (stateMachine.WaitingJoinableTasks is List joinableTasks) { - AsyncStateMachine? previousStateMachine = allStateMachines - .FirstOrDefault(s => s.Task.Address == wrappedTask.Address); - if (previousStateMachine is object && stateMachine != previousStateMachine) + foreach (ClrObject waitingJoinableTask in joinableTasks) { - stateMachine.Previous = previousStateMachine; - previousStateMachine.Next = stateMachine; - previousStateMachine.DependentCount++; + FindWaitingTaskFromJoinableTask(waitingJoinableTask, stateMachine); } } } @@ -306,6 +343,22 @@ private static void ChainStateMachinesBasedOnJointableTasks(List s.Task.Address == wrappedTask.Address); + if (previousStateMachine is object && currentStateMachine != previousStateMachine) + { + currentStateMachine.Previous ??= previousStateMachine; + previousStateMachine.Next = currentStateMachine; + previousStateMachine.DependentCount++; + } + } + } } private static void MarkUIThreadDependingTasks(List allStateMachines) @@ -438,10 +491,7 @@ private void PrintOutStateMachines(List allStateMachines) { bool multipleLineBlock = this.PrintAsyncStateMachineChain(node, printedMachines); - if (multipleLineBlock) - { - Console.WriteLine(string.Empty); - } + Console.WriteLine(string.Empty); } // Print nodes which we didn't print because of loops. @@ -573,6 +623,8 @@ public AsyncStateMachine(int state, ClrObject stateMachine, ClrObject task) public AsyncStateMachine? AlterPrevious { get; set; } + public List? WaitingJoinableTasks { get; set; } + public ulong CodeAddress { get; set; } public override string ToString() From c8b02c769df2def3a3d6bc6ae1b2ee092e4b79c1 Mon Sep 17 00:00:00 2001 From: Lifeng Lu Date: Sun, 8 Feb 2026 16:07:24 -0800 Subject: [PATCH 2/4] Fix missing thread blocking state --- src/SosThreadingTools/DumpAsyncCommand.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SosThreadingTools/DumpAsyncCommand.cs b/src/SosThreadingTools/DumpAsyncCommand.cs index df93d98fb..9a3cc16ef 100644 --- a/src/SosThreadingTools/DumpAsyncCommand.cs +++ b/src/SosThreadingTools/DumpAsyncCommand.cs @@ -441,7 +441,7 @@ private void MarkThreadingBlockTasks(ClrHeap heap, List allSt } } - break; + continue; } } } From 9b8f1bf184df2032972c82964b88c143d725e29b Mon Sep 17 00:00:00 2001 From: Lifeng Lu Date: Sun, 8 Feb 2026 16:52:23 -0800 Subject: [PATCH 3/4] Remove unused value. --- src/SosThreadingTools/DumpAsyncCommand.cs | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/SosThreadingTools/DumpAsyncCommand.cs b/src/SosThreadingTools/DumpAsyncCommand.cs index 9a3cc16ef..65a697cb0 100644 --- a/src/SosThreadingTools/DumpAsyncCommand.cs +++ b/src/SosThreadingTools/DumpAsyncCommand.cs @@ -489,7 +489,7 @@ private void PrintOutStateMachines(List allStateMachines) .OrderByDescending(m => m.Depth) .ThenByDescending(m => m.SwitchToMainThreadTask.Address)) { - bool multipleLineBlock = this.PrintAsyncStateMachineChain(node, printedMachines); + this.PrintAsyncStateMachineChain(node, printedMachines); Console.WriteLine(string.Empty); } @@ -509,10 +509,9 @@ private void PrintOutStateMachines(List allStateMachines) } } - private bool PrintAsyncStateMachineChain(AsyncStateMachine node, HashSet printedMachines) + private void PrintAsyncStateMachineChain(AsyncStateMachine node, HashSet printedMachines) { int nLevel = 0; - bool multipleLineBlock = false; var loopDetection = new HashSet(); for (AsyncStateMachine? p = node; p is object; p = p.Next) @@ -522,7 +521,6 @@ private bool PrintAsyncStateMachineChain(AsyncStateMachine node, HashSet 0) { this.WriteString(".."); - multipleLineBlock = true; } else if (p.AlterPrevious is object) { @@ -531,14 +529,12 @@ private bool PrintAsyncStateMachineChain(AsyncStateMachine node, HashSet allStateMachines) From eba902000890f2e27633911b3fe9c69dbcace293 Mon Sep 17 00:00:00 2001 From: Lifeng Lu Date: Sun, 8 Feb 2026 17:32:35 -0800 Subject: [PATCH 4/4] Adjust logic based on Copilot feedbacks. --- src/SosThreadingTools/DumpAsyncCommand.cs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/SosThreadingTools/DumpAsyncCommand.cs b/src/SosThreadingTools/DumpAsyncCommand.cs index 65a697cb0..88c73a216 100644 --- a/src/SosThreadingTools/DumpAsyncCommand.cs +++ b/src/SosThreadingTools/DumpAsyncCommand.cs @@ -213,6 +213,10 @@ private static List GetJoinableTasksFromCollection(ClrObject joinable if (!key.IsNull) { joinableTasks.Add(key); + if (--count == 0) + { + break; + } } } } @@ -353,8 +357,16 @@ void FindWaitingTaskFromJoinableTask(ClrObject joinableTask, AsyncStateMachine c .FirstOrDefault(s => s.Task.Address == wrappedTask.Address); if (previousStateMachine is object && currentStateMachine != previousStateMachine) { - currentStateMachine.Previous ??= previousStateMachine; - previousStateMachine.Next = currentStateMachine; + if (currentStateMachine.Previous is null) + { + currentStateMachine.Previous = previousStateMachine; + previousStateMachine.Next = currentStateMachine; + } + else + { + previousStateMachine.Next ??= currentStateMachine; + } + previousStateMachine.DependentCount++; } }