<IncludeSymbols>true</IncludeSymbols>
<IsShippingAssembly>false</IsShippingAssembly>
</PropertyGroup>
-
+
<ItemGroup>
<PackageReference Include="Microsoft.Diagnostics.Runtime" Version="$(MicrosoftDiagnosticsRuntimeVersion)" />
- <PackageReference Include="ParallelStacks.Runtime" Version="$(ParallelStacksRuntimeVersion)" />
</ItemGroup>
-
+
<ItemGroup>
<ProjectReference Include="..\Microsoft.Diagnostics.DebugServices\Microsoft.Diagnostics.DebugServices.csproj" />
</ItemGroup>
+
+ <ItemGroup>
+ <Folder Include="ParallelStacks.Runtime\" />
+ </ItemGroup>
</Project>
--- /dev/null
+// 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.
+
+namespace ParallelStacks.Runtime
+{
+ /// <summary>
+ /// The method of this interface are called to render each part of the parallel call stacks
+ /// </summary>
+ /// <remarks>
+ /// Each method is responsible for adding color, tags or decoration on each element of the parallel stacks
+ /// </remarks>
+ public interface IRenderer
+ {
+ /// <summary>
+ /// Max number of thread IDs to display at the end of each stack frame group.
+ /// This is important in the case of 100+ threads applications.
+ /// </summary>
+ /// <remarks>
+ /// Use -1 if there should not be any limit.
+ /// </remarks>
+ int DisplayThreadIDsCountLimit { get; }
+
+ /// <summary>
+ /// Render empty line
+ /// </summary>
+ /// <param name="text"></param>
+ void Write(string text);
+
+ /// <summary>
+ /// Render count at the beginning of each line
+ /// </summary>
+ /// <param name="count"></param>
+ void WriteCount(string count);
+
+ /// <summary>
+ /// Render namespace of each method type
+ /// </summary>
+ /// <param name="ns"></param>
+ void WriteNamespace(string ns);
+
+ /// <summary>
+ /// Render each type in method signatures
+ /// </summary>
+ /// <param name="type"></param>
+ void WriteType(string type);
+
+ /// <summary>
+ /// Render separators such as ( and .
+ /// </summary>
+ /// <param name="separator"></param>
+ void WriteSeparator(string separator);
+
+ /// <summary>
+ /// Render dark signature element such as ByRef
+ /// </summary>
+ /// <param name="separator"></param>
+ void WriteDark(string separator);
+
+ /// <summary>
+ /// Render method name
+ /// </summary>
+ /// <param name="method"></param>
+ void WriteMethod(string method);
+
+ /// <summary>
+ /// Render method type (not including namespace)
+ /// </summary>
+ /// <param name="type"></param>
+ void WriteMethodType(string type);
+
+ /// <summary>
+ /// Render separator between different stack frame blocks
+ /// </summary>
+ /// <param name="text"></param>
+ void WriteFrameSeparator(string text);
+
+ /// <summary>
+ /// Render a thread id that will appear for each stack frames group (at the end of WriteFrameSeparator)
+ /// For example, in HTML it could be used to add a link to show details such as ClrStack -p
+ /// </summary>
+ /// <param name="threadID"></param>
+ string FormatTheadId(uint threadID);
+ }
+}
--- /dev/null
+// 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.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.InteropServices;
+using Microsoft.Diagnostics.Runtime;
+
+namespace ParallelStacks.Runtime
+{
+ 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.EnumerateStackTrace().Reverse())
+ {
+ if ((stackFrame.Kind != ClrStackFrameKind.ManagedMethod) || (stackFrame.Method == null))
+ continue;
+
+ stackFrames.Add(stackFrame);
+ }
+
+ if (stackFrames.Count == 0)
+ continue;
+
+ ps.AddStack(thread.OSThreadId, stackFrames.ToArray());
+ }
+
+ return ps;
+ }
+
+ public static ParallelStack Build(string dumpFile, string dacFilePath)
+ {
+ DataTarget dataTarget = null;
+ ParallelStack ps = null;
+ try
+ {
+ if (
+ (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) ||
+ (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
+ )
+ {
+ dataTarget = DataTarget.LoadDump(dumpFile);
+ }
+ else
+ {
+ throw new InvalidOperationException("Unsupported platform...");
+ }
+
+ var runtime = CreateRuntime(dataTarget, dacFilePath);
+ if (runtime == null)
+ {
+ return null;
+ }
+
+ ps = ParallelStack.Build(runtime);
+ }
+ finally
+ {
+ dataTarget?.Dispose();
+ }
+
+ return ps;
+ }
+
+ public static ParallelStack Build(int pid, string dacFilePath)
+ {
+ DataTarget dataTarget = null;
+ ParallelStack ps = null;
+ try
+ {
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ dataTarget = DataTarget.AttachToProcess(pid, true);
+ }
+ else
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
+ {
+ // ClrMD implementation for Linux is available only for Passive
+ dataTarget = DataTarget.AttachToProcess(pid, true);
+ }
+ else
+ {
+ throw new InvalidOperationException("Unsupported platform...");
+ }
+
+ var runtime = CreateRuntime(dataTarget, dacFilePath);
+ if (runtime == null)
+ {
+ return null;
+ }
+
+ ps = ParallelStack.Build(runtime);
+ }
+ finally
+ {
+ dataTarget?.Dispose();
+ }
+
+ return ps;
+ }
+
+ private static ClrRuntime CreateRuntime(DataTarget dataTarget, string dacFilePath)
+ {
+ // check bitness first
+ bool isTarget64Bit = (dataTarget.DataReader.PointerSize == 8);
+ if (Environment.Is64BitProcess != isTarget64Bit)
+ {
+ throw new InvalidOperationException(
+ $"Architecture mismatch: This tool is {(Environment.Is64BitProcess ? "64 bit" : "32 bit")} but target is {(isTarget64Bit ? "64 bit" : "32 bit")}");
+ }
+
+ var version = dataTarget.ClrVersions[0];
+ var runtime = (dacFilePath != null) ? version.CreateRuntime(dacFilePath) : version.CreateRuntime();
+ return runtime;
+ }
+
+ private ParallelStack(ClrStackFrame frame = null)
+ {
+ Stacks = new List<ParallelStack>();
+ ThreadIds = new List<uint>();
+ Frame = (frame == null) ? null : new StackFrame(frame);
+ }
+
+ public List<ParallelStack> Stacks { get; }
+
+ public StackFrame Frame { get; }
+
+ public List<uint> ThreadIds { get; set; }
+
+ private void AddStack(uint threadId, ClrStackFrame[] frames, int index = 0)
+ {
+ ThreadIds.Add(threadId);
+ var firstFrame = frames[index].Method?.Signature;
+ 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);
+ }
+ }
+}
--- /dev/null
+// 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.
+
+namespace ParallelStacks.Runtime
+{
+ public abstract class RendererBase : IRenderer
+ {
+ private readonly int _limit;
+
+ protected RendererBase(int limit)
+ {
+ _limit = limit;
+ }
+
+ public int DisplayThreadIDsCountLimit => _limit;
+
+ public abstract void Write(string text);
+ public abstract void WriteCount(string count);
+ public abstract void WriteNamespace(string ns);
+ public abstract void WriteType(string type);
+ public abstract void WriteSeparator(string separator);
+ public abstract void WriteDark(string separator);
+ public abstract void WriteMethod(string method);
+ public abstract void WriteMethodType(string type);
+ public abstract void WriteFrameSeparator(string text);
+ public abstract string FormatTheadId(uint threadID);
+ }
+}
--- /dev/null
+// 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.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace ParallelStacks.Runtime
+{
+ public static class RendererHelpers
+ {
+ public static void Render(this ParallelStack stacks, IRenderer visitor)
+ {
+ RenderStack(stacks, visitor);
+ }
+
+ private const int Padding = 5;
+ private static void RenderStack(ParallelStack stack, IRenderer visitor, int increment = 0)
+ {
+ var alignment = new string(' ', Padding * increment);
+ if (stack.Stacks.Count == 0)
+ {
+ var lastFrame = stack.Frame;
+ visitor.Write($"{Environment.NewLine}{alignment}");
+ visitor.WriteFrameSeparator($" ~~~~ {FormatThreadIdList(visitor, stack.ThreadIds)}");
+ visitor.WriteCount($"{Environment.NewLine}{alignment}{stack.ThreadIds.Count,Padding} ");
+
+ RenderFrame(lastFrame, visitor);
+ return;
+ }
+
+ foreach (var nextStackFrame in stack.Stacks.OrderBy(s => s.ThreadIds.Count))
+ {
+ RenderStack(nextStackFrame, visitor,
+ (nextStackFrame.ThreadIds.Count == stack.ThreadIds.Count) ? increment : increment + 1);
+ }
+
+ var currentFrame = stack.Frame;
+ visitor.WriteCount($"{Environment.NewLine}{alignment}{stack.ThreadIds.Count,Padding} ");
+ RenderFrame(currentFrame, visitor);
+ }
+
+ private static string FormatThreadIdList(IRenderer visitor, List<uint> threadIds)
+ {
+ var count = threadIds.Count;
+ var limit = visitor.DisplayThreadIDsCountLimit;
+ limit = Math.Min(count, limit);
+ if (limit < 0)
+ return string.Join(",", threadIds.Select(tid => visitor.FormatTheadId(tid)));
+ else
+ {
+ var result = string.Join(",", threadIds.GetRange(0, limit).Select(tid => visitor.FormatTheadId(tid)));
+ if (count > limit)
+ result += "...";
+ return result;
+ }
+ }
+
+ private static void RenderFrame(StackFrame frame, IRenderer visitor)
+ {
+ if (!string.IsNullOrEmpty(frame.TypeName))
+ {
+ var namespaces = frame.TypeName.Split('.');
+ for (int i = 0; i < namespaces.Length - 1; i++)
+ {
+ visitor.WriteNamespace(namespaces[i]);
+ visitor.WriteSeparator(".");
+ }
+ visitor.WriteMethodType(namespaces[namespaces.Length - 1]);
+ visitor.WriteSeparator(".");
+ }
+
+ visitor.WriteMethod(frame.MethodName);
+ visitor.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)
+ {
+ visitor.WriteType(parameter.Substring(0, pos));
+ visitor.WriteDark(" ByRef");
+ }
+ else
+ {
+ visitor.WriteType(parameter);
+ }
+ if (current < parameters.Count - 1) visitor.WriteSeparator(", ");
+ }
+ visitor.WriteSeparator(")");
+ }
+
+
+ }
+}
--- /dev/null
+// 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.
+
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Text;
+using Microsoft.Diagnostics.Runtime;
+
+namespace ParallelStacks
+{
+ public class StackFrame
+ {
+ public string TypeName { get; private set; }
+ public string MethodName { get; private set; }
+ public List<string> Signature { get; }
+
+ public string Text { get; }
+
+ public StackFrame(ClrStackFrame frame)
+ {
+ var signature = frame.Method?.Signature;
+ Text = string.IsNullOrEmpty(signature) ? "?" : string.Intern(signature);
+ Signature = new List<string>();
+ ComputeNames(frame);
+ }
+
+ private void ComputeNames(ClrStackFrame frame)
+ {
+ // start by parsing (short)type name
+ var typeName = frame.Method.Type.Name;
+ if (string.IsNullOrEmpty(typeName))
+ {
+ // IL generated frames
+ TypeName = string.Empty;
+ }
+ else
+ {
+ TypeName = typeName;
+ }
+
+ // generic methods are not well formatted by ClrMD
+ // foo<...>() => foo[[...]]()
+ var fullName = frame.Method?.Signature;
+ MethodName = frame.Method.Name;
+ if (MethodName.EndsWith("]]"))
+ {
+ // fix ClrMD bug with method name
+ MethodName = GetGenericMethodName(fullName);
+ }
+
+ Signature.AddRange(BuildSignature(fullName));
+ }
+
+ public static string GetShortTypeName(string typeName, int start, int end)
+ {
+ return GetNextTypeName(typeName, ref start, ref end);
+ }
+
+ // this helper is called in 2 situations to analyze a method signature parameter:
+ // - compute the next type in a generic definition
+ // - start from a full type name
+ // in all cases:
+ // - end = the index of the last character (could be far beyond the end of the next type name in case of generic)
+ // - start = first character of the type name
+ public static string GetNextTypeName(string typeName, ref int start, ref int end)
+ {
+ if (string.IsNullOrEmpty(typeName))
+ return string.Empty;
+
+ var sb = new StringBuilder();
+
+ // need to make the difference between generic and non generic parameters
+ // *.Int32 --> Int32
+ // *.IList`1<*.String> --> IList + continue on *.String>
+ // *.IDictionary`2<*.Int32,*.IList`1<*.String>> --> IDictionary + continue on *.Int32,*.IList`1<*.String>>
+ // *.Int32,*.IList`1<*.String>> --> Int32 + continue on *.IList`1<*.String>>
+ // *.Int32> --> Int32
+ // 1. look for generic
+ // 2. if not, look for , as separator of generic parameters
+ var pos = typeName.IndexOf('`', start, end - start);
+ var next = typeName.IndexOf(',', start, end - start);
+
+ // simple case of 1 type name (with maybe no namespace)
+ if ((pos == -1) && (next == -1))
+ {
+ AppendTypeNameWithoutNamespace(sb, typeName, start, end);
+
+ // it was the last type name
+ start = end;
+
+ return sb.ToString();
+ }
+
+ // this is the last type
+ if (next == -1)
+ {
+ // *.IList`1<...>,xxx
+ // *.IList`1<xxx,...>
+ return GetGenericTypeName(typeName, ref start, ref end);
+ }
+
+ // at least 1 type name (even before a generic type)
+ if (pos == -1)
+ {
+ // *.Int32,xxx with xxx could contain a generic
+ AppendTypeNameWithoutNamespace(sb, typeName, start, next-1);
+
+ // skip this type
+ start = next + 1;
+
+ return sb.ToString();
+ }
+
+ // a generic type before another type or a generic type with more than 1 parameter
+ if (pos < next)
+ {
+ // *.IList`1<...>,xxx
+ // *.IList`1<xxx,...>
+ return GetGenericTypeName(typeName, ref start, ref end);
+ }
+
+ // a non generic type before another type parameter
+ // *.Int32,xxx
+ AppendTypeNameWithoutNamespace(sb, typeName, start, next-1);
+
+ // skip this type
+ start = next + 1;
+
+ return sb.ToString();
+ }
+
+ public static string GetGenericTypeName(string typeName, ref int start, ref int end)
+ {
+ // System.Collections.Generic.IList`1<System.Collections.Generic.IEnumerable`1<System.String>>
+ // System.Collections.Generic.IDictionary`2<Int32,System.String>
+ var sb = new StringBuilder();
+
+ // look for ` to get the name and the count of generic parameters
+ var pos = typeName.IndexOf('`', start, end - start);
+
+ // build the name V-- don't want ` in the name
+ AppendTypeNameWithoutNamespace(sb, typeName, start, pos-1);
+ sb.Append('<');
+
+ // go to the first generic parameter
+ start = typeName.IndexOf('<', pos, end - pos) + 1;
+
+ // get each generic parameter
+ while (start < end)
+ {
+ var genericParameter = GetNextTypeName(typeName, ref start, ref end);
+ sb.Append(genericParameter);
+ if (start < end)
+ {
+ sb.Append(',');
+ }
+ }
+
+ return sb.ToString();
+ }
+
+ public static void AppendTypeNameWithoutNamespace(StringBuilder sb, string typeName, int start, int end)
+ {
+ var pos = typeName.LastIndexOf('.', end, end - start);
+ if (pos == -1)
+ { // no namespace
+ sb.Append(typeName, start, end - start + 1);
+ }
+ else
+ {
+ // skip the namespace
+ sb.Append(typeName, pos + 1, end - pos);
+ }
+ }
+
+ public static IEnumerable<string> BuildSignature(string fullName)
+ {
+ // {namespace.}type.method[[]](..., ..., ...)
+ var parameters = new List<string>();
+ var pos = fullName.LastIndexOf('(');
+ if (pos == -1)
+ {
+ return parameters;
+ }
+
+ // look for each parameter, one after the other
+ int next = pos;
+ string parameter = string.Empty;
+ while (next != (fullName.Length - 1))
+ {
+ next = fullName.IndexOf(", ", pos);
+ if (next == -1)
+ {
+ next = fullName.IndexOf(')'); // should be the last character of the string
+ Debug.Assert(next == fullName.Length - 1);
+ }
+
+ // skip . ,
+ parameter = GetParameter(fullName, pos + 1, next - 1);
+ if (parameter != null) parameters.Add(parameter);
+
+ pos = next + 1;
+ }
+
+ return parameters;
+ }
+
+ public static string GetParameter(string fullName, int start, int end)
+ {
+ const string BYREF = " ByRef";
+ // () no parameter
+ if (start >= end)
+ return null;
+
+ var sb = new StringBuilder();
+
+ // handle ByRef case
+ var isByRef = false;
+ if (fullName.LastIndexOf(BYREF, end) == end - BYREF.Length)
+ {
+ isByRef = true;
+ end -= BYREF.Length;
+ }
+
+ var typeName = GetShortTypeName(fullName, start, end);
+ sb.Append(typeName);
+
+ if (isByRef)
+ sb.Append(BYREF);
+
+ return sb.ToString();
+ }
+
+ public static string GetGenericMethodName(string fullName)
+ {
+ // foo[[...]] --> foo<...>
+ // namespace.type.Foo[[System.String, Int32]](System.Collections.Generic.IDictionary`2<Int32,System.String>)
+ var pos = fullName.IndexOf("[[");
+ if (pos == -1)
+ {
+ return fullName;
+ }
+
+ var start = fullName.LastIndexOf('.', pos);
+ return fullName.Substring(start + 1, pos - start - 1);
+ }
+ }
+}