Skip to content

[swift-inspect] Add json and summary options to DumpRawMetadata #81186

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,18 @@ internal struct Output: TextOutputStream {
}
}

internal func dumpJson(of: (any Encodable), outputFile: String?) throws {
let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
let data = try encoder.encode(of)
let jsonOutput = String(data: data, encoding: .utf8)!
if let outputFile = outputFile {
try jsonOutput.write(toFile: outputFile, atomically: true, encoding: .utf8)
} else {
print(jsonOutput)
}
}

internal struct DumpGenericMetadata: ParsableCommand {
static let configuration = CommandConfiguration(
abstract: "Print the target's generic metadata allocations.")
Expand All @@ -99,7 +111,10 @@ internal struct DumpGenericMetadata: ParsableCommand {
var backtraceOptions: BacktraceOptions

@OptionGroup()
var genericMetadataOptions: GenericMetadataOptions
var metadataOptions: MetadataOptions

@Flag(help: "Show allocations in mangled form")
var mangled: Bool = false
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why has this moved from the metadataOptions group?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MetadataOptions was previously GenericMetadataOptions and only provided options for swift-inspect --dump-generic-metadata. This PR is proposing that three of the four options from GenericMetadataOptions (json, summary', and outputFile) be added to swift-inspect --dump-raw-metadata. Rather than manually type these options out in [DumpRawMetadata.swift](https://github.com/swiftlang/swift/pull/81186/files#diff-bbb7ba64f4c92e0e341aa925f49f73da14ed9d4aeb92ea4f2e8c50f7c10cc129), I opted to share this set of ParsableArguments`, hence the name change.

However, this PR does not implement the pre-existing mangled option in --dump-raw-metadata. Which is why I chose to remove it from this now more generally used GenericMetadataOptions.


func run() throws {
disableStdErrBuffer()
Expand All @@ -126,7 +141,7 @@ internal struct DumpGenericMetadata: ParsableCommand {

return Metadata(ptr: pointer,
allocation: allocation,
name: process.context.name(type: pointer, mangled: genericMetadataOptions.mangled) ?? "<unknown>",
name: process.context.name(type: pointer, mangled: mangled) ?? "<unknown>",
isArrayOfClass: process.context.isArrayOfClass(pointer),
garbage: garbage,
backtrace: currentBacktrace)
Expand All @@ -146,30 +161,30 @@ internal struct DumpGenericMetadata: ParsableCommand {
}
}

if genericMetadataOptions.json {
if metadataOptions.json {
let processMetadata = ProcessMetadata(name: process.processName,
pid: process.processIdentifier as! ProcessIdentifier,
metadata: generics)
allProcesses.append(processMetadata)
} else if !genericMetadataOptions.summary {
} else if !metadataOptions.summary {
try dumpText(process: process, generics: generics)
}
} // inspect

if genericMetadataOptions.json {
if genericMetadataOptions.summary {
try dumpJson(of: metadataSummary)
if metadataOptions.json {
if metadataOptions.summary {
try dumpJson(of: metadataSummary, outputFile: metadataOptions.outputFile)
} else {
try dumpJson(of: allProcesses)
try dumpJson(of: allProcesses, outputFile: metadataOptions.outputFile)
}
} else if genericMetadataOptions.summary {
} else if metadataOptions.summary {
try dumpTextSummary(of: metadataSummary)
}
}

private func dumpText(process: any RemoteProcess, generics: [Metadata]) throws {
var erroneousMetadata: [(ptr: swift_reflection_ptr_t, name: String)] = []
var output = try Output(genericMetadataOptions.outputFile)
var output = try Output(metadataOptions.outputFile)
print("\(process.processName)(\(process.processIdentifier)):\n", to: &output)
print("Address", "Allocation", "Size", "Offset", "isArrayOfClass", "Name", separator: "\t", to: &output)
generics.forEach {
Expand Down Expand Up @@ -198,20 +213,8 @@ internal struct DumpGenericMetadata: ParsableCommand {
print("", to: &output)
}

private func dumpJson(of: (any Encodable)) throws {
let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
let data = try encoder.encode(of)
let jsonOutput = String(data: data, encoding: .utf8)!
if let outputFile = genericMetadataOptions.outputFile {
try jsonOutput.write(toFile: outputFile, atomically: true, encoding: .utf8)
} else {
print(jsonOutput)
}
}

private func dumpTextSummary(of: [String: MetadataSummary]) throws {
var output = try Output(genericMetadataOptions.outputFile)
var output = try Output(metadataOptions.outputFile)
print("Size", "Owners", "Name", separator: "\t", to: &output)
var totalSize = 0
var unknownSize = 0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,23 @@

import ArgumentParser
import SwiftRemoteMirror
import Foundation

private struct AllocatorTagTotal: Encodable {
let name: String
let tag: Int
var totalBytes: Int
}

private struct Summary: Encodable {
let totalBytesAllocated: Int
let allocatorTags: [AllocatorTagTotal]
}

private struct RawMetadataOutput: Encodable {
let allocationList: [swift_metadata_allocation_t]?
let summary: Summary?
}

internal struct DumpRawMetadata: ParsableCommand {
static let configuration = CommandConfiguration(
Expand All @@ -22,8 +39,15 @@ internal struct DumpRawMetadata: ParsableCommand {

@OptionGroup()
var backtraceOptions: BacktraceOptions

@OptionGroup()
var metadataOptions: MetadataOptions

func run() throws {
var allocatorTagTotals = [Int: AllocatorTagTotal]()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's drop AllocatorTagTotal, as we really only need the number of bytes allocated. We can get the name later by calling name(allocation:) in the summary output loop (it's cheap) and the the tag is already in the dictionary key. And a minor style thing, it's a bit nicer to initialize this with the empty dictionary literal like var allocatorTagTotals: [Int: Int] = [:], like allocationList's declaration just below.

Copy link
Contributor Author

@jabrylg jabrylg May 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah realized we can't easily get name(allocation:) outside of the closure because we need process.context to call it. Also can't throw the summary into the closure because it would break --all.

var total: Int = 0
var allocationList: [swift_metadata_allocation_t] = []

try inspect(options: options) { process in
let stacks: [swift_reflection_ptr_t:[swift_reflection_ptr_t]]? =
backtraceOptions.style == nil
Expand All @@ -33,6 +57,18 @@ internal struct DumpRawMetadata: ParsableCommand {
try process.context.allocations.forEach { allocation in
let name: String = process.context.name(allocation: allocation.tag) ?? "<unknown>"
print("Metadata allocation at: \(hex: allocation.ptr) size: \(allocation.size) tag: \(allocation.tag) (\(name))")

if metadataOptions.summary {
if var allocatorTagTotal = allocatorTagTotals[Int(allocation.tag)] {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a fun shorthand way to write this. Assuming this dictionary is now typed [swift_metadata_allocation_tag_t: Int] like I mentioned above, you can do this add-or-insert thing in a single line:

allocatorTagTotals[allocation.tag, default: 0] += allocation.size

allocatorTagTotal.totalBytes += allocation.size
allocatorTagTotals[Int(allocation.tag)] = allocatorTagTotal
} else {
allocatorTagTotals[Int(allocation.tag)] = AllocatorTagTotal(name: name, tag: Int(allocation.tag), totalBytes: allocation.size)
}

total += allocation.size
}
allocationList.append(allocation)
if let style = backtraceOptions.style {
if let stack = stacks?[allocation.ptr] {
print(backtrace(stack, style: style, process.symbolicate))
Expand All @@ -42,5 +78,27 @@ internal struct DumpRawMetadata: ParsableCommand {
}
}
}

if metadataOptions.json {
let jsonStruct: RawMetadataOutput
let allocatorTagArray = Array(allocatorTagTotals.values).sorted(by: {$0.totalBytes > $1.totalBytes})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you change the dictionary as suggested above, then you can grab the keys and values together and sort them like:

let tagSizes = allocatorTagTotals.sorted(by: { $0.1 > $1.1 })

Then you can iterate like:

for (tag, size) in tagSizes {


if metadataOptions.summary {
let summaryStruct = Summary(totalBytesAllocated: total, allocatorTags: allocatorTagArray)
jsonStruct = RawMetadataOutput(allocationList: allocationList, summary: summaryStruct)
} else {
jsonStruct = RawMetadataOutput(allocationList: allocationList, summary: nil)
}
try dumpJson(of: jsonStruct, outputFile: metadataOptions.outputFile)
} else if metadataOptions.summary {
let allocatorTagArray = Array(allocatorTagTotals.values).sorted(by: {$0.totalBytes > $1.totalBytes})

print("Metadata allocation summary:")
for tag in allocatorTagArray {
print("Tag: \(tag.tag) (\(tag.name)) Size: \(tag.totalBytes) bytes")
}

print("\nTotal bytes allocated: \(total)")
}
}
}
5 changes: 1 addition & 4 deletions tools/swift-inspect/Sources/swift-inspect/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,7 @@ internal struct BacktraceOptions: ParsableArguments {
}
}

internal struct GenericMetadataOptions: ParsableArguments {
@Flag(help: "Show allocations in mangled form")
var mangled: Bool = false

internal struct MetadataOptions: ParsableArguments {
@Flag(help: "Output JSON")
var json: Bool = false

Expand Down