Skip to content

Commit

Permalink
Add pstacks in gsose and as a .NET Core console application to show m…
Browse files Browse the repository at this point in the history
…erged threads call stacks a la Visual Studio "Parallel Stacks"
  • Loading branch information
Christophe Nasarre committed Mar 14, 2019
1 parent 3d73018 commit 4061a2c
Show file tree
Hide file tree
Showing 19 changed files with 857 additions and 38 deletions.
50 changes: 42 additions & 8 deletions Documentation/gsose.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,19 @@ gsose is a debugger extension DLL designed to dig into CLR data structures.
Functions are listed by category and shortcut names are listed in parenthesis.
Type "!help " for detailed info on that function.
Thread Pool Timers
Thread Pool Threads
----------------------------- -----------------------------
TpQueue(tpq) TimerInfo (ti)
TpQueue(tpq) ParallelStacks (pstacks)
TpRunning(tpr)
Tasks Strings
Tasks Timers
----------------------------- -----------------------------
TkState (tks) StringDuplicates (sd)
TkState (tks) TimerInfo (ti)
GetMethodName (gmn)
Data structures
-----------------------------
DumpConcurrentDictionary (dcd)
Data structures Strings
----------------------------- -----------------------------
DumpConcurrentDictionary (dcd) StringDuplicates (sd)
DumpConcurrentQueue (dcq)
Garbage Collector
Expand All @@ -32,6 +32,40 @@ PinnedObjects (po)



## ParallelStacks (pstacks)
```
!pstacks merges parallel stacks
0:000> !pstacks
________________________________________________
~~~~
1 (dynamicClass).IL_STUB_PInvoke(IntPtr, Byte*, Int32, Int32 ByRef, IntPtr)
...
1 System.Console.ReadLine()
1 NetCoreConsoleApp.Program.Main(String[])
________________________________________________
~~~~
1 System.Threading.Monitor.Wait(Object, Int32, Boolean)
...
1 System.Threading.Tasks.Task.Wait()
1 NetCoreConsoleApp.Program+c.b__1_4(Object)
~~~~
2 System.Threading.Monitor.Wait(Object, Int32, Boolean)
...
2 NetCoreConsoleApp.Program+c__DisplayClass1_0.b__7()
3 System.Threading.Tasks.Task.InnerInvoke()
4 System.Threading.Tasks.Task+c.cctor>b__278_1(Object)
...
4 System.Threading.Tasks.Task.ExecuteEntryUnsafe()
4 System.Threading.Tasks.Task.ExecuteWorkItem()
7 System.Threading.ThreadPoolWorkQueue.Dispatch()
7 System.Threading._ThreadPoolWaitCallback.PerformWaitCallback()
==> 8 threads with 2 roots
```



## TpQueue (tpq)
```
!TpQueue lists the enqueued workitems in the Clr Thread Pool followed by a summary of the different tasks/work items.
Expand Down Expand Up @@ -211,7 +245,7 @@ System.Collections.Concurrent.ConcurrentDictionary<System.Int32,NetCoreConsoleAp
!GCInfo
!GCInfo lists generations per segments. Show pinned objects with -pinned and object instances count/size with -stat (by default)
0:000> !gci -pinned
0:000> !gci [-stat] [-pinned]
13 - 7 generations
LOH | 9F06001000 - 9F0CDDB8C8 ( 115,189,960)
Expand Down
10 changes: 10 additions & 0 deletions Documentation/pstacks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# pstacks

This .NET Core tool allows you to load a .NET application memory dump file and displays the merged threads call stacks à la Visual Studio "Parallel Stacks".
![pstacks_result](./pstacks.png)

If you need to specify the path to the mscordac file, pass it as the second parameter (just after the dump file name)

## Platforms
As a .NET Core console application, it runs on Windows and Linux.
Note that it is not possible to attach to a live process on Linux (but could be adapted on Windows is needed)
Binary file added Documentation/pstacks.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2016-2018 Nasarre Christophe
Copyright (c) 2016-2019 Nasarre Christophe

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ The few "debugging extensions" that have been created at Criteo to help post-mor
[zip](./binaries/ClrMDStudio-1.5.1_x64.zip)
- as a [WinDBG extension](./Documentation/gsose.md) to get the same level of details plus more commands such as getting a method signature based on its address.
[zip](./binaries/gsose-1.5.3_x64.zip)
- as a [.NET Core console tool](./Documentation/pstacks.md) to load a .NET application memory dump and show merged threads call stack à la Visual Studio "parallel stacks" (works also on Linux).
[zip](./binaries/pstacks-1.0.1.zip)

More analyzers and commands will be added as needed.

Expand Down Expand Up @@ -38,15 +40,19 @@ More commands will be added as needed.


## Source Code
The `DebuggingExtensions` Visual Studio 2017 solution contains two projects:
The `DebuggingExtensions` Visual Studio 2017 solution contains three projects:

1. `ClrMDStudio`: WPF application that loads a dump file on which commands to be executed

2. `gsose`: "***G**rand **S**on **O**f **S**trike **E**xtension*" for WinDBG that exposes the same commands (and more)

3. `pstacks`: .NET Core console application that loads a dump file and shows merged parallel stacks



These projects depends on Nuget packages:

- [ClrMD](https://github.com/Microsoft/clrmd): C# library to explore dump files.
- [DynaMD](https://github.com/kevingosse/DynaMD): C# `dynamic`-based helpers on top of ClrMD.
- [ClrMDExports](https://github.com/kevingosse/ClrMDExports): Helper to write WinDBG/LLDB extensionss on top of ClrMD.

Binary file not shown.
Binary file added binaries/pstacks-1.0.1.zip
Binary file not shown.
24 changes: 24 additions & 0 deletions src/DebuggingExtensions.sln
Original file line number Diff line number Diff line change
Expand Up @@ -7,30 +7,54 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ClrMDStudio", "ClrMDStudio\
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "gsose", "gsose\gsose.csproj", "{3FCA8012-3BAD-41E4-91F4-534AA9A44F96}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ParallelStacks", "ParallelStacks\ParallelStacks.csproj", "{D17509EA-830E-4A29-A959-16D2D1E7C7E3}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{A03DF28C-DBEE-454E-A31C-0F38AD23BFAD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A03DF28C-DBEE-454E-A31C-0F38AD23BFAD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A03DF28C-DBEE-454E-A31C-0F38AD23BFAD}.Debug|x64.ActiveCfg = Debug|x64
{A03DF28C-DBEE-454E-A31C-0F38AD23BFAD}.Debug|x64.Build.0 = Debug|x64
{A03DF28C-DBEE-454E-A31C-0F38AD23BFAD}.Debug|x86.ActiveCfg = Debug|x86
{A03DF28C-DBEE-454E-A31C-0F38AD23BFAD}.Debug|x86.Build.0 = Debug|x86
{A03DF28C-DBEE-454E-A31C-0F38AD23BFAD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A03DF28C-DBEE-454E-A31C-0F38AD23BFAD}.Release|Any CPU.Build.0 = Release|Any CPU
{A03DF28C-DBEE-454E-A31C-0F38AD23BFAD}.Release|x64.ActiveCfg = Release|x64
{A03DF28C-DBEE-454E-A31C-0F38AD23BFAD}.Release|x64.Build.0 = Release|x64
{A03DF28C-DBEE-454E-A31C-0F38AD23BFAD}.Release|x86.ActiveCfg = Release|x86
{A03DF28C-DBEE-454E-A31C-0F38AD23BFAD}.Release|x86.Build.0 = Release|x86
{3FCA8012-3BAD-41E4-91F4-534AA9A44F96}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3FCA8012-3BAD-41E4-91F4-534AA9A44F96}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3FCA8012-3BAD-41E4-91F4-534AA9A44F96}.Debug|x64.ActiveCfg = Debug|x64
{3FCA8012-3BAD-41E4-91F4-534AA9A44F96}.Debug|x64.Build.0 = Debug|x64
{3FCA8012-3BAD-41E4-91F4-534AA9A44F96}.Debug|x86.ActiveCfg = Debug|x86
{3FCA8012-3BAD-41E4-91F4-534AA9A44F96}.Debug|x86.Build.0 = Debug|x86
{3FCA8012-3BAD-41E4-91F4-534AA9A44F96}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3FCA8012-3BAD-41E4-91F4-534AA9A44F96}.Release|Any CPU.Build.0 = Release|Any CPU
{3FCA8012-3BAD-41E4-91F4-534AA9A44F96}.Release|x64.ActiveCfg = Release|x64
{3FCA8012-3BAD-41E4-91F4-534AA9A44F96}.Release|x64.Build.0 = Release|x64
{3FCA8012-3BAD-41E4-91F4-534AA9A44F96}.Release|x86.ActiveCfg = Release|x86
{3FCA8012-3BAD-41E4-91F4-534AA9A44F96}.Release|x86.Build.0 = Release|x86
{D17509EA-830E-4A29-A959-16D2D1E7C7E3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D17509EA-830E-4A29-A959-16D2D1E7C7E3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D17509EA-830E-4A29-A959-16D2D1E7C7E3}.Debug|x64.ActiveCfg = Debug|x64
{D17509EA-830E-4A29-A959-16D2D1E7C7E3}.Debug|x64.Build.0 = Debug|x64
{D17509EA-830E-4A29-A959-16D2D1E7C7E3}.Debug|x86.ActiveCfg = Debug|Any CPU
{D17509EA-830E-4A29-A959-16D2D1E7C7E3}.Debug|x86.Build.0 = Debug|Any CPU
{D17509EA-830E-4A29-A959-16D2D1E7C7E3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D17509EA-830E-4A29-A959-16D2D1E7C7E3}.Release|Any CPU.Build.0 = Release|Any CPU
{D17509EA-830E-4A29-A959-16D2D1E7C7E3}.Release|x64.ActiveCfg = Release|x64
{D17509EA-830E-4A29-A959-16D2D1E7C7E3}.Release|x64.Build.0 = Release|x64
{D17509EA-830E-4A29-A959-16D2D1E7C7E3}.Release|x86.ActiveCfg = Release|Any CPU
{D17509EA-830E-4A29-A959-16D2D1E7C7E3}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
205 changes: 205 additions & 0 deletions src/ParallelStacks/ParallelStack.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Diagnostics.Runtime;

namespace ParallelStacks
{
public class ParallelStack
{
public static ParallelStack Build(ClrRuntime runtime)
{
var ps = new ParallelStack();
var stackFrames = new List<ClrStackFrame>(64);
foreach (var thread in runtime.Threads)
{
stackFrames.Clear();

foreach (var stackFrame in thread.StackTrace.Reverse())
{
if (stackFrame.Kind != ClrStackFrameType.ManagedMethod)
continue;

stackFrames.Add(stackFrame);
}

if (stackFrames.Count == 0)
continue;

ps.AddStack(thread.ManagedThreadId, stackFrames.ToArray());
}

return ps;
}

private bool _useDml;

private ParallelStack(ClrStackFrame frame = null)
{
Stacks = new List<ParallelStack>();
ThreadIds = new List<int>();
Frame = (frame == null) ? null : new StackFrame(frame);
}

public List<ParallelStack> Stacks { get; }

public StackFrame Frame { get; }

public List<int> ThreadIds { get; set; }

public void WriteToConsole(bool useDml = false)
{
_useDml = useDml;
WriteStack(this);
}

private const int padding = 5;
private void WriteStack(ParallelStack stack, int increment = 0)
{
var alignment = new string(' ', padding * increment);
if (stack.Stacks.Count == 0)
{
var lastFrame = stack.Frame;
Console.Write($"{Environment.NewLine}{alignment}");
WriteFrameSeparator(" ~~~~");
//Console.Write($"{Environment.NewLine}{alignment}{stack.ThreadIds.Count,padding} ");
WriteCount($"{Environment.NewLine}{alignment}{stack.ThreadIds.Count,padding} ");
WriteFrame(lastFrame);
return;
}

foreach (var nextStackFrame in stack.Stacks.OrderBy(s => s.ThreadIds.Count))
{
WriteStack(nextStackFrame,
(nextStackFrame.ThreadIds.Count == stack.ThreadIds.Count) ? increment : increment + 1);
}

var currentFrame = stack.Frame;
//Console.Write($"{Environment.NewLine}{alignment}{stack.ThreadIds.Count,padding} ");
WriteCount($"{Environment.NewLine}{alignment}{stack.ThreadIds.Count,padding} ");
WriteFrame(currentFrame);
}

private void WriteFrame(StackFrame frame)
{
if (!string.IsNullOrEmpty(frame.TypeName))
{
var namespaces = frame.TypeName.Split('.');
for (int i = 0; i < namespaces.Length - 1; i++)
{
WriteNamespace(namespaces[i]);
WriteSeparator(".");
}
WriteMethodType(namespaces[namespaces.Length - 1]);
WriteSeparator(".");
}

WriteMethod(frame.MethodName);
WriteSeparator("(");

var parameters = frame.Signature;
for (int current = 0; current < parameters.Count; current++)
{
var parameter = parameters[current];
// handle byref case
var pos = parameter.LastIndexOf(" ByRef");
if (pos != -1)
{
WriteType(parameter.Substring(0, pos));
WriteDark(" ByRef");
}
else
{
WriteType(parameter);
}
if (current < parameters.Count - 1) WriteSeparator(", ");
}
WriteSeparator(")");
}

private void WriteCount(string count)
{
if (_useDml)
{
Console.Write($"<col fg=\"srcpair\" bg=\"wbg\">{count}</col>");
return;
}

WriteWithColor(count, ConsoleColor.Cyan);
}
private void WriteNamespace(string ns)
{
WriteWithColor(ns, ConsoleColor.DarkCyan);
}
private void WriteType(string type)
{
WriteWithColor(type, ConsoleColor.Gray);
}
private void WriteSeparator(string separator)
{
WriteWithColor(separator, ConsoleColor.White);
}
private void WriteDark(string separator)
{
WriteWithColor(separator, ConsoleColor.DarkGray);
}
private void WriteMethod(string method)
{
if (_useDml)
{
Console.Write($"<col fg=\"srckw\" bg=\"wbg\">{method}</col>");
return;
}

WriteWithColor(method, ConsoleColor.Cyan);
}
private void WriteMethodType(string type)
{
if (_useDml)
{
Console.Write($"<b><col fg=\"srckw\" bg=\"wbg\">{type}</col></b>");
return;
}

WriteWithColor(type, ConsoleColor.DarkCyan);
}
private void WriteFrameSeparator(string text)
{
if (_useDml)
{
Console.Write($"<b><col fg=\"srcpair\" bg=\"wbg\">{text}</col></b>");
return;
}

WriteWithColor(text, ConsoleColor.Yellow);
}

private void WriteWithColor(string text, ConsoleColor color)
{
var current = Console.ForegroundColor;
Console.ForegroundColor = color;
Console.Write(text);
Console.ForegroundColor = current;
}

private void AddStack(int threadId, ClrStackFrame[] frames, int index = 0)
{
ThreadIds.Add(threadId);
var firstFrame = frames[index].DisplayString;
var callstack = Stacks.FirstOrDefault(s => s.Frame.Text == firstFrame);
if (callstack == null)
{
callstack = new ParallelStack(frames[index]);
Stacks.Add(callstack);
}

if (index == frames.Length - 1)
{
callstack.ThreadIds.Add(threadId);
return;
}

callstack.AddStack(threadId, frames, index + 1);
}
}
}
Loading

0 comments on commit 4061a2c

Please sign in to comment.