From c5aaaf3feb89352e9911d01d94ada6d04a9da2a2 Mon Sep 17 00:00:00 2001 From: Ellie Kornstaedt Date: Thu, 29 Feb 2024 18:42:14 -0800 Subject: [PATCH] Add support for structured output Summary: When running in automation, we'll want to report analysis results to our performance database. To find analysis results (such as number of live bytes on the heap), we don't want our automation drivers to have to parse the textual output of MemorySnapshotAnalyzer. Therefore, this diff adds a command line option that makes MemorySnapshotAnalyzer write out, before it exits, everything it did as a JSON file ("structured output"). This JSON file will be easier to interpret by out automation than textual (human-readable, aka "display string") output. Differential Revision: D54330870 fbshipit-source-id: 7b09c4a0b50181da141c608d7062f430ae41dc5d --- AbstractMemorySnapshot/HeapSegment.cs | 15 +- AbstractMemorySnapshot/IStructuredOutput.cs | 38 ++++ AbstractMemorySnapshot/MemoryView.cs | 14 +- AbstractMemorySnapshot/TraceableHeap.cs | 2 +- AbstractMemorySnapshot/TypeSystem.cs | 18 +- .../TestSegmentedTraceableHeap.cs | 4 +- AbstractMemorySnapshotTests/TestTypeSystem.cs | 2 +- AbstractMemorySnapshotTests/TypeSystemTest.cs | 4 +- Analysis/Backtracer.cs | 15 +- Analysis/GroupingBacktracer.cs | 27 ++- Analysis/IBacktracer.cs | 3 +- Analysis/IRootSet.cs | 4 +- Analysis/RootSet.cs | 13 +- Analysis/SingletonRootSet.cs | 6 +- Analysis/StitchedTraceableHeap.cs | 6 +- Analysis/StitchedTypeSystem.cs | 12 +- Analysis/TracedHeap.cs | 8 +- AnalysisTests/BacktracerTest.cs | 11 +- AnalysisTests/MockStructuredOutput.cs | 70 ++++++++ AnalysisTests/MockTraceableHeap.cs | 2 +- CommandInfrastructure/Command.cs | 147 +++++++++++---- CommandInfrastructure/CommandLineArgument.cs | 21 ++- CommandInfrastructure/ConsoleOutput.cs | 6 +- CommandInfrastructure/Context.cs | 99 +++++++++- CommandInfrastructure/FileOutput.cs | 4 +- CommandInfrastructure/JsonStructuredOutput.cs | 170 ++++++++++++++++++ .../PassthroughStructuredOutput.cs | 78 ++++++++ CommandInfrastructure/Repl.cs | 67 +++++-- Commands/BacktraceCommand.cs | 69 +++++-- Commands/ClearConsoleCommand.cs | 4 +- Commands/DescribeCommand.cs | 4 +- Commands/DumpAssembliesCommand.cs | 10 +- Commands/DumpCommand.cs | 38 ++-- Commands/DumpInvalidReferencesCommand.cs | 37 +++- Commands/DumpObjectCommand.cs | 43 ++++- Commands/DumpRootsCommand.cs | 13 +- Commands/DumpSegmentCommand.cs | 22 ++- Commands/DumpTypeCommand.cs | 64 +++++-- Commands/FindCommand.cs | 20 ++- Commands/HeapDomCommand.cs | 43 ++--- Commands/HeapDomStatsCommand.cs | 16 +- Commands/ListObjectsCommand.cs | 50 +++++- Commands/ListSegmentsCommand.cs | 21 ++- Commands/LoadCommand.cs | 4 +- Commands/PrintCommand.cs | 12 +- Commands/ReferenceClassifierCommand.cs | 51 ++++-- Commands/StatsCommand.cs | 61 +++++-- MemorySnapshotAnalyzer/Program.cs | 41 ++++- .../ReferenceClassifierStore.cs | 3 +- UnityBackend/UnityManagedHeap.cs | 4 +- UnityBackend/UnityManagedTypeSystem.cs | 25 ++- UnityBackend/UnityNativeObjectHeap.cs | 2 +- UnityBackend/UnityNativeObjectTypeSystem.cs | 7 +- 53 files changed, 1261 insertions(+), 269 deletions(-) create mode 100644 AbstractMemorySnapshot/IStructuredOutput.cs create mode 100644 AnalysisTests/MockStructuredOutput.cs create mode 100644 CommandInfrastructure/JsonStructuredOutput.cs create mode 100644 CommandInfrastructure/PassthroughStructuredOutput.cs diff --git a/AbstractMemorySnapshot/HeapSegment.cs b/AbstractMemorySnapshot/HeapSegment.cs index 41af359..4eeadd7 100644 --- a/AbstractMemorySnapshot/HeapSegment.cs +++ b/AbstractMemorySnapshot/HeapSegment.cs @@ -1,10 +1,12 @@ -/* +/* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ +using System.Text; + namespace MemorySnapshotAnalyzer.AbstractMemorySnapshot { public sealed class HeapSegment @@ -30,6 +32,17 @@ public HeapSegment(NativeWord startAddress, MemoryView memoryView, bool isRuntim public bool IsRuntimeTypeInformation => m_isRuntimeTypeInformation; + public void Describe(IStructuredOutput output, StringBuilder sb) + { + output.AddProperty("startAddress", m_startAddress.ToString()); + output.AddProperty("isRtti", m_isRuntimeTypeInformation.ToString()); + sb.AppendFormat("{0} segment at {1} (", + m_isRuntimeTypeInformation ? "runtime type information" : "managed heap", + m_startAddress); + m_memoryView.Describe(output, sb); + sb.Append(')'); + } + public override string ToString() { return string.Format("{0} segment at {1} ({2})", diff --git a/AbstractMemorySnapshot/IStructuredOutput.cs b/AbstractMemorySnapshot/IStructuredOutput.cs new file mode 100644 index 0000000..bda51db --- /dev/null +++ b/AbstractMemorySnapshot/IStructuredOutput.cs @@ -0,0 +1,38 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +namespace MemorySnapshotAnalyzer.AbstractMemorySnapshot +{ + public interface IStructuredOutput + { + void AddProperty(string key, string value); + + void AddProperty(string key, long value); + + void AddDisplayString(string message); + + void AddDisplayString(string format, params object[] args); + + void AddDisplayStringLine(string message); + + void AddDisplayStringLine(string format, params object[] args); + + void AddDisplayStringLineIndented(int indent, string format, params object[] args); + + void BeginArray(string key); + + void BeginElement(); + + void EndElement(); + + void EndArray(); + + void BeginChild(string key); + + void EndChild(); + } +} diff --git a/AbstractMemorySnapshot/MemoryView.cs b/AbstractMemorySnapshot/MemoryView.cs index 5d9e4d8..1cff64f 100644 --- a/AbstractMemorySnapshot/MemoryView.cs +++ b/AbstractMemorySnapshot/MemoryView.cs @@ -1,4 +1,4 @@ -/* +/* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the @@ -29,6 +29,18 @@ public override string ToString() return $"{m_memoryAccessor} offset {m_offset} size 0x{m_size:X} ({m_size})"; } + public void Describe(IStructuredOutput output, StringBuilder sb) + { + output.AddProperty("memoryAccessor", m_memoryAccessor.ToString()!); + output.AddProperty("offset", m_offset); + output.AddProperty("size", m_size); + sb.AppendFormat("{0} offset {1} size 0x{2:X} ({3})", + m_memoryAccessor, + m_offset, + m_size, + m_size); + } + public long Size => m_size; public bool IsValid => m_size > 0; diff --git a/AbstractMemorySnapshot/TraceableHeap.cs b/AbstractMemorySnapshot/TraceableHeap.cs index a151a9f..00c44bf 100644 --- a/AbstractMemorySnapshot/TraceableHeap.cs +++ b/AbstractMemorySnapshot/TraceableHeap.cs @@ -49,7 +49,7 @@ public TraceableHeap(TypeSystem typeSystem) public abstract bool ContainsAddress(NativeWord address); - public abstract string? DescribeAddress(NativeWord address); + public abstract string? DescribeAddress(NativeWord address, IStructuredOutput output); public abstract SegmentedHeap? SegmentedHeapOpt { get; } } diff --git a/AbstractMemorySnapshot/TypeSystem.cs b/AbstractMemorySnapshot/TypeSystem.cs index d673987..e90a38e 100644 --- a/AbstractMemorySnapshot/TypeSystem.cs +++ b/AbstractMemorySnapshot/TypeSystem.cs @@ -97,6 +97,11 @@ static int ComputeArity(ReadOnlySpan genericArguments) public abstract bool IsArray(int typeIndex); + public string Kind(int typeIndex) + { + return IsValueType(typeIndex) ? "value" : IsArray(typeIndex) ? "array" : "object"; + } + public abstract int Rank(int typeIndex); public abstract int NumberOfFields(int typeIndex); @@ -123,7 +128,18 @@ static int ComputeArity(ReadOnlySpan genericArguments) public abstract int SystemVoidStarTypeIndex { get; } - public abstract IEnumerable DumpStats(); + public abstract IEnumerable DumpStats(IStructuredOutput output); + + public void OutputType(IStructuredOutput output, string key, int typeIndex) + { + output.BeginChild(key); + + output.AddProperty("assembly", Assembly(typeIndex)); + output.AddProperty("qualifiedName", QualifiedName(typeIndex)); + output.AddProperty("typeIndex", typeIndex); + + output.EndChild(); + } ValueTuple EnsurePointerOffsets(int typeIndex) { diff --git a/AbstractMemorySnapshotTests/TestSegmentedTraceableHeap.cs b/AbstractMemorySnapshotTests/TestSegmentedTraceableHeap.cs index 9ef2745..0c1bb11 100644 --- a/AbstractMemorySnapshotTests/TestSegmentedTraceableHeap.cs +++ b/AbstractMemorySnapshotTests/TestSegmentedTraceableHeap.cs @@ -66,11 +66,13 @@ public override int GetObjectSize(NativeWord objectAddress, int typeIndex, bool throw new System.NotImplementedException(); } - public override string? DescribeAddress(NativeWord address) + public override string? DescribeAddress(NativeWord address, IStructuredOutput output) { int typeInfoIndex = TypeInfoAddressToIndex(address); if (typeInfoIndex != -1) { + output.AddProperty("addressTargetKind", "vtable"); + TypeSystem.OutputType(output, "vtableType", typeInfoIndex); return string.Format("VTable[{0}, type index {1}]", TypeSystem.QualifiedName(typeInfoIndex), typeInfoIndex); diff --git a/AbstractMemorySnapshotTests/TestTypeSystem.cs b/AbstractMemorySnapshotTests/TestTypeSystem.cs index bc438ae..421f682 100644 --- a/AbstractMemorySnapshotTests/TestTypeSystem.cs +++ b/AbstractMemorySnapshotTests/TestTypeSystem.cs @@ -417,7 +417,7 @@ public override int GetArrayElementSize(int elementTypeIndex) public override int SystemVoidStarTypeIndex => throw new NotImplementedException(); - public override IEnumerable DumpStats() + public override IEnumerable DumpStats(IStructuredOutput output) { throw new NotImplementedException(); } diff --git a/AbstractMemorySnapshotTests/TypeSystemTest.cs b/AbstractMemorySnapshotTests/TypeSystemTest.cs index 0627cca..747a848 100644 --- a/AbstractMemorySnapshotTests/TypeSystemTest.cs +++ b/AbstractMemorySnapshotTests/TypeSystemTest.cs @@ -43,7 +43,7 @@ public void TestQualifiedGenericNameWithArity() Is.EqualTo("GenericTypeWithNesting`2")); Assert.That(m_typeSystem!.QualifiedGenericNameWithArity((int)TestTypeIndex.EmptyTypeNameCornerCase), - Is.EqualTo("")); + Is.EqualTo(string.Empty)); } [Test] @@ -317,7 +317,7 @@ public void TestBindSelector() Assert.That(selector.StaticPrefix, Has.Exactly(0).Items); Assert.That(selector.DynamicTail, Is.Null); - Assert.That(selector.Stringify(m_typeSystem!, pathIndex: 0, inStaticPrefix: true), Is.EqualTo("")); + Assert.That(selector.Stringify(m_typeSystem!, pathIndex: 0, inStaticPrefix: true), Is.EqualTo(string.Empty)); }); // Base case: single field diff --git a/Analysis/Backtracer.cs b/Analysis/Backtracer.cs index 2e83103..a695d6a 100644 --- a/Analysis/Backtracer.cs +++ b/Analysis/Backtracer.cs @@ -97,14 +97,18 @@ public int PostorderIndexToNodeIndex(int postorderIndex) return postorderIndex; } - public string DescribeNodeIndex(int nodeIndex, bool fullyQualified) + public string DescribeNodeIndex(int nodeIndex, IStructuredOutput output, bool fullyQualified) { + output.AddProperty("nodeIndex", nodeIndex); + if (nodeIndex == m_rootNodeIndex) { + output.AddProperty("nodeKind", "process"); return "Process"; } else if (nodeIndex == m_unreachableNodeIndex) { + output.AddProperty("nodeKind", "unreachable"); return "Unreachable"; } @@ -112,19 +116,23 @@ public string DescribeNodeIndex(int nodeIndex, bool fullyQualified) int typeIndex = m_tracedHeap.PostorderTypeIndexOrSentinel(postorderIndex); if (typeIndex == -1) { + output.AddProperty("nodeKind", "root"); List<(int rootIndex, PointerInfo pointerFlags)> rootInfos = m_tracedHeap.PostorderRootIndices(nodeIndex); if (rootInfos.Count == 1) { - return m_rootSet.DescribeRoot(rootInfos[0].rootIndex, fullyQualified); + return m_rootSet.DescribeRoot(rootInfos[0].rootIndex, output, fullyQualified); } else { var sb = new StringBuilder(); - m_tracedHeap.DescribeRootIndices(nodeIndex, sb); + m_tracedHeap.DescribeRootIndices(nodeIndex, sb, output); return sb.ToString(); } } + output.AddProperty("nodeKind", "object"); + output.AddProperty("objectIndex", postorderIndex); + m_traceableHeap.TypeSystem.OutputType(output, "objectType", typeIndex); string typeName = fullyQualified ? $"{m_traceableHeap.TypeSystem.Assembly(typeIndex)}:{m_traceableHeap.TypeSystem.QualifiedName(typeIndex)}" : m_traceableHeap.TypeSystem.UnqualifiedName(typeIndex); @@ -132,6 +140,7 @@ public string DescribeNodeIndex(int nodeIndex, bool fullyQualified) string? objectName = m_traceableHeap.GetObjectName(m_tracedHeap.PostorderAddress(postorderIndex)); if (objectName != null) { + output.AddProperty("objectName", objectName); return string.Format("{0}('{1}')#{2}", typeName, objectName, diff --git a/Analysis/GroupingBacktracer.cs b/Analysis/GroupingBacktracer.cs index 72e735f..f097f3b 100644 --- a/Analysis/GroupingBacktracer.cs +++ b/Analysis/GroupingBacktracer.cs @@ -8,6 +8,7 @@ using MemorySnapshotAnalyzer.AbstractMemorySnapshot; using System; using System.Collections.Generic; +using System.Reflection; namespace MemorySnapshotAnalyzer.Analysis { @@ -148,33 +149,45 @@ int IBacktracer.PostorderIndexToNodeIndex(int postorderIndex) return m_parentBacktracer.PostorderIndexToNodeIndex(postorderIndex); } - string IBacktracer.DescribeNodeIndex(int nodeIndex, bool fullyQualified) + string IBacktracer.DescribeNodeIndex(int nodeIndex, IStructuredOutput output, bool fullyQualified) { if (nodeIndex == m_rootNodeIndex) { - return m_parentBacktracer.DescribeNodeIndex(m_parentBacktracer.RootNodeIndex, fullyQualified); + return m_parentBacktracer.DescribeNodeIndex(m_parentBacktracer.RootNodeIndex, output, fullyQualified); } else if (nodeIndex == m_unreachableNodeIndex) { - return m_parentBacktracer.DescribeNodeIndex(m_parentBacktracer.UnreachableNodeIndex, fullyQualified); + return m_parentBacktracer.DescribeNodeIndex(m_parentBacktracer.UnreachableNodeIndex, output, fullyQualified); } else if (nodeIndex >= m_firstAssemblyIndex) { - return $"{m_assemblyNames[nodeIndex - m_firstAssemblyIndex]}#{nodeIndex}"; + string assemblyName = m_assemblyNames[nodeIndex - m_firstAssemblyIndex]; + output.AddProperty("nodeKind", "assembly"); + output.AddProperty("nodeIndex", nodeIndex); + output.AddProperty("assemblyName", assemblyName); + return $"{assemblyName}#{nodeIndex}"; } else if (nodeIndex >= m_firstNamespaceIndex) { int namespaceIndex = nodeIndex - m_firstNamespaceIndex; - return $"{m_namespaceNames[namespaceIndex]}#{nodeIndex}"; + string namespaceName = m_namespaceNames[namespaceIndex]; + output.AddProperty("nodeKind", "namespace"); + output.AddProperty("nodeIndex", nodeIndex); + output.AddProperty("namespaceName", namespaceName); + return $"{namespaceName}#{nodeIndex}"; } else if (nodeIndex >= m_firstClassIndex) { int classIndex = nodeIndex - m_firstClassIndex; - return $"{m_classNames[classIndex]}#{nodeIndex}"; + string className = m_classNames[classIndex]; + output.AddProperty("nodeKind", "class"); + output.AddProperty("nodeIndex", nodeIndex); + output.AddProperty("className", className); + return $"{className}#{nodeIndex}"; } else { - return m_parentBacktracer.DescribeNodeIndex(nodeIndex, fullyQualified); + return m_parentBacktracer.DescribeNodeIndex(nodeIndex, output, fullyQualified); } } diff --git a/Analysis/IBacktracer.cs b/Analysis/IBacktracer.cs index 56ea66e..c0884bd 100644 --- a/Analysis/IBacktracer.cs +++ b/Analysis/IBacktracer.cs @@ -6,6 +6,7 @@ */ using System.Collections.Generic; +using MemorySnapshotAnalyzer.AbstractMemorySnapshot; namespace MemorySnapshotAnalyzer.Analysis { @@ -27,7 +28,7 @@ public interface IBacktracer int PostorderIndexToNodeIndex(int postorderIndex); - string DescribeNodeIndex(int nodeIndex, bool fullyQualified); + string DescribeNodeIndex(int nodeIndex, IStructuredOutput output, bool fullyQualified); string NodeType(int nodeIndex); diff --git a/Analysis/IRootSet.cs b/Analysis/IRootSet.cs index dfd47c7..7800272 100644 --- a/Analysis/IRootSet.cs +++ b/Analysis/IRootSet.cs @@ -1,4 +1,4 @@ -/* +/* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the @@ -30,7 +30,7 @@ public struct StaticRootInfo bool IsGCHandle(int rootIndex); - string DescribeRoot(int rootIndex, bool fullyQualified); + string DescribeRoot(int rootIndex, IStructuredOutput output, bool fullyQualified); StaticRootInfo GetStaticRootInfo(int rootIndex); } diff --git a/Analysis/RootSet.cs b/Analysis/RootSet.cs index faa2c4a..bd18f13 100644 --- a/Analysis/RootSet.cs +++ b/Analysis/RootSet.cs @@ -1,4 +1,4 @@ -/* +/* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the @@ -109,22 +109,29 @@ bool IRootSet.IsGCHandle(int rootIndex) return m_roots[rootIndex].PointerInfo.TypeIndex == -1; } - string IRootSet.DescribeRoot(int rootIndex, bool fullyQualified) + string IRootSet.DescribeRoot(int rootIndex, IStructuredOutput output, bool fullyQualified) { RootEntry entry = m_roots[rootIndex]; int typeIndex = entry.PointerInfo.TypeIndex; if (typeIndex == -1) { + output.AddProperty("rootKind", "gcHandle"); + output.AddProperty("rootNumber", entry.PointerInfo.FieldNumber); return $"GCHandle#{entry.PointerInfo.FieldNumber}"; } else { + output.AddProperty("rootKind", "staticVariable"); + m_traceableHeap.TypeSystem.OutputType(output, "containingType", typeIndex); string typeName = fullyQualified ? $"{m_traceableHeap.TypeSystem.Assembly(typeIndex)}:{m_traceableHeap.TypeSystem.QualifiedName(typeIndex)}" : m_traceableHeap.TypeSystem.UnqualifiedName(typeIndex); + string fieldName = m_traceableHeap.TypeSystem.FieldName(typeIndex, entry.PointerInfo.FieldNumber); + output.AddProperty("fieldName", fieldName); + output.AddProperty("offset", entry.Offset); return string.Format("{0}.{1}+0x{2:X}", typeName, - m_traceableHeap.TypeSystem.FieldName(typeIndex, entry.PointerInfo.FieldNumber), + fieldName, entry.Offset); } } diff --git a/Analysis/SingletonRootSet.cs b/Analysis/SingletonRootSet.cs index 0d04ec9..c16e0f0 100644 --- a/Analysis/SingletonRootSet.cs +++ b/Analysis/SingletonRootSet.cs @@ -1,4 +1,4 @@ -/* +/* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the @@ -44,8 +44,10 @@ bool IRootSet.IsGCHandle(int rootIndex) return false; } - string IRootSet.DescribeRoot(int rootIndex, bool fullyQualified) + string IRootSet.DescribeRoot(int rootIndex, IStructuredOutput output, bool fullyQualified) { + output.AddProperty("rootTargetKind", "object"); + output.AddProperty("rootTargetAddress", m_address.ToString()); return $"Object@{m_address}"; } diff --git a/Analysis/StitchedTraceableHeap.cs b/Analysis/StitchedTraceableHeap.cs index 0faaed1..901397a 100644 --- a/Analysis/StitchedTraceableHeap.cs +++ b/Analysis/StitchedTraceableHeap.cs @@ -181,10 +181,10 @@ public override bool ContainsAddress(NativeWord address) return m_secondary.ContainsAddress(address) || base.ContainsAddress(address); } - public override string? DescribeAddress(NativeWord address) + public override string? DescribeAddress(NativeWord address, IStructuredOutput output) { - string? secondaryDescription = m_secondary.DescribeAddress(address); - string? primaryDescription = m_primary.DescribeAddress(address); + string? secondaryDescription = m_secondary.DescribeAddress(address, output); + string? primaryDescription = m_primary.DescribeAddress(address, output); if (secondaryDescription != null && primaryDescription != null) { return $"{primaryDescription}/{secondaryDescription}"; diff --git a/Analysis/StitchedTypeSystem.cs b/Analysis/StitchedTypeSystem.cs index 28c1d56..bb7e11f 100644 --- a/Analysis/StitchedTypeSystem.cs +++ b/Analysis/StitchedTypeSystem.cs @@ -1,4 +1,4 @@ -/* +/* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the @@ -275,17 +275,21 @@ public override int GetArrayElementSize(int elementTypeIndex) public override int SystemVoidStarTypeIndex => m_systemVoidStarTypeIndex; - public override IEnumerable DumpStats() + public override IEnumerable DumpStats(IStructuredOutput output) { - foreach (var s in m_first.DumpStats()) + output.BeginChild("stichedFirst"); + foreach (var s in m_first.DumpStats(output)) { yield return s; } + output.EndChild(); - foreach (var s in m_second.DumpStats()) + output.BeginChild("stitchedSecond"); + foreach (var s in m_second.DumpStats(output)) { yield return s; } + output.EndChild(); } } } diff --git a/Analysis/TracedHeap.cs b/Analysis/TracedHeap.cs index 555df98..2aa4e2c 100644 --- a/Analysis/TracedHeap.cs +++ b/Analysis/TracedHeap.cs @@ -222,9 +222,10 @@ public int PostorderTypeIndexOrSentinel(int postorderIndex) return m_objectAddressToRootIndices[m_postorderEntries[postorderIndex].Address]; } - public void DescribeRootIndices(int postorderIndex, StringBuilder sb) + public void DescribeRootIndices(int postorderIndex, StringBuilder sb, IStructuredOutput output) { List<(int rootIndex, PointerInfo pointerFlags)> rootInfos = PostorderRootIndices(postorderIndex); + output.BeginArray("roots"); sb.AppendFormat("roots#{0}{{", postorderIndex); for (int i = 0; i < rootInfos.Count; i++) { @@ -232,9 +233,12 @@ public void DescribeRootIndices(int postorderIndex, StringBuilder sb) { sb.Append(", "); } - sb.Append(m_rootSet.DescribeRoot(rootInfos[i].rootIndex, fullyQualified: true)); + output.BeginElement(); + sb.Append(m_rootSet.DescribeRoot(rootInfos[i].rootIndex, output, fullyQualified: true)); + output.EndElement(); } sb.Append('}'); + output.EndArray(); } public IEnumerable TagsForAddress(NativeWord address) diff --git a/AnalysisTests/BacktracerTest.cs b/AnalysisTests/BacktracerTest.cs index 871ea76..5acc784 100644 --- a/AnalysisTests/BacktracerTest.cs +++ b/AnalysisTests/BacktracerTest.cs @@ -87,6 +87,7 @@ internal BasicTraceableHeap() public void TestBasic() { Backtracer backtracer = MakeBacktracer(new BasicTraceableHeap(), gcHandleWeight: 0, fuseRoots: false); + IStructuredOutput output = new MockStructuredOutput(); Assert.That(backtracer.TracedHeap, Is.EqualTo(m_tracedHeap)); @@ -122,11 +123,11 @@ public void TestBasic() Assert.That(backtracer.Predecessors(gcHandle0), Is.EquivalentTo(new int[] { backtracer.RootNodeIndex })); Assert.That(backtracer.Predecessors(backtracer.RootNodeIndex), Is.EquivalentTo(Array.Empty())); - Assert.That(backtracer.DescribeNodeIndex(backtracer.RootNodeIndex, fullyQualified: true), Is.EqualTo("Process")); - Assert.That(backtracer.DescribeNodeIndex(backtracer.RootNodeIndex, fullyQualified: true), Is.EqualTo("Process")); - Assert.That(backtracer.DescribeNodeIndex(gcHandle0, fullyQualified: true), Is.EqualTo("GCHandle#0")); - Assert.That(backtracer.DescribeNodeIndex(nodeIndex1, fullyQualified: true), Is.EqualTo("Test.Assembly:System.Int64#0")); - Assert.That(backtracer.DescribeNodeIndex(nodeIndex1, fullyQualified: false), Is.EqualTo("Int64#0")); + Assert.That(backtracer.DescribeNodeIndex(backtracer.RootNodeIndex, output, fullyQualified: true), Is.EqualTo("Process")); + Assert.That(backtracer.DescribeNodeIndex(backtracer.RootNodeIndex, output, fullyQualified: true), Is.EqualTo("Process")); + Assert.That(backtracer.DescribeNodeIndex(gcHandle0, output, fullyQualified: true), Is.EqualTo("GCHandle#0")); + Assert.That(backtracer.DescribeNodeIndex(nodeIndex1, output, fullyQualified: true), Is.EqualTo("Test.Assembly:System.Int64#0")); + Assert.That(backtracer.DescribeNodeIndex(nodeIndex1, output, fullyQualified: false), Is.EqualTo("Int64#0")); Assert.That(backtracer.NodeType(backtracer.RootNodeIndex), Is.EqualTo("root")); Assert.That(backtracer.NodeType(backtracer.UnreachableNodeIndex), Is.EqualTo("unreachable")); diff --git a/AnalysisTests/MockStructuredOutput.cs b/AnalysisTests/MockStructuredOutput.cs new file mode 100644 index 0000000..ec14fa1 --- /dev/null +++ b/AnalysisTests/MockStructuredOutput.cs @@ -0,0 +1,70 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +using MemorySnapshotAnalyzer.AbstractMemorySnapshot; + +namespace MemorySnapshotAnalyzer.AnalysisTests +{ + internal sealed class MockStructuredOutput : IStructuredOutput + { + internal MockStructuredOutput() + { + } + + public void AddProperty(string key, string value) + { + } + + public void AddProperty(string key, long value) + { + } + + public void AddDisplayString(string message) + { + } + + public void AddDisplayString(string format, params object[] args) + { + } + + public void AddDisplayStringLine(string message) + { + } + + public void AddDisplayStringLine(string format, params object[] args) + { + } + + public void AddDisplayStringLineIndented(int indent, string format, params object[] args) + { + } + + public void BeginArray(string key) + { + } + + public void BeginElement() + { + } + + public void EndElement() + { + } + + public void EndArray() + { + } + + public void BeginChild(string key) + { + } + + public void EndChild() + { + } + } +} diff --git a/AnalysisTests/MockTraceableHeap.cs b/AnalysisTests/MockTraceableHeap.cs index e4026d5..4e679af 100644 --- a/AnalysisTests/MockTraceableHeap.cs +++ b/AnalysisTests/MockTraceableHeap.cs @@ -161,7 +161,7 @@ public override bool ContainsAddress(NativeWord address) throw new System.NotImplementedException(); } - public override string? DescribeAddress(NativeWord address) + public override string? DescribeAddress(NativeWord address, IStructuredOutput output) { throw new System.NotImplementedException(); } diff --git a/CommandInfrastructure/Command.cs b/CommandInfrastructure/Command.cs index abe1cc3..054eea9 100644 --- a/CommandInfrastructure/Command.cs +++ b/CommandInfrastructure/Command.cs @@ -16,20 +16,20 @@ public abstract class Command { readonly Repl m_repl; readonly Context m_context; - IOutput m_output; + IStructuredOutput m_output; protected Command(Repl repl) { m_repl = repl; m_context = m_repl.CurrentContext; - m_output = m_repl.Output; + m_output = m_repl.StructuredOutput; } public Repl Repl => m_repl; public Context Context => m_context; - public IOutput Output => m_output; + public IStructuredOutput Output => m_output; public sealed class Unredirector : IDisposable { @@ -42,13 +42,13 @@ public Unredirector(Command command) public void Dispose() { - m_command.m_output = m_command.m_repl.Output; + m_command.m_output = m_command.m_repl.StructuredOutput; } } - public Unredirector RedirectOutput(IOutput output) + public Unredirector RedirectOutput(IStructuredOutput structuredOutput) { - m_output = output; + m_output = structuredOutput; return new(this); } @@ -136,6 +136,7 @@ public HeapDom CurrentHeapDom public void DescribeAddress(NativeWord addressOfValue, StringBuilder sb) { + Output.AddProperty("address", addressOfValue.ToString()); if (addressOfValue.Value == 0) { @@ -150,11 +151,13 @@ public void DescribeAddress(NativeWord addressOfValue, StringBuilder sb) MemoryView memoryView = segmentedHeap.GetMemoryViewForAddress(addressOfValue); if (!memoryView.IsValid) { + Output.AddProperty("addressMapped", "false"); sb.AppendFormat("{0}: not in mapped memory", addressOfValue); return; } nativeValue = memoryView.ReadNativeWord(0, CurrentMemorySnapshot.Native); + Output.AddProperty("addressContents", nativeValue.ToString()); sb.AppendFormat("{0}: {1} ", addressOfValue, nativeValue); } @@ -172,6 +175,7 @@ public void DescribeAddress(NativeWord addressOfValue, StringBuilder sb) postorderIndex = CurrentTracedHeap.ObjectAddressToPostorderIndex(nativeValue); if (postorderIndex != -1) { + Output.AddProperty("pointerTo", "True"); sb.Append("pointer to "); DescribeObject(postorderIndex, nativeValue, sb); return; @@ -184,7 +188,7 @@ public void DescribeAddress(NativeWord addressOfValue, StringBuilder sb) return; } - string? typeDescription = CurrentTraceableHeap.DescribeAddress(addressOfValue); + string? typeDescription = CurrentTraceableHeap.DescribeAddress(addressOfValue, Output); if (typeDescription != null) { sb.AppendFormat("{0}", typeDescription); @@ -193,19 +197,11 @@ public void DescribeAddress(NativeWord addressOfValue, StringBuilder sb) if (segmentedHeap != null) { - HeapSegment? segment = segmentedHeap.GetSegmentForAddress(nativeValue); - if (segment == null) - { - sb.Append("not a pointer to mapped memory"); - } - else if (segment.IsRuntimeTypeInformation) - { - sb.AppendFormat("pointer into rtti[segment @ {0:X016}]", segment.StartAddress); - } - else - { - sb.AppendFormat("pointer into managed heap[segment @ {0:X016}]", segment.StartAddress); - } + + // If the address is not in mapped memory, then we already returned from this method, further above. + HeapSegment segment = segmentedHeap.GetSegmentForAddress(nativeValue)!; + sb.Append("pointer into "); + segment.Describe(Output, sb); } } @@ -218,6 +214,8 @@ protected void DescribePointerInfo(PointerInfo pointerInfo, StringBu { PointerFlags baseFlags = pointerInfo.PointerFlags.WithoutWeight(); int weight = pointerInfo.PointerFlags.Weight(); + Output.AddProperty("pointerFlags", baseFlags.ToString()); + Output.AddProperty("referenceWeight", weight); if (weight != 0) { sb.AppendFormat(" ({0}, weight {1})", baseFlags, weight); @@ -236,7 +234,7 @@ void DescribeObject(int postorderIndex, NativeWord objectAddress, StringBuilder int typeIndex = CurrentTraceableHeap.TryGetTypeIndex(objectAddress); if (typeIndex == -1) { - CurrentTracedHeap.DescribeRootIndices(postorderIndex, sb); + CurrentTracedHeap.DescribeRootIndices(postorderIndex, sb, Output); return; } @@ -246,23 +244,28 @@ void DescribeObject(int postorderIndex, NativeWord objectAddress, StringBuilder AppendWeight(CurrentBacktracer.Weight(nodeIndex), sb); } + Output.AddProperty("objectIndex", postorderIndex); sb.AppendFormat("live object[index {0}", postorderIndex); string? name = CurrentTraceableHeap.GetObjectName(objectAddress); if (name != null) { + Output.AddProperty("objectName", name); sb.AppendFormat(" \"{0}\"", name); } int objectSize = CurrentTraceableHeap.GetObjectSize(objectAddress, typeIndex, committedOnly: false); + Output.AddProperty("objectSize", objectSize); sb.AppendFormat(", size {0}", objectSize); int committedSize = CurrentTraceableHeap.GetObjectSize(objectAddress, typeIndex, committedOnly: true); if (committedSize != objectSize) { + Output.AddProperty("committedSize", committedSize); sb.AppendFormat(" (committed {0})", committedSize); } + CurrentTraceableHeap.TypeSystem.OutputType(Output, "objectType", typeIndex); sb.AppendFormat(", type {0}:{1} (type index {2})]", CurrentTraceableHeap.TypeSystem.Assembly(typeIndex), CurrentTraceableHeap.TypeSystem.QualifiedName(typeIndex), @@ -277,6 +280,8 @@ void DescribeObject(int postorderIndex, NativeWord objectAddress, StringBuilder if (objectView.IsValid) { (int stringLength, string s) = ReadString(objectView, maxLength: 80); + Output.AddProperty("stringLength", stringLength); + Output.AddProperty("stringValue", s); sb.AppendFormat(" String of length {0} = \"{1}\"", stringLength, s); } } @@ -285,8 +290,9 @@ void DescribeObject(int postorderIndex, NativeWord objectAddress, StringBuilder AppendTags(objectAddress, sb); } - protected static void AppendWeight(int weight, StringBuilder sb) + protected void AppendWeight(int weight, StringBuilder sb) { + Output.AddProperty("referenceWeight", weight); if (weight == 1) { sb.Append("** "); @@ -322,12 +328,23 @@ protected void AppendFields(int postorderIndex, NativeWord targetAddress, String } else { + Output.BeginArray("fields"); sb.Append(' '); first = false; } - sb.Append(CurrentTraceableHeap.TypeSystem.FieldName(pointerInfo.TypeIndex, pointerInfo.FieldNumber)); + + string fieldName = CurrentTraceableHeap.TypeSystem.FieldName(pointerInfo.TypeIndex, pointerInfo.FieldNumber); + Output.BeginElement(); + Output.AddProperty("fieldName", CurrentTraceableHeap.TypeSystem.FieldName(pointerInfo.TypeIndex, pointerInfo.FieldNumber)); + sb.Append(fieldName); + Output.EndElement(); } } + + if (!first) + { + Output.EndArray(); + } } protected void AppendTags(NativeWord address, StringBuilder sb) @@ -337,6 +354,8 @@ protected void AppendTags(NativeWord address, StringBuilder sb) { if (first) { + Output.BeginArray("tags"); + sb.Append(" tags("); first = false; } @@ -345,12 +364,17 @@ protected void AppendTags(NativeWord address, StringBuilder sb) sb.Append(','); } + Output.BeginElement(); + Output.AddProperty("tag", tag); sb.Append(tag); + Output.EndElement(); } if (!first) { sb.Append(')'); + + Output.EndArray(); } } @@ -362,21 +386,30 @@ protected void DumpObjectPointers(NativeWord address) throw new CommandException($"unable to determine object type"); } - Output.WriteLine("Object of type {0}:{1} (type index {2})", + CurrentTraceableHeap.TypeSystem.OutputType(Output, "objectType", typeIndex); + Output.AddDisplayStringLine("Object of type {0}:{1} (type index {2})", CurrentTraceableHeap.TypeSystem.Assembly(typeIndex), CurrentTraceableHeap.TypeSystem.QualifiedName(typeIndex), typeIndex); + Output.BeginArray("objectPointers"); var sb = new StringBuilder(); foreach (PointerInfo pointerInfo in CurrentTraceableHeap.GetPointers(address, typeIndex)) { + Output.BeginElement(); + DescribePointerInfo(pointerInfo, sb); if (sb.Length > 0) { - Output.WriteLineIndented(1, sb.ToString()); + string s = sb.ToString(); + Output.AddProperty("pointer", s); + Output.AddDisplayStringLineIndented(1, s); sb.Clear(); } + + Output.EndElement(); } + Output.EndArray(); } protected void DumpObjectMemory(NativeWord address, MemoryView objectView, int indent, string? fieldNameOpt = null) @@ -401,18 +434,25 @@ protected void DumpObjectMemory(MemoryView objectView, int typeIndex, int indent if (typeIndex == typeSystem.SystemStringTypeIndex) { (int stringLength, string s) = ReadString(objectView, maxLength: int.MaxValue); - Output.WriteLineIndented(indent, "String of length {0} = \"{1}\"", stringLength, s); + Output.AddProperty("kind", "string"); + Output.AddProperty("stringLength", stringLength); + Output.AddProperty("stringValue", s); + Output.AddDisplayStringLineIndented(indent, "String of length {0} = \"{1}\"", stringLength, s); } else if (typeSystem.IsArray(typeIndex)) { int elementTypeIndex = typeSystem.BaseOrElementTypeIndex(typeIndex); int arraySize = CurrentSegmentedHeapOpt!.ReadArraySize(objectView); - Output.WriteLineIndented(indent, "Array of length {0} with element type {1}:{2} (type index {3})", + Output.AddProperty("kind", "array"); + Output.AddProperty("arrayLength", arraySize); + CurrentTraceableHeap.TypeSystem.OutputType(Output, "elementType", elementTypeIndex); + Output.AddDisplayStringLineIndented(indent, "Array of length {0} with element type {1}:{2} (type index {3})", arraySize, typeSystem.Assembly(elementTypeIndex), typeSystem.QualifiedName(elementTypeIndex), elementTypeIndex); + Output.BeginArray("elements"); int elementSize = typeSystem.GetArrayElementSize(elementTypeIndex); for (int i = 0; i < arraySize; i++) { @@ -423,18 +463,25 @@ protected void DumpObjectMemory(MemoryView objectView, int typeIndex, int indent break; } + Output.BeginElement(); + Output.AddProperty("elementOffset", elementOffset); MemoryView elementView = objectView.GetRange(elementOffset, elementSize); - Output.WriteLineIndented(indent + 1, "Element {0} at offset {1}", i, elementOffset); + Output.AddDisplayStringLineIndented(indent + 1, "Element {0} at offset {1}", i, elementOffset); DumpFieldMemory(elementView, elementTypeIndex, indent + 2); + Output.EndElement(); } + Output.EndArray(); } else { - Output.WriteLineIndented(indent, "Object of type {0}:{1} (type index {2})", + Output.AddProperty("kind", "object"); + CurrentTraceableHeap.TypeSystem.OutputType(Output, "objectType", typeIndex); + Output.AddDisplayStringLineIndented(indent, "Object of type {0}:{1} (type index {2})", typeSystem.Assembly(typeIndex), typeSystem.QualifiedName(typeIndex), typeIndex); + Output.BeginArray("fields"); int numberOfFields = typeSystem.NumberOfFields(typeIndex); for (int fieldNumber = 0; fieldNumber < numberOfFields; fieldNumber++) { @@ -451,15 +498,24 @@ protected void DumpObjectMemory(MemoryView objectView, int typeIndex, int indent int fieldOffset = typeSystem.FieldOffset(typeIndex, fieldNumber, withHeader: true); int fieldTypeIndex = typeSystem.FieldType(typeIndex, fieldNumber); - Output.WriteLineIndented(indent + 1, "+{0} {1} : {2} {3}:{4} (type index {5})", + string kind = typeSystem.Kind(fieldTypeIndex); + + Output.BeginElement(); + Output.AddProperty("fieldOffset", fieldOffset); + Output.AddProperty("fieldName", fieldName); + Output.AddProperty("kind", kind); + CurrentTraceableHeap.TypeSystem.OutputType(Output, "fieldType", fieldTypeIndex); + Output.AddDisplayStringLineIndented(indent + 1, "+{0} {1} : {2} {3}:{4} (type index {5})", fieldOffset, fieldName, - typeSystem.IsValueType(fieldTypeIndex) ? "value" : typeSystem.IsArray(fieldTypeIndex) ? "array" : "object", + kind, typeSystem.Assembly(fieldTypeIndex), typeSystem.QualifiedName(fieldTypeIndex), fieldTypeIndex); DumpFieldMemory(objectView.GetRange(fieldOffset, objectView.Size - fieldOffset), fieldTypeIndex, indent + 2); + Output.EndElement(); } + Output.EndArray(); int baseTypeIndex = typeSystem.BaseOrElementTypeIndex(typeIndex); if (baseTypeIndex >= 0) @@ -502,7 +558,7 @@ protected void DumpFieldMemory(MemoryView objectView, int fieldTypeIndex, int in NativeWord reference = objectView.ReadPointer(0, CurrentMemorySnapshot.Native); var sb = new StringBuilder(); DescribeAddress(reference, sb); - Output.WriteLineIndented(indent, sb.ToString()); + Output.AddDisplayStringLineIndented(indent, sb.ToString()); } } @@ -512,6 +568,7 @@ protected void DumpValueTypeMemory(MemoryView objectView, int typeIndex, int ind int numberOfFields = typeSystem.NumberOfFields(typeIndex); int numberOfFieldsDumped = 0; + Output.BeginArray("fields"); for (int fieldNumber = 0; fieldNumber < numberOfFields; fieldNumber++) { if (typeSystem.FieldIsStatic(typeIndex, fieldNumber)) @@ -528,13 +585,21 @@ protected void DumpValueTypeMemory(MemoryView objectView, int typeIndex, int ind int fieldOffset = typeSystem.FieldOffset(typeIndex, fieldNumber, withHeader: false); int fieldTypeIndex = typeSystem.FieldType(typeIndex, fieldNumber); + Output.BeginElement(); + // Avoid infinite recursion due to the way that primitive types (such as System.Int32) are defined. if (fieldTypeIndex != typeIndex) { - Output.WriteLineIndented(indent, "+{0} {1} : {2} {3}:{4} (type index {5})", + string kind = typeSystem.Kind(fieldTypeIndex); + + Output.AddProperty("fieldOffset", fieldOffset); + Output.AddProperty("fieldName", fieldName); + Output.AddProperty("kind", kind); + CurrentTraceableHeap.TypeSystem.OutputType(Output, "fieldType", fieldTypeIndex); + Output.AddDisplayStringLineIndented(indent, "+{0} {1} : {2} {3}:{4} (type index {5})", fieldOffset, fieldName, - typeSystem.IsValueType(fieldTypeIndex) ? "value" : typeSystem.IsArray(fieldTypeIndex) ? "array" : "object", + kind, typeSystem.Assembly(fieldTypeIndex), typeSystem.QualifiedName(fieldTypeIndex), fieldTypeIndex); @@ -548,24 +613,30 @@ protected void DumpValueTypeMemory(MemoryView objectView, int typeIndex, int ind { if (valueOpt is char c) { - Output.WriteLineIndented(indent, "Value {0} 0x{0:X04} '{1}'", (int)c, char.IsControl(c) ? '.' : c); + Output.AddProperty("charValue", (int)c); + Output.AddDisplayStringLineIndented(indent, "Value {0} 0x{0:X04} '{1}'", (int)c, char.IsControl(c) ? '.' : c); } else if (valueOpt is byte b) { - Output.WriteLineIndented(indent, "Value {0} 0x{0:X02} '{1}'", b, char.IsControl((char)b) ? '.' : (char)b); + Output.AddProperty("byteValue", b); + Output.AddDisplayStringLineIndented(indent, "Value {0} 0x{0:X02} '{1}'", b, char.IsControl((char)b) ? '.' : (char)b); } else { - Output.WriteLineIndented(indent, "Value {0}", valueOpt); + Output.AddProperty("value", valueOpt.ToString()!); + Output.AddDisplayStringLineIndented(indent, "Value {0}", valueOpt); } numberOfFieldsDumped++; } } + + Output.EndElement(); } + Output.EndArray(); if (numberOfFieldsDumped == 0) { - Output.WriteLineIndented(indent, "No fields that could be dumped"); + Output.AddDisplayStringLineIndented(indent, "No fields that could be dumped"); } } diff --git a/CommandInfrastructure/CommandLineArgument.cs b/CommandInfrastructure/CommandLineArgument.cs index d7454cf..dab3262 100644 --- a/CommandInfrastructure/CommandLineArgument.cs +++ b/CommandInfrastructure/CommandLineArgument.cs @@ -1,4 +1,4 @@ -/* +/* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the @@ -282,5 +282,24 @@ public TypeSet ResolveTypeIndexOrPattern(Context context, bool includeDerived) return typeSet; } + + public void Describe(IStructuredOutput output) + { + switch (m_type) + { + case CommandLineArgumentType.Atom: + output.AddProperty("kind", "atom"); + output.AddProperty("value", m_atomOrStringValue!); + break; + case CommandLineArgumentType.String: + output.AddProperty("kind", "string"); + output.AddProperty("value", m_atomOrStringValue!); + break; + case CommandLineArgumentType.Integer: + output.AddProperty("kind", "integer"); + output.AddProperty("value", (long)m_integerValue); + break; + } + } } } diff --git a/CommandInfrastructure/ConsoleOutput.cs b/CommandInfrastructure/ConsoleOutput.cs index 22c0cac..d25aaa3 100644 --- a/CommandInfrastructure/ConsoleOutput.cs +++ b/CommandInfrastructure/ConsoleOutput.cs @@ -1,4 +1,4 @@ -/* +/* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the @@ -123,13 +123,13 @@ void IOutput.WriteLine(string format, params object[] args) void IOutput.WriteLineIndented(int indent, string format, params object[] args) { - string? indentString; - if (!m_indents.TryGetValue(indent, out indentString)) + if (!m_indents.TryGetValue(indent, out string? indentString)) { indentString = new string(' ', indent * 2); m_indents[indent] = indentString; } Console.Write(indentString); + if (args.Length > 0) { Console.WriteLine(format, args); diff --git a/CommandInfrastructure/Context.cs b/CommandInfrastructure/Context.cs index 7f9ea18..6f5bb78 100644 --- a/CommandInfrastructure/Context.cs +++ b/CommandInfrastructure/Context.cs @@ -76,11 +76,13 @@ public static Context WithSameOptionsAs(Context other, ILogger logger, int newId public void SummarizeNewWarnings() { + // TODO: also write to IStructuredOutput m_logger.SummarizeNew(s => m_output.WriteLine(s)); } public void FlushWarnings() { + // TODO: also write to IStructuredOutput m_logger.Flush(s => m_output.WriteLine(s)); } @@ -190,6 +192,66 @@ public IEnumerable Serialize() } } + public void DumpToStructuredOutput(IStructuredOutput output) + { + output.BeginChild("heapSnapshot"); + if (m_currentMemorySnapshot != null) + { + output.AddProperty("filename", m_currentMemorySnapshot.Filename); + output.AddProperty("format", m_currentMemorySnapshot.Format); + } + output.EndChild(); + + output.BeginChild("traceableHeap"); + output.AddProperty("kind", m_traceableHeap_kind.ToString()); + output.AddProperty("fuseObjectPairs", m_traceableHeap_fuseObjectPairs.ToString()); + DumpReferenceClassifiersToStructuredOutput(output); + if (m_currentTraceableHeap != null) + { + output.AddProperty("description", m_currentTraceableHeap.Description); + output.AddProperty("numberOfTypeIndices", m_currentTraceableHeap.TypeSystem.NumberOfTypeIndices); + output.AddProperty("numberOfObjectPairs", m_currentTraceableHeap.NumberOfObjectPairs); + output.AddProperty("withMemory", (m_currentTraceableHeap.SegmentedHeapOpt != null).ToString()); + } + output.EndChild(); + + output.BeginChild("rootSet"); + output.AddProperty("singleRootAddress", m_rootSet_singletonRootAddress.ToString()); + output.AddProperty("weakGChandles", m_rootSet_weakGCHandles.ToString()); + if (m_currentRootSet != null) + { + output.AddProperty("numberOfRoots", m_currentRootSet.NumberOfRoots); + output.AddProperty("numberOfGCHandles", m_currentRootSet.NumberOfGCHandles); + output.AddProperty("numberOfStaticRoots", m_currentRootSet.NumberOfStaticRoots); + } + output.EndChild(); + + output.BeginChild("tracedHeap"); + if (m_currentTracedHeap != null) + { + output.AddProperty("numberOfDistinctRoots", m_currentTracedHeap.NumberOfDistinctRoots); + output.AddProperty("numberOfInvalidRoots", m_currentTracedHeap.NumberOfInvalidRoots); + output.AddProperty("numberOfInvalidPointers", m_currentTracedHeap.NumberOfInvalidPointers); + output.AddProperty("numberOfLiveBytes", m_currentTracedHeap.NumberOfLiveBytes); + output.AddProperty("numberOfLiveObjects", m_currentTracedHeap.NumberOfLiveObjects); + } + output.EndChild(); + + output.BeginChild("backtracer"); + output.AddProperty("groupStatics", m_backtracer_groupStatics.ToString()); + Backtracer_ReferencesToIgnore_DumpToStructuredOutput(output); + output.AddProperty("fuseRoots", m_backtracer_fuseRoots.ToString()); + output.AddProperty("computed", (m_currentBacktracer != null).ToString()); + output.EndChild(); + + output.BeginChild("heapDom"); + if (m_currentHeapDom != null) + { + output.AddProperty("numberOfNonLeafNodes", m_currentHeapDom.NumberOfNonLeafNodes); + } + output.EndChild(); + } + public void ClearContext() { if (m_currentMemorySnapshot != null) @@ -291,13 +353,30 @@ string GetReferenceClassifierDescription() if (m_referenceClassifierStore.TryGetGroup(groupName, out ReferenceClassifierGroup? group)) { - sb.AppendFormat("({0})", group!.NumberOfRules); + sb.AppendFormat("({0})", group.NumberOfRules); } } return sb.Length == 0 ? "none" : sb.ToString(); } + void DumpReferenceClassifiersToStructuredOutput(IStructuredOutput output) + { + output.BeginArray("referenceClassifiers"); + foreach (string groupName in m_traceableHeap_referenceClassificationGroups) + { + output.BeginElement(); + output.AddProperty("groupName", groupName); + + if (m_referenceClassifierStore.TryGetGroup(groupName, out ReferenceClassifierGroup? group)) + { + output.AddProperty("numberOfRules", group.NumberOfRules); + } + output.EndElement(); + } + output.EndArray(); + } + public TraceableHeap? CurrentTraceableHeap => m_currentTraceableHeap; public void EnsureTraceableHeap() @@ -528,6 +607,24 @@ string Backtracer_ReferencesToIgnore_Stringify() return sb.ToString(); } + void Backtracer_ReferencesToIgnore_DumpToStructuredOutput(IStructuredOutput output) + { + output.BeginArray("referencesToIgnore"); + + if (m_backtracer_referencesToIgnore != null) + { + foreach ((int childPostorderIndex, int parentPostorderIndex) in m_backtracer_referencesToIgnore) + { + output.BeginElement(); + output.AddProperty("childPostorderIndex", childPostorderIndex); + output.AddProperty("parentPostorderIndex", parentPostorderIndex); + output.EndElement(); + } + } + + output.EndArray(); + } + public bool Backtracer_FuseRoots { get { return m_backtracer_fuseRoots; } diff --git a/CommandInfrastructure/FileOutput.cs b/CommandInfrastructure/FileOutput.cs index 738106a..e1481af 100644 --- a/CommandInfrastructure/FileOutput.cs +++ b/CommandInfrastructure/FileOutput.cs @@ -94,13 +94,13 @@ public void WriteLine(string format, params object[] args) public void WriteLineIndented(int indent, string format, params object[] args) { - string? indentString; - if (!m_indents.TryGetValue(indent, out indentString)) + if (!m_indents.TryGetValue(indent, out string? indentString)) { indentString = new string(' ', indent * 2); m_indents[indent] = indentString; } m_writer.Write(indentString); + if (args.Length > 0) { m_writer.WriteLine(format, args); diff --git a/CommandInfrastructure/JsonStructuredOutput.cs b/CommandInfrastructure/JsonStructuredOutput.cs new file mode 100644 index 0000000..26d79f9 --- /dev/null +++ b/CommandInfrastructure/JsonStructuredOutput.cs @@ -0,0 +1,170 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Text.Json; +using System.Text.Json.Nodes; +using MemorySnapshotAnalyzer.AbstractMemorySnapshot; + +namespace MemorySnapshotAnalyzer.CommandInfrastructure +{ + public sealed class JsonStructuredOutput : IStructuredOutput + { + readonly bool m_keepDisplayStrings; + readonly IStructuredOutput? m_chainedStructuredOutput; + readonly JsonObject m_root; + readonly Dictionary m_indents = new(); + JsonNode m_cursor; + Dictionary m_displayStrings; + + public JsonStructuredOutput(IStructuredOutput? chainedStructuredOutput, bool keepDisplayStrings) + { + m_keepDisplayStrings = keepDisplayStrings; + m_chainedStructuredOutput = chainedStructuredOutput; + + m_root = new JsonObject(); + m_cursor = m_root; + m_displayStrings = new(); + } + + StringBuilder? CurrentDisplayString + { + get + { + if (!m_keepDisplayStrings) + { + return null; + } + + JsonObject cursor = m_cursor.AsObject(); + if (!m_displayStrings.TryGetValue(cursor, out StringBuilder? sb)) + { + sb = new StringBuilder(); + m_displayStrings.Add(cursor, sb); + } + + return sb; + } + } + + public void AddProperty(string key, string value) + { + m_cursor.AsObject().Add(key, value); + m_chainedStructuredOutput?.AddProperty(key, value); + } + + public void AddProperty(string key, long value) + { + m_cursor.AsObject().Add(key, value); + m_chainedStructuredOutput?.AddProperty(key, value); + } + + public void AddDisplayString(string message) + { + CurrentDisplayString?.Append(message); + m_chainedStructuredOutput?.AddDisplayString(message); + } + + public void AddDisplayString(string format, params object[] args) + { + CurrentDisplayString?.AppendFormat(format, args); + m_chainedStructuredOutput?.AddDisplayString(format, args); + } + + public void AddDisplayStringLine(string message) + { + CurrentDisplayString?.AppendLine(message); + m_chainedStructuredOutput?.AddDisplayStringLine(message); + } + + public void AddDisplayStringLine(string format, params object[] args) + { + CurrentDisplayString?.AppendFormat(format, args); + CurrentDisplayString?.AppendLine(); + m_chainedStructuredOutput?.AddDisplayStringLine(format, args); + } + + public void AddDisplayStringLineIndented(int indent, string format, params object[] args) + { + if (!m_indents.TryGetValue(indent, out string? indentString)) + { + indentString = new string(' ', indent * 2); + m_indents[indent] = indentString; + } + CurrentDisplayString?.Append(indentString); + + if (args.Length > 0) + { + CurrentDisplayString?.AppendFormat(format, args); + CurrentDisplayString?.AppendLine(); + } + else + { + // Do not interpret format string. + CurrentDisplayString?.AppendLine(format); + } + } + + public void BeginArray(string key) + { + JsonArray array = new JsonArray(); + m_cursor.AsObject().Add(key, array); + m_cursor = array; + m_chainedStructuredOutput?.BeginArray(key); + } + + public void BeginElement() + { + JsonObject element = new JsonObject(); + m_cursor.AsArray().Add(element); + m_cursor = element; + m_chainedStructuredOutput?.BeginElement(); + } + + public void EndElement() + { + m_cursor = m_cursor.Parent!; + m_chainedStructuredOutput?.EndElement(); + } + + public void EndArray() + { + m_cursor = m_cursor.Parent!; + m_chainedStructuredOutput?.EndArray(); + } + + public void BeginChild(string key) + { + JsonObject child = new JsonObject(); + m_cursor.AsObject().Add(key, child); + m_cursor = child; + m_chainedStructuredOutput?.BeginChild(key); + } + + public void EndChild() + { + m_cursor = m_cursor.Parent!; + m_chainedStructuredOutput?.EndChild(); + } + + public void WriteTo(Stream stream) + { + foreach ((JsonObject obj, StringBuilder sb) in m_displayStrings) + { + obj.Add("displayString", sb.ToString()); + } + + JsonWriterOptions options = new() { Indented = true }; + using (Utf8JsonWriter writer = new(stream, options)) + { + m_root.WriteTo(writer); + } + } + } +} diff --git a/CommandInfrastructure/PassthroughStructuredOutput.cs b/CommandInfrastructure/PassthroughStructuredOutput.cs new file mode 100644 index 0000000..9b24a2d --- /dev/null +++ b/CommandInfrastructure/PassthroughStructuredOutput.cs @@ -0,0 +1,78 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +using MemorySnapshotAnalyzer.AbstractMemorySnapshot; + +namespace MemorySnapshotAnalyzer.CommandInfrastructure +{ + public sealed class PassthroughStructuredOutput : IStructuredOutput + { + readonly IOutput m_output; + + public PassthroughStructuredOutput(IOutput output) + { + m_output = output; + } + + public void AddProperty(string key, string value) + { + } + + public void AddProperty(string key, long value) + { + } + + public void AddDisplayString(string message) + { + m_output.Write(message); + } + + public void AddDisplayString(string format, params object[] args) + { + m_output.Write(format, args); + } + + public void AddDisplayStringLine(string message) + { + m_output.WriteLine(message); + } + + public void AddDisplayStringLine(string format, params object[] args) + { + m_output.WriteLine(format, args); + } + + public void AddDisplayStringLineIndented(int indent, string format, params object[] args) + { + m_output.WriteLineIndented(indent, format, args); + } + + public void BeginArray(string key) + { + } + + public void BeginElement() + { + } + + public void EndElement() + { + } + + public void EndArray() + { + } + + public void BeginChild(string key) + { + } + + public void EndChild() + { + } + } +} diff --git a/CommandInfrastructure/Repl.cs b/CommandInfrastructure/Repl.cs index ffb3f76..b398809 100644 --- a/CommandInfrastructure/Repl.cs +++ b/CommandInfrastructure/Repl.cs @@ -26,6 +26,7 @@ enum NamedArgumentKind readonly IConfiguration m_configuration; readonly IOutput m_output; + readonly IStructuredOutput m_structuredOutput; readonly ILoggerFactory m_loggerFactory; readonly bool m_isInteractive; readonly List m_memorySnapshotLoaders; @@ -36,10 +37,11 @@ enum NamedArgumentKind string? m_currentCommandLine; int m_currentContextId; - public Repl(IConfiguration configuration, IOutput output, ILoggerFactory loggerFactory, bool isInteractive) + public Repl(IConfiguration configuration, IOutput output, IStructuredOutput structuredOutput, ILoggerFactory loggerFactory, bool isInteractive) { m_configuration = configuration; m_output = output; + m_structuredOutput = structuredOutput; m_loggerFactory = loggerFactory; m_isInteractive = isInteractive; m_memorySnapshotLoaders = new(); @@ -125,6 +127,8 @@ public void AddCommand(Type commandType, params string[] names) public IOutput Output => m_output; + public IStructuredOutput StructuredOutput => m_structuredOutput; + public bool IsInteractive => m_isInteractive; public string CurrentCommandLine => m_currentCommandLine!; @@ -277,25 +281,51 @@ public void RunCommand(string line) throw new CommandException($"unknown command '{commandLine.CommandName}'"); } - var commandConstructorSignature = new Type[] { typeof(Repl) }; - var commandConstructorArguments = new object[] { this }; - - Command command = (Command)commandType.GetConstructor(commandConstructorSignature)!.Invoke(commandConstructorArguments); - AssignArgumentValues(command, commandLine); - try { - m_currentCommandLine = line; - command.Run(); + m_structuredOutput.BeginElement(); + m_structuredOutput.AddProperty("commandName", commandType.Name); + m_structuredOutput.AddProperty("commandLine", line); + + var commandConstructorSignature = new Type[] { typeof(Repl) }; + var commandConstructorArguments = new object[] { this }; + + Command command = (Command)commandType.GetConstructor(commandConstructorSignature)!.Invoke(commandConstructorArguments); + try + { + m_structuredOutput.BeginChild("commandLineArguments"); + AssignArgumentValues(command, commandLine); + } + finally + { + m_structuredOutput.EndChild(); + } + + try + { + m_currentCommandLine = line; + m_structuredOutput.BeginChild("commandOutput"); + command.Run(); + } + finally + { + m_currentCommandLine = null; + m_structuredOutput.EndChild(); + + foreach (var kvp in m_contexts) + { + kvp.Value.SummarizeNewWarnings(); + } + } } finally { - m_currentCommandLine = null; + m_structuredOutput.BeginChild("context"); + m_structuredOutput.AddProperty("currentContextId", m_currentContextId); + CurrentContext.DumpToStructuredOutput(m_structuredOutput); + m_structuredOutput.EndChild(); - foreach (var kvp in m_contexts) - { - kvp.Value.SummarizeNewWarnings(); - } + m_structuredOutput.EndElement(); } } @@ -479,6 +509,9 @@ void AssignArgumentValues(Command command, CommandLine commandLine) if (index < positionalArguments.Count) { SetFieldValue(commandLine.CommandName, command, field, positionalArguments[index]); + m_structuredOutput.BeginChild(field.Name); + positionalArguments[index].Describe(m_structuredOutput); + m_structuredOutput.EndChild(); } else if (!optional) { @@ -493,6 +526,9 @@ void AssignArgumentValues(Command command, CommandLine commandLine) if (namedArguments.TryGetValue(namedArgumentName, out var value)) { SetFieldValue(commandLine.CommandName, command, field, value); + m_structuredOutput.BeginChild(field.Name); + value.Describe(m_structuredOutput); + m_structuredOutput.EndChild(); } } else if (attribute.AttributeType == typeof(FlagArgumentAttribute)) @@ -508,6 +544,9 @@ void AssignArgumentValues(Command command, CommandLine commandLine) { field.SetValue(command, value ? 1 : 0); } + m_structuredOutput.BeginChild(field.Name); + m_structuredOutput.AddProperty("value", value.ToString()); + m_structuredOutput.EndChild(); } } } diff --git a/Commands/BacktraceCommand.cs b/Commands/BacktraceCommand.cs index 9810638..a03f925 100644 --- a/Commands/BacktraceCommand.cs +++ b/Commands/BacktraceCommand.cs @@ -81,12 +81,15 @@ public override void Run() else if (OutputDotFilename != null) { using (var fileOutput = new FileOutput(OutputDotFilename, useUnixNewlines: false)) - using (RedirectOutput(fileOutput)) + using (RedirectOutput(new PassthroughStructuredOutput(fileOutput))) { DumpBacktracesToDot(nodeIndex); } - Output.WriteLine("{0} nodes and {1} edges output", + Output.AddProperty("dotFilename", OutputDotFilename); + Output.AddProperty("numberOfNodesOutput", m_numberOfNodesOutput); + Output.AddProperty("numberOfEdgesOutput", m_numberOfEdgesOutput); + Output.AddDisplayStringLine("{0} nodes and {1} edges output", m_numberOfNodesOutput, m_numberOfEdgesOutput); } @@ -111,11 +114,25 @@ void DumpBacktraces(int nodeIndex, HashSet ancestors, HashSet seen, in } ancestors.Add(nodeIndex); + bool first = true; foreach (int predIndex in CurrentBacktracer.Predecessors(nodeIndex)) { + if (first) + { + Output.BeginArray("predecessors"); + first = false; + } + + Output.BeginElement(); DumpBacktraces(predIndex, ancestors, seen, depth + 1, successorNodeIndex: nodeIndex); + Output.EndElement(); } ancestors.Remove(nodeIndex); + + if (!first) + { + Output.EndArray(); + } } bool DumpBacktraceLine(int nodeIndex, HashSet ancestors, HashSet seen, int indent, int successorNodeIndex, string prefix) @@ -128,10 +145,12 @@ bool DumpBacktraceLine(int nodeIndex, HashSet ancestors, HashSet seen, // Back reference to a node that was already printed. if (ancestors.Contains(nodeIndex)) { + Output.AddProperty("seen", "upstack"); sb.Append("^^ "); } else { + Output.AddProperty("seen", "upAndOver"); sb.Append("~~ "); } } @@ -142,7 +161,7 @@ bool DumpBacktraceLine(int nodeIndex, HashSet ancestors, HashSet seen, AppendWeight(CurrentBacktracer.Weight(nodeIndex), sb); } - sb.Append(CurrentBacktracer.DescribeNodeIndex(nodeIndex, FullyQualified)); + sb.Append(CurrentBacktracer.DescribeNodeIndex(nodeIndex, Output, FullyQualified)); if (CurrentBacktracer.IsLiveObjectNode(nodeIndex)) { @@ -155,7 +174,7 @@ bool DumpBacktraceLine(int nodeIndex, HashSet ancestors, HashSet seen, AppendTags(address, sb); } - Output.WriteLineIndented(indent, sb.ToString()); + Output.AddDisplayStringLineIndented(indent, sb.ToString()); return result; } @@ -184,12 +203,16 @@ void DumpLifelines(int nodeIndex) int[] reachableRoots = lifelines.Keys.ToArray(); Array.Sort(reachableRoots, (a, b) => lifelines[a].Length.CompareTo(lifelines[b].Length)); + Output.BeginArray("reachableRoots"); foreach (int rootNodeIndex in reachableRoots) { - Output.WriteLine("{0}: {1} hop(s)", - CurrentBacktracer.DescribeNodeIndex(rootNodeIndex, FullyQualified), + Output.BeginElement(); + Output.AddDisplayStringLine("{0}: {1} hop(s)", + CurrentBacktracer.DescribeNodeIndex(rootNodeIndex, Output, FullyQualified), lifelines[rootNodeIndex].Length); + Output.EndElement(); } + Output.EndArray(); } else { @@ -233,12 +256,16 @@ void DumpTrie(int nodeIndex, TrieNode trie, HashSet ancestors, HashSet if (trie.Children != null) { + Output.BeginArray("children"); foreach ((int predNodeIndex, TrieNode child) in trie.Children) { bool newCondense = singleChild && trie.Children.Count == 1; int newIndent = newCondense ? indent : indent + 1; + Output.BeginElement(); DumpTrie(predNodeIndex, child, ancestors, seen, newIndent, nodeIndex, condensed: newCondense, singleChild: trie.Children.Count == 1); + Output.EndElement(); } + Output.EndArray(); } } @@ -288,9 +315,9 @@ void ComputeLifelines(int nodeIndex, List currentPath, HashSet seen, D void DumpBacktracesToDot(int nodeIndex) { - Output.WriteLine("digraph BT {"); + Output.AddDisplayStringLine("digraph BT {"); DumpBacktracesToDot(nodeIndex, -1, new HashSet(), 0); - Output.WriteLine("}"); + Output.AddDisplayStringLine("}"); } void DumpBacktracesToDot(int nodeIndex, int optSuccessorNodeIndex, HashSet seen, int depth) @@ -298,7 +325,7 @@ void DumpBacktracesToDot(int nodeIndex, int optSuccessorNodeIndex, HashSet if (optSuccessorNodeIndex != -1) { // declare the edge - Output.WriteLine(" n{0} -> n{1}", + Output.AddDisplayStringLine(" n{0} -> n{1}", nodeIndex, optSuccessorNodeIndex); m_numberOfEdgesOutput++; @@ -307,9 +334,9 @@ void DumpBacktracesToDot(int nodeIndex, int optSuccessorNodeIndex, HashSet if (!seen.Contains(nodeIndex)) { // declare the node to give it a recognizable label - Output.WriteLine(" n{0} [label=\"{1}\"]", + Output.AddDisplayStringLine(" n{0} [label=\"{1}\"]", nodeIndex, - CurrentBacktracer.DescribeNodeIndex(nodeIndex, FullyQualified)); + CurrentBacktracer.DescribeNodeIndex(nodeIndex, Output, FullyQualified)); m_numberOfNodesOutput++; seen.Add(nodeIndex); @@ -326,19 +353,31 @@ void DumpBacktracesToDot(int nodeIndex, int optSuccessorNodeIndex, HashSet void DumpDominators(int nodeIndex) { + Output.BeginArray("dominators"); + int currentNodeIndex = nodeIndex; int i = 0; do { - Output.WriteLineIndented(i, "{0} - exclusive size {1}, inclusive size {2}", - CurrentHeapDom.Backtracer.DescribeNodeIndex(currentNodeIndex, FullyQualified), - CurrentHeapDom.NodeSize(currentNodeIndex), - CurrentHeapDom.TreeSize(currentNodeIndex)); + Output.BeginElement(); + + long nodeSize = CurrentHeapDom.NodeSize(currentNodeIndex); + long treeSize = CurrentHeapDom.TreeSize(currentNodeIndex); + Output.AddProperty("nodeSize", nodeSize); + Output.AddProperty("treeSize", treeSize); + Output.AddDisplayStringLineIndented(i, "{0} - exclusive size {1}, inclusive size {2}", + CurrentHeapDom.Backtracer.DescribeNodeIndex(currentNodeIndex, Output, FullyQualified), + nodeSize, + treeSize); currentNodeIndex = CurrentHeapDom.GetDominator(currentNodeIndex); i = 1; + + Output.EndElement(); } // Note that with a SingletonRootSet, we may be asked to dump the dominator tree for an unreachable node. while (currentNodeIndex != -1 && currentNodeIndex != CurrentHeapDom.RootNodeIndex); + + Output.EndArray(); } public override string HelpText => "backtrace [[] ['depth ] | 'lifelines ['toroots] | 'owners | 'dom] ['fullyqualified] ['fields]"; diff --git a/Commands/ClearConsoleCommand.cs b/Commands/ClearConsoleCommand.cs index 9bc7b16..25b9d5f 100644 --- a/Commands/ClearConsoleCommand.cs +++ b/Commands/ClearConsoleCommand.cs @@ -1,4 +1,4 @@ -/* +/* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the @@ -15,7 +15,7 @@ public ClearConsoleCommand(Repl repl) : base(repl) { } public override void Run() { - Output.Clear(); + Repl.Output.Clear(); } public override string HelpText => "cls"; diff --git a/Commands/DescribeCommand.cs b/Commands/DescribeCommand.cs index e58b753..961feef 100644 --- a/Commands/DescribeCommand.cs +++ b/Commands/DescribeCommand.cs @@ -1,4 +1,4 @@ -/* +/* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the @@ -24,7 +24,7 @@ public override void Run() { var sb = new StringBuilder(); DescribeAddress(Address, sb); - Output.WriteLine(sb.ToString()); + Output.AddDisplayStringLine(sb.ToString()); } public override string HelpText => "describe
"; diff --git a/Commands/DumpAssembliesCommand.cs b/Commands/DumpAssembliesCommand.cs index 4d79438..deddab8 100644 --- a/Commands/DumpAssembliesCommand.cs +++ b/Commands/DumpAssembliesCommand.cs @@ -1,4 +1,4 @@ -/* +/* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the @@ -44,13 +44,19 @@ public override void Run() } } + Output.BeginArray("assemblies"); foreach (var kvp in assemblies) { - Output.WriteLine("assembly {0}: {1} type{2}", + Output.BeginElement(); + Output.AddProperty("assemblyName", kvp.Key); + Output.AddProperty("typeCount", kvp.Value); + Output.AddDisplayStringLine("assembly {0}: {1} type{2}", kvp.Key, kvp.Value, kvp.Value != 1 ? "s" : ""); + Output.EndElement(); } + Output.EndArray(); } public override string HelpText => "dumpassemblies []"; diff --git a/Commands/DumpCommand.cs b/Commands/DumpCommand.cs index 0d8886c..b5343c7 100644 --- a/Commands/DumpCommand.cs +++ b/Commands/DumpCommand.cs @@ -1,4 +1,4 @@ -/* +/* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the @@ -36,45 +36,29 @@ public override void Run() throw new CommandException($"address {Address} not in mapped memory"); } HexDumpAsAddresses(memoryView, Address); - Output.WriteLine(); - } - - protected void HexDump(MemoryView memoryView, NativeWord baseAddress, int width) - { - long currentLineOffset = 0; - while (currentLineOffset < memoryView.Size) - { - var sb = new StringBuilder(); - sb.AppendFormat("{0}:", baseAddress + currentLineOffset); - for (int i = 0; i < width; i++) - { - if (currentLineOffset + (i + 1) * CurrentMemorySnapshot.Native.Size > memoryView.Size) - { - return; - } - - NativeWord value = memoryView.ReadNativeWord(currentLineOffset + i * CurrentMemorySnapshot.Native.Size, CurrentMemorySnapshot.Native); - sb.AppendFormat(" {0}", value); - } - - Output.WriteLine(sb.ToString()); - - currentLineOffset += width * CurrentMemorySnapshot.Native.Size; - } + Output.AddDisplayStringLine(string.Empty); } protected void HexDumpAsAddresses(MemoryView memoryView, NativeWord baseAddress) { + Output.BeginArray("memoryContentsAsAddresses"); + var sb = new StringBuilder(); long currentLineOffset = 0; while (currentLineOffset + CurrentMemorySnapshot.Native.Size <= memoryView.Size) { + Output.BeginElement(); + DescribeAddress(baseAddress + currentLineOffset, sb); - Output.WriteLine(sb.ToString()); + Output.AddDisplayStringLine(sb.ToString()); sb.Clear(); currentLineOffset += CurrentMemorySnapshot.Native.Size; + + Output.EndElement(); } + + Output.EndArray(); } public override string HelpText => "dump
"; diff --git a/Commands/DumpInvalidReferencesCommand.cs b/Commands/DumpInvalidReferencesCommand.cs index 12ea85c..99218dd 100644 --- a/Commands/DumpInvalidReferencesCommand.cs +++ b/Commands/DumpInvalidReferencesCommand.cs @@ -47,20 +47,28 @@ public override void Run() objects.Add(objectAddress.Value); } - Output.WriteLine("Found {0} invalid targets referenced from {1} roots and {2} separate objects", + Output.AddProperty("numberOfInvalidTargets", references.Count); + Output.AddProperty("numberOfReferencingRoots", numberOfRoots); + Output.AddProperty("numberOfReferencingObjects", objects.Count); + Output.AddDisplayStringLine("Found {0} invalid targets referenced from {1} roots and {2} separate objects", references.Count, numberOfRoots, objects.Count); if (Roots) { + Output.BeginArray("rootsWithInvalidReferences"); foreach ((int rootIndex, NativeWord reference) in roots) { - Output.WriteLine("Root with index {0} ({1}) contains invalid reference {2}", + Output.BeginElement(); + Output.AddProperty("targetAddress", reference.ToString()); + Output.AddDisplayStringLine("Root with index {0} ({1}) contains invalid reference {2}", rootIndex, - CurrentRootSet.DescribeRoot(rootIndex, fullyQualified: true), + CurrentRootSet.DescribeRoot(rootIndex, Output, fullyQualified: true), reference); + Output.EndElement(); } + Output.EndArray(); } if (Objects || !Roots) @@ -80,15 +88,25 @@ public override void Run() byType.Add((reference, objectAddress)); } + Output.BeginArray("objectsWithInvalidReferences"); + StringBuilder sb = new(); foreach ((int typeIndex, List<(NativeWord reference, NativeWord objectAddress)> byType) in invalidReferencesByType) { + Output.BeginElement(); + Output.BeginArray("byType"); for (int i = 0; i < byType.Count; i++) { + Output.BeginElement(); + (NativeWord reference, NativeWord objectAddress) = byType[i]; int postorderIndex = CurrentTracedHeap.ObjectAddressToPostorderIndex(objectAddress); AppendFields(postorderIndex, reference, sb); - Output.WriteLine("Object {0} (index {1}) of type {2}:{3} (type index {4}) contains invalid reference {5}{6}", + CurrentTraceableHeap.TypeSystem.OutputType(Output, "objectType", typeIndex); + Output.AddProperty("objectAddress", objectAddress.ToString()); + Output.AddProperty("objectIndex", postorderIndex); + Output.AddProperty("targetAddress", reference.ToString()); + Output.AddDisplayStringLine("Object {0} (index {1}) of type {2}:{3} (type index {4}) contains invalid reference {5}{6}", objectAddress, postorderIndex, CurrentTraceableHeap.TypeSystem.Assembly(typeIndex), @@ -98,13 +116,22 @@ public override void Run() sb.ToString()); sb.Clear(); + Output.EndElement(); + if (!Verbose && byType.Count > 2) { - Output.WriteLine("... and {0} more with this type", byType.Count - 1); + Output.BeginElement(); + Output.AddProperty("elidedNumberOfOfjects", byType.Count - 1); + Output.AddDisplayStringLine("... and {0} more with this type", byType.Count - 1); + Output.EndElement(); break; } } + Output.EndArray(); + Output.EndElement(); } + + Output.EndArray(); } } diff --git a/Commands/DumpObjectCommand.cs b/Commands/DumpObjectCommand.cs index 57fcbd3..eb6cf3a 100644 --- a/Commands/DumpObjectCommand.cs +++ b/Commands/DumpObjectCommand.cs @@ -80,14 +80,14 @@ void DumpObjectPointers() StringBuilder sb = new(); DescribeAddress(address, sb); - Output.WriteLine(sb.ToString()); + Output.AddDisplayStringLine(sb.ToString()); DumpObjectPointers(address); } void DumpObjectMemoryAsType(SegmentedHeap segmentedHeap) { - NativeWord address = default; + NativeWord address; // Try to see what we're asked to operate on - an address or a postorder index. // Note that we don't use Context.ResolveToPostorderIndex here, as we want to support @@ -114,9 +114,14 @@ void DumpObjectMemoryAsType(SegmentedHeap segmentedHeap) throw new CommandException($"type pattern does not match exactly one type"); } + Output.BeginArray("asType"); foreach (int typeIndex in typeSet.TypeIndices) { - Output.WriteLine("dumping address {0} as type {1}:{2} (type index {3})", + Output.BeginElement(); + + Output.AddProperty("address", address.ToString()); + CurrentTraceableHeap.TypeSystem.OutputType(Output, "type", typeIndex); + Output.AddDisplayStringLine("dumping address {0} as type {1}:{2} (type index {3})", address, CurrentTraceableHeap.TypeSystem.Assembly(typeIndex), CurrentTraceableHeap.TypeSystem.QualifiedName(typeIndex), @@ -130,7 +135,18 @@ void DumpObjectMemoryAsType(SegmentedHeap segmentedHeap) { DumpObjectMemory(objectView, typeIndex, 0, Field); } + + Output.EndElement(); } + Output.EndArray(); + } + + void OutputWarning(string message) + { + Output.BeginElement(); + Output.AddProperty("message", message); + Output.AddDisplayStringLine(message); + Output.EndElement(); } void DumpObjectMemory(SegmentedHeap segmentedHeap) @@ -148,14 +164,21 @@ void DumpObjectMemory(SegmentedHeap segmentedHeap) StringBuilder sb = new(); DescribeAddress(address, sb); - Output.WriteLine(sb.ToString()); + Output.AddDisplayStringLine(sb.ToString()); string[] fields = Rule.ParseSelector(Selector); - Selector selector = CurrentTraceableHeap.TypeSystem.BindSelector(Output.WriteLine, typeIndex, fields, expectDynamic: false, expectReferenceType: false); + Output.BeginArray("warnings"); + Selector selector = CurrentTraceableHeap.TypeSystem.BindSelector(OutputWarning, typeIndex, fields, expectDynamic: false, expectReferenceType: false); + Output.EndArray(); + if (selector.StaticPrefix != null) { - foreach ((SegmentedHeap.ValueReference valueReference, NativeWord _) in segmentedHeap.InterpretSelector((_, message) => Output.WriteLine(message), address, "command line", selector)) + Output.BeginArray("selected"); + + foreach ((SegmentedHeap.ValueReference valueReference, NativeWord _) in segmentedHeap.InterpretSelector((_, message) => OutputWarning(message), address, "command line", selector)) { + Output.BeginElement(); + if (valueReference.WithHeader) { DumpObjectMemory(valueReference.AddressOfContainingObject, segmentedHeap, 1); @@ -164,11 +187,15 @@ void DumpObjectMemory(SegmentedHeap segmentedHeap) { sb.Clear(); DescribeAddress(valueReference.AddressOfContainingObject, sb); - Output.WriteLineIndented(1, sb.ToString()); + Output.AddDisplayStringLineIndented(1, sb.ToString()); DumpValueTypeMemory(valueReference.ValueView, valueReference.TypeIndex, 2, Field); } + + Output.EndElement(); } + + Output.EndArray(); } } else @@ -181,7 +208,7 @@ void DumpObjectMemory(NativeWord address, SegmentedHeap segmentedHeap, int inden { StringBuilder sb = new(); DescribeAddress(address, sb); - Output.WriteLineIndented(indent, sb.ToString()); + Output.AddDisplayStringLineIndented(indent, sb.ToString()); MemoryView objectView = segmentedHeap.GetMemoryViewForAddress(address); DumpObjectMemory(address, objectView, indent, Field); diff --git a/Commands/DumpRootsCommand.cs b/Commands/DumpRootsCommand.cs index 8ba7bea..be3404c 100644 --- a/Commands/DumpRootsCommand.cs +++ b/Commands/DumpRootsCommand.cs @@ -35,6 +35,9 @@ public override void Run() var sb = new StringBuilder(); IRootSet rootSet = CurrentRootSet; + + Output.BeginArray("roots"); + for (int rootIndex = 0; rootIndex < rootSet.NumberOfRoots; rootIndex++) { PointerInfo pointerInfo = rootSet.GetRoot(rootIndex); @@ -43,16 +46,22 @@ public override void Run() continue; } + Output.BeginElement(); + DescribePointerInfo(pointerInfo, sb); if (sb.Length > 0) { - Output.WriteLine("{0}: {1} -> {2}", + Output.AddDisplayStringLine("{0}: {1} -> {2}", rootIndex, - rootSet.DescribeRoot(rootIndex, fullyQualified: true), + rootSet.DescribeRoot(rootIndex, Output, fullyQualified: true), sb); sb.Clear(); } + + Output.EndElement(); } + + Output.EndArray(); } public override string HelpText => "dumproots"; diff --git a/Commands/DumpSegmentCommand.cs b/Commands/DumpSegmentCommand.cs index d6a65f6..7b84c69 100644 --- a/Commands/DumpSegmentCommand.cs +++ b/Commands/DumpSegmentCommand.cs @@ -1,4 +1,4 @@ -/* +/* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the @@ -44,7 +44,9 @@ public override void Run() throw new CommandException($"{Address} is not an address in, or an index of, a managed heap segment"); } - Output.WriteLine("{0}", segment); + StringBuilder sb = new(); + segment.Describe(Output, sb); + Output.AddDisplayStringLine("{0}", sb.ToString()); if (Objects) { @@ -55,7 +57,7 @@ public override void Run() HexDumpAsAddresses(segment.MemoryView, segment.StartAddress); } - Output.WriteLine(); + Output.AddDisplayStringLine(string.Empty); } void DumpObjects(HeapSegment segment) @@ -77,6 +79,8 @@ void DumpObjects(HeapSegment segment) } } + Output.BeginArray("objects"); + var sb = new StringBuilder(); NativeWord previousAddress = segment.StartAddress; foreach (KeyValuePair kvp in objectMap) @@ -84,15 +88,23 @@ void DumpObjects(HeapSegment segment) NativeWord objectAddress = CurrentMemorySnapshot.Native.From(kvp.Key); if (objectAddress != previousAddress) { - Output.WriteLine("Gap of size {0}", objectAddress.Value - previousAddress.Value); + ulong gapSize = objectAddress.Value - previousAddress.Value; + Output.BeginElement(); + Output.AddProperty("gapOfSize", (long)gapSize); + Output.AddDisplayStringLine("Gap of size {0}", gapSize); + Output.EndElement(); } + Output.BeginElement(); DescribeAddress(objectAddress, sb); - Output.WriteLine(sb.ToString()); + Output.AddDisplayStringLine(sb.ToString()); sb.Clear(); + Output.EndElement(); previousAddress = objectAddress + kvp.Value; } + + Output.EndArray(); } public override string HelpText => "dumpseg
['objects]"; diff --git a/Commands/DumpTypeCommand.cs b/Commands/DumpTypeCommand.cs index ceb7b52..33a0572 100644 --- a/Commands/DumpTypeCommand.cs +++ b/Commands/DumpTypeCommand.cs @@ -1,4 +1,4 @@ -/* +/* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the @@ -38,54 +38,80 @@ public override void Run() if (TypeIndexOrPattern == null) { // Dump all types. - Output.WriteLine("Number of type indices: {0}", typeSystem.NumberOfTypeIndices); + Output.AddProperty("numberOfTypeIndices", typeSystem.NumberOfTypeIndices); + Output.AddDisplayStringLine("Number of type indices: {0}", typeSystem.NumberOfTypeIndices); + Output.BeginArray("types"); for (int typeIndex = 0; typeIndex < typeSystem.NumberOfTypeIndices; typeIndex++) { - DumpType(typeIndex, 0); + if (!Statics || HasStaticFields(typeIndex)) + { + Output.BeginElement(); + DumpType(typeIndex, 0); + Output.EndElement(); + } } + Output.EndArray(); } else { + Output.BeginArray("types"); TypeSet typeSet = TypeIndexOrPattern.ResolveTypeIndexOrPattern(Context, IncludeDerived); foreach (int typeIndex in typeSet.TypeIndices) { - DumpType(typeIndex, 0); + if (!Statics || HasStaticFields(typeIndex)) + { + Output.BeginElement(); + DumpType(typeIndex, 0); + Output.EndElement(); + } } + Output.EndArray(); } } void DumpType(int typeIndex, int indent) { - if (Statics && !HasStaticFields(typeIndex)) - { - return; - } - TypeSystem typeSystem = CurrentTraceableHeap.TypeSystem; - Output.WriteLineIndented(indent, "Type {0}: qualified name {1}:{2}, {3} with base size {4}, rank {5}", + string kind = typeSystem.Kind(typeIndex); + typeSystem.OutputType(Output, "type", typeIndex); + Output.AddProperty("kind", kind); + Output.AddProperty("baseSize", typeSystem.BaseSize(typeIndex)); + Output.AddProperty("rank", typeSystem.Rank(typeIndex)); + Output.AddDisplayStringLineIndented(indent, "Type {0}: qualified name {1}:{2}, {3} type with base size {4}, rank {5}", typeIndex, typeSystem.Assembly(typeIndex), typeSystem.QualifiedName(typeIndex), - typeSystem.IsValueType(typeIndex) ? "value type" : typeSystem.IsArray(typeIndex) ? "array" : "object", + kind, typeSystem.BaseSize(typeIndex), typeSystem.Rank(typeIndex)); if (Verbose || Statics) { + Output.BeginArray("fields"); + int numberOfFields = typeSystem.NumberOfFields(typeIndex); for (int fieldNumber = 0; fieldNumber < numberOfFields; fieldNumber++) { + Output.BeginElement(); + bool isStatic = typeSystem.FieldIsStatic(typeIndex, fieldNumber); int fieldTypeIndex = typeSystem.FieldType(typeIndex, fieldNumber); if (Statics && isStatic || !Statics && !isStatic) { - Output.WriteLineIndented(indent + 1, "{0} field {1} (number {2}) at offset {3}: {4} type {5} (index {6})", + Output.AddProperty("isStatic", typeSystem.FieldIsStatic(typeIndex, fieldNumber).ToString()); + Output.AddProperty("fieldName", typeSystem.FieldName(typeIndex, fieldNumber)); + Output.AddProperty("fieldNumber", fieldNumber); + Output.AddProperty("fieldOffset", typeSystem.FieldOffset(typeIndex, fieldNumber, withHeader: true)); + Output.AddProperty("fieldKind", typeSystem.Kind(typeIndex)); + typeSystem.OutputType(Output, "fieldType", fieldTypeIndex); + Output.AddDisplayStringLineIndented(indent + 1, "{0} field {1} (number {2}) at offset {3}: {4} type {5}:{6} (index {7})", typeSystem.FieldIsStatic(typeIndex, fieldNumber) ? "Static" : "Instance", typeSystem.FieldName(typeIndex, fieldNumber), fieldNumber, typeSystem.FieldOffset(typeIndex, fieldNumber, withHeader: true), - typeSystem.IsValueType(fieldTypeIndex) ? "value" : typeSystem.IsArray(fieldTypeIndex) ? "array" : "object", + typeSystem.Kind(fieldTypeIndex), + typeSystem.Assembly(fieldTypeIndex), typeSystem.QualifiedName(fieldTypeIndex), fieldTypeIndex); } @@ -99,10 +125,15 @@ void DumpType(int typeIndex, int indent) } else { - Output.WriteLineIndented(indent + 2, "Uninitialized"); + Output.AddProperty("isInitialized", "false"); + Output.AddDisplayStringLineIndented(indent + 2, "Uninitialized"); } } + + Output.EndElement(); } + + Output.EndArray(); } int baseOrElementTypeIndex = typeSystem.BaseOrElementTypeIndex(typeIndex); @@ -110,7 +141,8 @@ void DumpType(int typeIndex, int indent) { if (Verbose) { - Output.WriteLineIndented(indent + 1, "{0} type {1} (index {2})", + typeSystem.OutputType(Output, typeSystem.IsArray(typeIndex) ? "elementType" : "baseType", baseOrElementTypeIndex); + Output.AddDisplayStringLineIndented(indent + 1, "{0} type {1} (index {2})", typeSystem.IsArray(typeIndex) ? "Element" : "Base", typeSystem.QualifiedName(baseOrElementTypeIndex), baseOrElementTypeIndex); @@ -118,7 +150,9 @@ void DumpType(int typeIndex, int indent) if (Recursive) { + Output.BeginChild("baseType"); DumpType(baseOrElementTypeIndex, indent + 1); + Output.EndChild(); } } } diff --git a/Commands/FindCommand.cs b/Commands/FindCommand.cs index 5f70408..fc6b12a 100644 --- a/Commands/FindCommand.cs +++ b/Commands/FindCommand.cs @@ -1,4 +1,4 @@ -/* +/* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the @@ -7,6 +7,7 @@ using MemorySnapshotAnalyzer.AbstractMemorySnapshot; using MemorySnapshotAnalyzer.CommandInfrastructure; +using System.Text; namespace MemorySnapshotAnalyzer.Commands { @@ -29,7 +30,10 @@ public override void Run() Native native = CurrentMemorySnapshot.Native; + Output.BeginArray("instances"); + long instancesFound = 0; + StringBuilder sb = new(); for (int i = 0; i < segmentedHeap.NumberOfSegments; i++) { HeapSegment segment = segmentedHeap.GetSegment(i); @@ -40,14 +44,22 @@ public override void Run() NativeWord value = memoryView.ReadNativeWord(offset, native); if (value == NativeWord) { - Output.WriteLine("{0} : {1}", segment.StartAddress + offset, segment); + Output.BeginElement(); + Output.AddProperty("address", (segment.StartAddress + offset).ToString()); + segment.Describe(Output, sb); + Output.AddDisplayStringLine("{0} : {1}", segment.StartAddress + offset, sb.ToString()); + sb.Clear(); instancesFound++; + Output.EndElement(); } } } - Output.WriteLine(); - Output.WriteLine("total instances found = {0}", instancesFound); + Output.EndArray(); + + Output.AddDisplayStringLine(string.Empty); + Output.AddProperty("totalNumberOfInstances", instancesFound); + Output.AddDisplayStringLine("total instances found = {0}", instancesFound); } public override string HelpText => "find "; diff --git a/Commands/HeapDomCommand.cs b/Commands/HeapDomCommand.cs index 454eacb..f7e6c40 100644 --- a/Commands/HeapDomCommand.cs +++ b/Commands/HeapDomCommand.cs @@ -81,9 +81,9 @@ public override void Run() try { using (var fileOutput = new FileOutput(OutputFilename!, useUnixNewlines: true)) - using (RedirectOutput(fileOutput)) + using (RedirectOutput(new PassthroughStructuredOutput(fileOutput))) { - Output.Write("data="); + Output.AddDisplayString("data="); DumpTree(); } @@ -94,7 +94,8 @@ public override void Run() throw new CommandException(ex.Message); } - Output.WriteLine("wrote {0} nodes", m_numberOfNodesWritten); + Output.AddProperty("numberOfNodesWritten", m_numberOfNodesWritten); + Output.AddDisplayStringLine("wrote {0} nodes", m_numberOfNodesWritten); } void StartHtmlFile() @@ -207,23 +208,23 @@ long DumpTree(int nodeIndex, IComparer comparer, bool needComma, int depth, m_numberOfNodesWritten++; if (needComma) { - Output.Write(","); + Output.AddDisplayString(","); } - Output.Write("{{\"name\":\"{0}\",", CurrentHeapDom.Backtracer.DescribeNodeIndex(nodeIndex, fullyQualified: true)); + Output.AddDisplayString("{{\"name\":\"{0}\",", CurrentHeapDom.Backtracer.DescribeNodeIndex(nodeIndex, Output, fullyQualified: true)); if (nodeIndex == CurrentHeapDom.RootNodeIndex) { - Output.Write("\"filename\":{0},", + Output.AddDisplayString("\"filename\":{0},", JsonConvert.ToString(CurrentMemorySnapshot.Filename)); - Output.Write("\"heapDomCommandLine\":{0},", + Output.AddDisplayString("\"heapDomCommandLine\":{0},", JsonConvert.ToString(Repl.CurrentCommandLine)); - Output.Write("\"context\":{0},", + Output.AddDisplayString("\"context\":{0},", JsonConvert.ToString(string.Join('\n', Context.Serialize()))); } if (NodeTypes) { - Output.Write("\"nodetype\":\"{0}\",", CurrentHeapDom.Backtracer.NodeType(nodeIndex)); + Output.AddDisplayString("\"nodetype\":\"{0}\",", CurrentHeapDom.Backtracer.NodeType(nodeIndex)); } bool elideChildren = false; @@ -248,7 +249,7 @@ long DumpTree(int nodeIndex, IComparer comparer, bool needComma, int depth, if (diffString != null) { - Output.Write("\"diff\":\"{0}\",", diffString); + Output.AddDisplayString("\"diff\":\"{0}\",", diffString); } } @@ -270,17 +271,17 @@ long DumpTree(int nodeIndex, IComparer comparer, bool needComma, int depth, } else if (numberOfChildren == 0) { - Output.Write("\"value\":{0}", CurrentHeapDom.NodeSize(nodeIndex)); + Output.AddDisplayString("\"value\":{0}", CurrentHeapDom.NodeSize(nodeIndex)); } else if (elideChildren) { - Output.Write("\"value\":{0}", CurrentHeapDom.TreeSize(nodeIndex)); + Output.AddDisplayString("\"value\":{0}", CurrentHeapDom.TreeSize(nodeIndex)); } else { DumpChildren(children!, CurrentHeapDom.TreeSize(nodeIndex), comparer, depth); } - Output.Write("}"); + Output.AddDisplayString("}"); numberOfElidedNodes = 0; return CurrentHeapDom.TreeSize(nodeIndex); @@ -288,7 +289,7 @@ long DumpTree(int nodeIndex, IComparer comparer, bool needComma, int depth, void DumpChildren(List children, long treeSize, IComparer comparer, int depth) { - Output.Write("\"children\":["); + Output.AddDisplayString("\"children\":["); var sortedChildren = new int[children.Count]; for (int i = 0; i < children.Count; i++) @@ -321,29 +322,29 @@ void DumpChildren(List children, long treeSize, IComparer comparer, in { if (needComma) { - Output.Write(","); + Output.AddDisplayString(","); } if (numberOfElidedNodes > 0) { - Output.Write("{{\"name\":\"elided+{0}\",", numberOfElidedNodes); + Output.AddDisplayString("{{\"name\":\"elided+{0}\",", numberOfElidedNodes); if (NodeTypes) { - Output.Write("\"nodetype\":\"elided\","); + Output.AddDisplayString("\"nodetype\":\"elided\","); } } else { - Output.Write("{\"name\":\"intrinsic\","); + Output.AddDisplayString("{\"name\":\"intrinsic\","); if (NodeTypes) { - Output.Write("\"nodetype\":\"intrinsic\","); + Output.AddDisplayString("\"nodetype\":\"intrinsic\","); } } - Output.Write("\"value\":{0}}}", intrinsicSize); + Output.AddDisplayString("\"value\":{0}}}", intrinsicSize); } - Output.Write("]"); + Output.AddDisplayString("]"); } int NumberOfNodesInTree(int nodeIndex) diff --git a/Commands/HeapDomStatsCommand.cs b/Commands/HeapDomStatsCommand.cs index 2c109bb..97d7e23 100644 --- a/Commands/HeapDomStatsCommand.cs +++ b/Commands/HeapDomStatsCommand.cs @@ -1,4 +1,4 @@ -/* +/* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the @@ -63,7 +63,10 @@ void DumpStats() } } - Output.WriteLine("Number of nodes dominated only by root node: {0} objects out of {1} toplevel nodes, for a total of {2} bytes", + Output.AddProperty("totalNumberOfObjects", totalObjectCount); + Output.AddProperty("totalNumberOfToplevelNodes", totalToplevelCount); + Output.AddProperty("totalSize", totalSize); + Output.AddDisplayStringLine("Number of nodes dominated only by root node: {0} objects out of {1} toplevel nodes, for a total of {2} bytes", totalObjectCount, totalToplevelCount, totalSize); @@ -78,16 +81,23 @@ void DumpStats() Array.Sort(statsArray, (a, b) => b.Value.Count.CompareTo(a.Value.Count)); } + Output.BeginArray("toplevelNodes"); foreach (var kvp in statsArray) { + Output.BeginElement(); int typeIndex = kvp.Key; int count = kvp.Value.Count; - Output.WriteLine("Type {0} (index {1}): {2} instances, total {3} bytes", + CurrentTraceableHeap.TypeSystem.OutputType(Output, "nodeType", typeIndex); + Output.AddProperty("count", count); + Output.AddDisplayStringLine("Type {0}:{1} (index {2}): {3} instances, total {4} bytes", + CurrentTraceableHeap.TypeSystem.Assembly(typeIndex), CurrentTraceableHeap.TypeSystem.QualifiedName(typeIndex), typeIndex, count, totalSizeByType[typeIndex]); + Output.EndElement(); } + Output.EndArray(); } public override string HelpText => "heapdomstats ['bysize]"; diff --git a/Commands/ListObjectsCommand.cs b/Commands/ListObjectsCommand.cs index d798a46..4bf0320 100644 --- a/Commands/ListObjectsCommand.cs +++ b/Commands/ListObjectsCommand.cs @@ -200,14 +200,16 @@ internal void Add(int postorderIndex) m_postorderIndices.Add(postorderIndex); } - internal void DumpSummary(IOutput output) + internal void DumpSummary(IStructuredOutput output) { - output.WriteLine("number of live objects = {0}, total size = {1}", + output.AddProperty("numberOfObjects", m_numberOfObjects); + output.AddProperty("totalSize", m_totalSize); + output.AddDisplayStringLine("number of live objects = {0}, total size = {1}", m_numberOfObjects, m_totalSize); } - internal void DumpStatistics(IOutput output) + internal void DumpStatistics(IStructuredOutput output) { KeyValuePair[] kvps = m_perTypeCounts.ToArray(); switch (m_sortOrder) @@ -224,16 +226,29 @@ internal void DumpStatistics(IOutput output) break; } + output.BeginArray("statistics"); + TypeSystem typeSystem = m_context.CurrentTraceableHeap!.TypeSystem; foreach (KeyValuePair kvp in kvps) { - output.WriteLine("{0} object(s) of type {1}:{2} (type index {3}, total size {4})", + output.BeginElement(); + + output.AddProperty("numberOfObjects", kvp.Value.count); + output.AddProperty("assembly", typeSystem.Assembly(kvp.Key)); + output.AddProperty("qualifiedName", typeSystem.QualifiedName(kvp.Key)); + output.AddProperty("typeIndex", kvp.Key); + output.AddProperty("totalSize", kvp.Value.size); + output.AddDisplayStringLine("{0} object(s) of type {1}:{2} (type index {3}, total size {4})", kvp.Value.count, typeSystem.Assembly(kvp.Key), typeSystem.QualifiedName(kvp.Key), kvp.Key, kvp.Value.size); + + output.EndElement(); } + + output.EndArray(); } internal IEnumerable ForAll() @@ -313,26 +328,45 @@ void ListObjects(SortOrder sortOrder, int domParentPostorderIndex) } else if (ExecCommandLine != null) { - foreach (int postorderIndex in selection.ForAll()) + Output.BeginArray("commandExecutions"); + try { - Repl.RunCommand($"{ExecCommandLine} {postorderIndex}"); + foreach (int postorderIndex in selection.ForAll()) + { + Repl.RunCommand($"{ExecCommandLine} {postorderIndex}"); + } + } + finally + { + Output.EndArray(); } } else { + Output.BeginArray("objects"); + var sb = new StringBuilder(); foreach (int postorderIndex in selection.ForAll()) { + Output.BeginElement(); + NativeWord address = CurrentTracedHeap.PostorderAddress(postorderIndex); DescribeAddress(address, sb); if (sortOrder == SortOrder.SortByDomSize) { int typeIndex = CurrentTracedHeap.PostorderTypeIndexOrSentinel(postorderIndex); - sb.AppendFormat(" (dom size {0})", selection.GetSize(postorderIndex, typeIndex)); + long size = selection.GetSize(postorderIndex, typeIndex); + sb.AppendFormat(" (dom size {0})", size); + Output.AddProperty("domSize", size); } - Output.WriteLine(sb.ToString()); + + Output.AddDisplayStringLine(sb.ToString()); sb.Clear(); + + Output.EndElement(); } + + Output.EndArray(); } } diff --git a/Commands/ListSegmentsCommand.cs b/Commands/ListSegmentsCommand.cs index ed6c50f..8d7679d 100644 --- a/Commands/ListSegmentsCommand.cs +++ b/Commands/ListSegmentsCommand.cs @@ -1,4 +1,4 @@ -/* +/* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the @@ -7,6 +7,7 @@ using MemorySnapshotAnalyzer.AbstractMemorySnapshot; using MemorySnapshotAnalyzer.CommandInfrastructure; +using System.Text; namespace MemorySnapshotAnalyzer.Commands { @@ -41,18 +42,28 @@ public override void Run() numberOfObjectSegments++; } } - Output.WriteLine($"total number of segments: {segmentedHeap.NumberOfSegments} ({numberOfObjectSegments} object, {numberOfRttiSegments} RTTI)"); + Output.AddProperty("totalNumberOfSegments", segmentedHeap.NumberOfSegments); + Output.AddProperty("numberOfObjectSegments", numberOfObjectSegments); + Output.AddProperty("numberOfRttiSegments", numberOfRttiSegments); + Output.AddDisplayStringLine($"total number of segments: {segmentedHeap.NumberOfSegments} ({numberOfObjectSegments} object, {numberOfRttiSegments} RTTI)"); + + Output.BeginArray("segments"); + StringBuilder sb = new(); for (int i = 0; i < segmentedHeap.NumberOfSegments; i++) { HeapSegment segment = segmentedHeap.GetSegment(i); if (!RttiOnly || segment.IsRuntimeTypeInformation) { - Output.WriteLine("segment {0,6}: {1}", - i, - segment); + Output.BeginElement(); + Output.AddDisplayString("segment {0,6}: ", i); + segment.Describe(Output, sb); + Output.AddDisplayStringLine(sb.ToString()); + sb.Clear(); + Output.EndElement(); } } + Output.EndArray(); } public override string HelpText => "listsegs ['rttionly]"; diff --git a/Commands/LoadCommand.cs b/Commands/LoadCommand.cs index db4b231..67995d2 100644 --- a/Commands/LoadCommand.cs +++ b/Commands/LoadCommand.cs @@ -1,4 +1,4 @@ -/* +/* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the @@ -65,7 +65,7 @@ public override void Run() } context.CurrentMemorySnapshot = memorySnapshot; - Output.WriteLine($"{memorySnapshot.Format} memory snapshot loaded successfully"); + Output.AddDisplayStringLine($"{memorySnapshot.Format} memory snapshot loaded successfully"); Repl.DumpContexts(); } diff --git a/Commands/PrintCommand.cs b/Commands/PrintCommand.cs index b1d8685..f44d84d 100644 --- a/Commands/PrintCommand.cs +++ b/Commands/PrintCommand.cs @@ -1,4 +1,4 @@ -/* +/* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the @@ -21,19 +21,21 @@ public PrintCommand(Repl repl) : base(repl) {} public override void Run() { + Value!.Describe(Output); + switch (Value!.ArgumentType) { case CommandLineArgumentType.Atom: - Output.WriteLine("(Atom) {0}", Value.AtomValue); + Output.AddDisplayStringLine("(Atom) {0}", Value.AtomValue); break; case CommandLineArgumentType.String: - Output.WriteLine("(String) \"{0}\"", Value.StringValue); + Output.AddDisplayStringLine("(String) \"{0}\"", Value.StringValue); break; case CommandLineArgumentType.Integer: - Output.WriteLine("(Integer) {0}", Value.IntegerValue); + Output.AddDisplayStringLine("(Integer) {0}", Value.IntegerValue); if (Context.CurrentMemorySnapshot != null) { - Output.WriteLine("(NativeWord) {0}", Value.AsNativeWord(CurrentMemorySnapshot.Native)); + Output.AddDisplayStringLine("(NativeWord) {0}", Value.AsNativeWord(CurrentMemorySnapshot.Native)); } break; default: diff --git a/Commands/ReferenceClassifierCommand.cs b/Commands/ReferenceClassifierCommand.cs index c4291f2..80d7cd5 100644 --- a/Commands/ReferenceClassifierCommand.cs +++ b/Commands/ReferenceClassifierCommand.cs @@ -86,14 +86,14 @@ public override void Run() try { using (var fileOutput = new FileOutput(ReferenceClassifierFilenameToSave, useUnixNewlines: true)) - using (RedirectOutput(fileOutput)) + using (RedirectOutput(new PassthroughStructuredOutput(fileOutput))) { List = true; Verbose = true; - Output.WriteLine("###"); - Output.WriteLine("### THIS FILE WAS GENERATED BY MemorySnapshotAnalyzer USING \"rc 'fromdll\"; DO NOT EDIT"); - Output.WriteLine("###"); - Output.WriteLine(); + Output.AddDisplayStringLine("###"); + Output.AddDisplayStringLine("### THIS FILE WAS GENERATED BY MemorySnapshotAnalyzer USING \"rc 'fromdll\"; DO NOT EDIT"); + Output.AddDisplayStringLine("###"); + Output.AddDisplayStringLine(string.Empty); RunForGroups(); } } @@ -170,10 +170,28 @@ List ResolveGroupNames() void RunForGroups() { + if (List) + { + Output.BeginArray("groups"); + } + bool dumpContext = false; foreach (string groupName in ResolveGroupNames()) { + if (List) + { + Output.BeginElement(); + } RunForGroup(groupName, ref dumpContext); + if (List) + { + Output.EndElement(); + } + } + + if (List) + { + Output.EndArray(); } if (dumpContext) @@ -216,35 +234,44 @@ void RunForGroup(string groupName, ref bool dumpContext) { if (store.TryGetGroup(groupName, out ReferenceClassifierGroup? group)) { - DumpGroup(group!, verbose: Verbose); + DumpGroup(group, verbose: Verbose); } else { - Output.WriteLine($"unknown reference classifier group \"{group}\""); + throw new CommandException($"unknown group \"{groupName}\""); } } } void DumpGroup(ReferenceClassifierGroup group, bool verbose) { + Output.AddProperty("groupName", group.Name); + Output.AddProperty("numberOfRules", group.NumberOfRules); + if (verbose) { - Output.WriteLine("[{0}]", group.Name); - Output.WriteLine(); + Output.AddDisplayStringLine("[{0}]", group.Name); + Output.AddDisplayStringLine(string.Empty); SortedSet ruleTexts = new(); foreach (Rule rule in group.GetRules()) { ruleTexts.Add(rule.ToString()!); } + + Output.BeginArray("rules"); foreach (string ruleText in ruleTexts) { - Output.WriteLine("{0}", ruleText); + Output.BeginElement(); + Output.AddProperty("ruleText", ruleText); + Output.AddDisplayStringLine("{0}", ruleText); + Output.EndElement(); } - Output.WriteLine(); + Output.EndArray(); + Output.AddDisplayStringLine(string.Empty); } else { - Output.WriteLine("reference classifier group \"{0}\": {1} rule(s)", group.Name, group.NumberOfRules); + Output.AddDisplayStringLine("reference classifier group \"{0}\": {1} rule(s)", group.Name, group.NumberOfRules); } } diff --git a/Commands/StatsCommand.cs b/Commands/StatsCommand.cs index 54cb3c2..00dd4f4 100644 --- a/Commands/StatsCommand.cs +++ b/Commands/StatsCommand.cs @@ -1,4 +1,4 @@ -/* +/* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the @@ -8,6 +8,7 @@ using MemorySnapshotAnalyzer.AbstractMemorySnapshot; using MemorySnapshotAnalyzer.CommandInfrastructure; using System.Collections.Generic; +using System.Text; namespace MemorySnapshotAnalyzer.Commands { @@ -30,29 +31,35 @@ public override void Run() { if (TypeSystemStatistics) { + Output.BeginChild("typeSystemStatistics"); DumpTypeSystemStatistics(); - Output.WriteLine(); + Output.AddDisplayStringLine(string.Empty); + Output.EndChild(); } if (HeapStatistics) { + Output.BeginChild("heapStatistics"); DumpHeapStatistics(); - Output.WriteLine(); + Output.AddDisplayStringLine(string.Empty); + Output.EndChild(); } if (Fragmentation) { + Output.BeginChild("fragmentationStatistics"); DumpFragmentation(); - Output.WriteLine(); + Output.AddDisplayStringLine(string.Empty); + Output.EndChild(); } } void DumpTypeSystemStatistics() { TypeSystem typeSystem = CurrentTraceableHeap.TypeSystem; - foreach (string s in typeSystem.DumpStats()) + foreach (string s in typeSystem.DumpStats(Output)) { - Output.WriteLine(s); + Output.AddDisplayStringLine(s); } } @@ -87,21 +94,28 @@ void DumpHeapStatistics() } } - Output.WriteLine("total heap size {0}", totalSize); + Output.AddProperty("totalSize", totalSize); + Output.AddDisplayStringLine("total heap size {0}", totalSize); + + Output.BeginArray("histogram"); foreach (var kvp in histogram) { - int objectCount; - objectSegmentHistogram.TryGetValue(kvp.Key, out objectCount); - int rttiCount; - rttiSegmentHistogram.TryGetValue(kvp.Key, out rttiCount); - Output.WriteLine("size {0,8}: total segments {1} (object {2}, rtti {3})", kvp.Key, kvp.Value, objectCount, rttiCount); + Output.BeginElement(); + objectSegmentHistogram.TryGetValue(kvp.Key, out int objectCount); + rttiSegmentHistogram.TryGetValue(kvp.Key, out int rttiCount); + Output.AddProperty("size", kvp.Key); + Output.AddProperty("totalNumberOfSegments", kvp.Value); + Output.AddProperty("numberOfObjectSegments", objectCount); + Output.AddProperty("numberOfRttiSegments", rttiCount); + Output.AddDisplayStringLine("size {0,8}: total segments {1} (object {2}, rtti {3})", kvp.Key, kvp.Value, objectCount, rttiCount); + Output.EndElement(); } + Output.EndArray(); } static void Tally(SortedDictionary histogram, long size) { - int count; - if (histogram.TryGetValue(size, out count)) + if (histogram.TryGetValue(size, out int count)) { histogram[size] = count + 1; } @@ -158,20 +172,33 @@ void DumpFragmentation() segmentStats.Add(segment.StartAddress.Value, tuple); } - Output.WriteLine("total heap size = {0}, used size = {1}, free size = {2}", + Output.AddProperty("totalHeapSize", (long)totalHeapSize); + Output.AddProperty("totalUsedSize", (long)totalUsedSize); + Output.AddProperty("totalFreeSize", (long)totalFreeSize); + Output.AddDisplayStringLine("total heap size = {0}, used size = {1}, free size = {2}", totalHeapSize, totalUsedSize, totalFreeSize); + Output.BeginArray("segmentStats"); + StringBuilder sb = new(); foreach (KeyValuePair kvp in segmentStats) { + Output.BeginElement(); HeapSegment segment = segmentedHeap.GetSegmentForAddress(CurrentMemorySnapshot.Native.From(kvp.Key))!; - Output.WriteLine("{0}: largest gap {1}, total object size {2}, total free size {3}", - segment, + segment.Describe(Output, sb); + Output.AddProperty("largestGap", (long)kvp.Value.largestGap); + Output.AddProperty("totalObjectSize", (long)kvp.Value.totalObjectSize); + Output.AddProperty("totalFreeSize", (long)kvp.Value.totalFreeSize); + Output.AddDisplayStringLine("{0}: largest gap {1}, total object size {2}, total free size {3}", + sb.ToString(), kvp.Value.largestGap, kvp.Value.totalObjectSize, kvp.Value.totalFreeSize); + sb.Clear(); + Output.EndElement(); } + Output.EndArray(); } (ulong largestGap, ulong totalObjectSize, ulong totalFreeSize) ComputeUsageForSegment(HeapSegment segment, SortedDictionary objectMap) diff --git a/MemorySnapshotAnalyzer/Program.cs b/MemorySnapshotAnalyzer/Program.cs index 0c274be..e4c9935 100644 --- a/MemorySnapshotAnalyzer/Program.cs +++ b/MemorySnapshotAnalyzer/Program.cs @@ -12,14 +12,16 @@ using Microsoft.Extensions.Configuration; using System; using System.Collections.Generic; +using System.IO; static class Program { - class CommandLineArguments + sealed class CommandLineArguments { internal bool Help; internal bool NonInteractive; internal string? LogOutputFilename; + internal string? JsonOutputFilename; internal string? StartupSnapshotFilename; internal List BatchFilenames = new(); @@ -48,6 +50,19 @@ internal CommandLineArguments(string[] args) LogOutputFilename = args[++i]; break; + case "--json": + if (JsonOutputFilename != null) + { + throw new CommandException("--json may be specified at most once"); + } + + if (i + 1 >= args.Length) + { + throw new CommandException("--json must be given an argument"); + } + + JsonOutputFilename = args[++i]; + break; case "--load": if (StartupSnapshotFilename != null) { @@ -91,6 +106,8 @@ public static int Main(string[] args) int returnValue = 0; FileOutput? errorOutput = null; + JsonStructuredOutput? jsonOutput = null; + string? jsonOutputFilename = null; Repl.RunWithHandler(() => { CommandLineArguments commandLineArguments = new(args); @@ -115,7 +132,21 @@ public static int Main(string[] args) output = new ConsoleOutput(); } - using Repl repl = new(configuration, output, new MemoryLoggerFactory(), isInteractive: !commandLineArguments.NonInteractive); + IStructuredOutput structuredOutput; + if (commandLineArguments.JsonOutputFilename != null) + { + jsonOutput = new JsonStructuredOutput(new PassthroughStructuredOutput(output), keepDisplayStrings: false); + structuredOutput = jsonOutput; + jsonOutputFilename = commandLineArguments.JsonOutputFilename; + + jsonOutput.BeginArray("commands"); + } + else + { + structuredOutput = new PassthroughStructuredOutput(output); + } + + using Repl repl = new(configuration, output, structuredOutput, new MemoryLoggerFactory(), isInteractive: !commandLineArguments.NonInteractive); repl.AddMemorySnapshotLoader(new UnityMemorySnapshotLoader()); @@ -181,6 +212,12 @@ public static int Main(string[] args) errorOutput.Dispose(); } + if (jsonOutput != null) + { + using FileStream fileStream = new(jsonOutputFilename!, FileMode.Create, FileAccess.Write); + jsonOutput.WriteTo(fileStream); + } + return returnValue; } } diff --git a/ReferenceClassifiers/ReferenceClassifierStore.cs b/ReferenceClassifiers/ReferenceClassifierStore.cs index 9665b1e..02da4fe 100644 --- a/ReferenceClassifiers/ReferenceClassifierStore.cs +++ b/ReferenceClassifiers/ReferenceClassifierStore.cs @@ -7,6 +7,7 @@ using MemorySnapshotAnalyzer.AbstractMemorySnapshot; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; namespace MemorySnapshotAnalyzer.ReferenceClassifiers @@ -33,7 +34,7 @@ public void ClearGroup(string groupName) m_referenceClassifierGroups.Remove(groupName); } - public bool TryGetGroup(string groupName, out ReferenceClassifierGroup? group) + public bool TryGetGroup(string groupName, [NotNullWhen(true)] out ReferenceClassifierGroup? group) { return m_referenceClassifierGroups.TryGetValue(groupName, out group); } diff --git a/UnityBackend/UnityManagedHeap.cs b/UnityBackend/UnityManagedHeap.cs index e2d1be0..325350f 100644 --- a/UnityBackend/UnityManagedHeap.cs +++ b/UnityBackend/UnityManagedHeap.cs @@ -87,11 +87,13 @@ public override int GetObjectSize(NativeWord objectAddress, int typeIndex, bool return m_segmentedHeap.UnityManagedTypeSystem.GetObjectSize(objectView, typeIndex, committedOnly); } - public override string? DescribeAddress(NativeWord address) + public override string? DescribeAddress(NativeWord address, IStructuredOutput output) { int typeInfoIndex = m_unityManagedTypeSystem.TypeInfoAddressToIndex(address); if (typeInfoIndex != -1) { + output.AddProperty("addressTargetKind", "vtable"); + TypeSystem.OutputType(output, "vtableType", typeInfoIndex); return string.Format("VTable[{0}:{1}, type index {2}]", m_unityManagedTypeSystem.Assembly(typeInfoIndex), m_unityManagedTypeSystem.QualifiedName(typeInfoIndex), diff --git a/UnityBackend/UnityManagedTypeSystem.cs b/UnityBackend/UnityManagedTypeSystem.cs index a4e597d..5a4c17a 100644 --- a/UnityBackend/UnityManagedTypeSystem.cs +++ b/UnityBackend/UnityManagedTypeSystem.cs @@ -1,4 +1,4 @@ -/* +/* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the @@ -317,19 +317,40 @@ int GetFieldSize(int typeIndex) public override int SystemVoidStarTypeIndex => m_systemVoidStarTypeIndex; - public override IEnumerable DumpStats() + public override IEnumerable DumpStats(IStructuredOutput output) { + output.AddProperty("pointerSize", PointerSize); yield return string.Format("Pointer size: {0}", PointerSize); + + output.AddProperty("objectHeaderSize", m_virtualMachineInformation.ObjectHeaderSize); yield return string.Format("Object header size: {0}", m_virtualMachineInformation.ObjectHeaderSize); + + output.AddProperty("arrayHeaderSize", m_virtualMachineInformation.ArrayHeaderSize); yield return string.Format("Array header size: {0}", m_virtualMachineInformation.ArrayHeaderSize); + + output.AddProperty("arrayBoundsOffsetInHeader", m_virtualMachineInformation.ArrayBoundsOffsetInHeader); yield return string.Format("Array bounds offset in header: {0}", m_virtualMachineInformation.ArrayBoundsOffsetInHeader); + + output.AddProperty("arraySizeOffsetInHeader", m_virtualMachineInformation.ArraySizeOffsetInHeader); yield return string.Format("Array size offset in header: {0}", m_virtualMachineInformation.ArraySizeOffsetInHeader); + + output.AddProperty("allocationGranularity", m_virtualMachineInformation.AllocationGranularity); yield return string.Format("Allocation granularity: {0}", m_virtualMachineInformation.AllocationGranularity); + yield return string.Empty; + + output.AddProperty("numberOfTypeIndices", NumberOfTypeIndices); yield return string.Format("Number of types: {0}", NumberOfTypeIndices); + yield return string.Empty; + + output.AddProperty("systemStringTypeIndex", SystemStringTypeIndex); yield return string.Format("System.String type index: {0}", SystemStringTypeIndex); + + output.AddProperty("systemStringLengthOffset", SystemStringLengthOffset); yield return string.Format("System.String length offset: {0}", SystemStringLengthOffset); + + output.AddProperty("systemStringFirstCharOffset", SystemStringFirstCharOffset); yield return string.Format("System.String first char offset: {0}", SystemStringFirstCharOffset); } } diff --git a/UnityBackend/UnityNativeObjectHeap.cs b/UnityBackend/UnityNativeObjectHeap.cs index e139b8a..0309b7f 100644 --- a/UnityBackend/UnityNativeObjectHeap.cs +++ b/UnityBackend/UnityNativeObjectHeap.cs @@ -130,7 +130,7 @@ public override bool ContainsAddress(NativeWord address) return m_nativeObjectsByAddress.ContainsKey(address.Value); } - public override string? DescribeAddress(NativeWord address) + public override string? DescribeAddress(NativeWord address, IStructuredOutput output) { return null; } diff --git a/UnityBackend/UnityNativeObjectTypeSystem.cs b/UnityBackend/UnityNativeObjectTypeSystem.cs index 4c3955c..315f4a6 100644 --- a/UnityBackend/UnityNativeObjectTypeSystem.cs +++ b/UnityBackend/UnityNativeObjectTypeSystem.cs @@ -1,4 +1,4 @@ -/* +/* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the @@ -136,9 +136,12 @@ public override int GetArrayElementSize(int elementTypeIndex) public override int SystemVoidStarTypeIndex => -1; - public override IEnumerable DumpStats() + public override IEnumerable DumpStats(IStructuredOutput output) { + output.AddProperty("pointerSize", PointerSize); yield return string.Format("Pointer size: {0}", PointerSize); + + output.AddProperty("numberOfTypeIndices", NumberOfTypeIndices); yield return string.Format("Number of types: {0}", NumberOfTypeIndices); } }