From 6c09155943e048e24d27e57f0b9fd90d192b7989 Mon Sep 17 00:00:00 2001 From: chrisnas Date: Wed, 17 Mar 2021 02:28:16 +0100 Subject: [PATCH] Remove ParallelStack.Runtime nuget reference and copy its implementation in dotnet-dump instead. (#2088) Authored-by: Christophe Nasarre --- ...osoft.Diagnostics.ExtensionCommands.csproj | 9 +- .../ParallelStacks.Runtime/IRenderer.cs | 85 ++++++ .../ParallelStacks.Runtime/ParallelStack.cs | 158 +++++++++++ .../ParallelStacks.Runtime/RendererBase.cs | 29 ++ .../ParallelStacks.Runtime/RendererHelpers.cs | 99 +++++++ .../ParallelStacks.Runtime/StackFrame.cs | 249 ++++++++++++++++++ 6 files changed, 626 insertions(+), 3 deletions(-) create mode 100644 src/Microsoft.Diagnostics.ExtensionCommands/ParallelStacks.Runtime/IRenderer.cs create mode 100644 src/Microsoft.Diagnostics.ExtensionCommands/ParallelStacks.Runtime/ParallelStack.cs create mode 100644 src/Microsoft.Diagnostics.ExtensionCommands/ParallelStacks.Runtime/RendererBase.cs create mode 100644 src/Microsoft.Diagnostics.ExtensionCommands/ParallelStacks.Runtime/RendererHelpers.cs create mode 100644 src/Microsoft.Diagnostics.ExtensionCommands/ParallelStacks.Runtime/StackFrame.cs diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/Microsoft.Diagnostics.ExtensionCommands.csproj b/src/Microsoft.Diagnostics.ExtensionCommands/Microsoft.Diagnostics.ExtensionCommands.csproj index 3c8adc2a5..4a47509c5 100644 --- a/src/Microsoft.Diagnostics.ExtensionCommands/Microsoft.Diagnostics.ExtensionCommands.csproj +++ b/src/Microsoft.Diagnostics.ExtensionCommands/Microsoft.Diagnostics.ExtensionCommands.csproj @@ -12,13 +12,16 @@ true false - + - - + + + + + diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/ParallelStacks.Runtime/IRenderer.cs b/src/Microsoft.Diagnostics.ExtensionCommands/ParallelStacks.Runtime/IRenderer.cs new file mode 100644 index 000000000..2cffd9016 --- /dev/null +++ b/src/Microsoft.Diagnostics.ExtensionCommands/ParallelStacks.Runtime/IRenderer.cs @@ -0,0 +1,85 @@ +// 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 +{ + /// + /// The method of this interface are called to render each part of the parallel call stacks + /// + /// + /// Each method is responsible for adding color, tags or decoration on each element of the parallel stacks + /// + public interface IRenderer + { + /// + /// 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. + /// + /// + /// Use -1 if there should not be any limit. + /// + int DisplayThreadIDsCountLimit { get; } + + /// + /// Render empty line + /// + /// + void Write(string text); + + /// + /// Render count at the beginning of each line + /// + /// + void WriteCount(string count); + + /// + /// Render namespace of each method type + /// + /// + void WriteNamespace(string ns); + + /// + /// Render each type in method signatures + /// + /// + void WriteType(string type); + + /// + /// Render separators such as ( and . + /// + /// + void WriteSeparator(string separator); + + /// + /// Render dark signature element such as ByRef + /// + /// + void WriteDark(string separator); + + /// + /// Render method name + /// + /// + void WriteMethod(string method); + + /// + /// Render method type (not including namespace) + /// + /// + void WriteMethodType(string type); + + /// + /// Render separator between different stack frame blocks + /// + /// + void WriteFrameSeparator(string text); + + /// + /// 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 + /// + /// + string FormatTheadId(uint threadID); + } +} diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/ParallelStacks.Runtime/ParallelStack.cs b/src/Microsoft.Diagnostics.ExtensionCommands/ParallelStacks.Runtime/ParallelStack.cs new file mode 100644 index 000000000..247809628 --- /dev/null +++ b/src/Microsoft.Diagnostics.ExtensionCommands/ParallelStacks.Runtime/ParallelStack.cs @@ -0,0 +1,158 @@ +// 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(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(); + ThreadIds = new List(); + Frame = (frame == null) ? null : new StackFrame(frame); + } + + public List Stacks { get; } + + public StackFrame Frame { get; } + + public List 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); + } + } +} diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/ParallelStacks.Runtime/RendererBase.cs b/src/Microsoft.Diagnostics.ExtensionCommands/ParallelStacks.Runtime/RendererBase.cs new file mode 100644 index 000000000..8bd26df8a --- /dev/null +++ b/src/Microsoft.Diagnostics.ExtensionCommands/ParallelStacks.Runtime/RendererBase.cs @@ -0,0 +1,29 @@ +// 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); + } +} diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/ParallelStacks.Runtime/RendererHelpers.cs b/src/Microsoft.Diagnostics.ExtensionCommands/ParallelStacks.Runtime/RendererHelpers.cs new file mode 100644 index 000000000..4fbd5a0f1 --- /dev/null +++ b/src/Microsoft.Diagnostics.ExtensionCommands/ParallelStacks.Runtime/RendererHelpers.cs @@ -0,0 +1,99 @@ +// 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 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(")"); + } + + + } +} diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/ParallelStacks.Runtime/StackFrame.cs b/src/Microsoft.Diagnostics.ExtensionCommands/ParallelStacks.Runtime/StackFrame.cs new file mode 100644 index 000000000..1ee3a5630 --- /dev/null +++ b/src/Microsoft.Diagnostics.ExtensionCommands/ParallelStacks.Runtime/StackFrame.cs @@ -0,0 +1,249 @@ +// 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 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(); + 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 + 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 + 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.IDictionary`2 + 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 BuildSignature(string fullName) + { + // {namespace.}type.method[[]](..., ..., ...) + var parameters = new List(); + 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) + var pos = fullName.IndexOf("[["); + if (pos == -1) + { + return fullName; + } + + var start = fullName.LastIndexOf('.', pos); + return fullName.Substring(start + 1, pos - start - 1); + } + } +} -- 2.34.1