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); } }