mirror of
https://github.com/apple/swift-nio.git
synced 2026-05-20 20:30:36 +00:00
7ea505e5bf
Motivation: We often debug allocations on macOS, so being able to parse the output from malloc-aggregation.d is valuable. Modifications: - Add a dtrace parser to stackdiff Result: Can diff outputs from dtrace
138 lines
4.7 KiB
Swift
138 lines
4.7 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
//
|
|
// This source file is part of the SwiftNIO open source project
|
|
//
|
|
// Copyright (c) 2025 Apple Inc. and the SwiftNIO project authors
|
|
// Licensed under Apache License v2.0
|
|
//
|
|
// See LICENSE.txt for license information
|
|
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
|
|
//
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
//
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
import Foundation
|
|
|
|
/// Parses output from NIO's `malloc-aggregation.d` DTrace script.
|
|
public struct DTraceParser: StackParser {
|
|
private static var allocationsRegex: Regex<(Substring, Substring)> {
|
|
/^ +(\d+)$/
|
|
}
|
|
|
|
private var state: ParseState
|
|
private var stacks: [WeightedStack]
|
|
|
|
private init() {
|
|
self.stacks = []
|
|
self.state = .parsingHeader
|
|
}
|
|
|
|
private enum ParseResult {
|
|
case `continue`
|
|
case needsNextLine
|
|
case parsedStack(Stack, Int)
|
|
}
|
|
|
|
private enum ParseState {
|
|
case parsingHeader
|
|
case parsingStack(ParsingStackState)
|
|
}
|
|
|
|
private struct ParsingStackState {
|
|
var lines: [String]
|
|
|
|
init() {
|
|
self.lines = []
|
|
}
|
|
|
|
mutating func parse(_ line: String) -> (ParseState, ParseResult) {
|
|
if let match = line.firstMatch(of: DTraceParser.allocationsRegex) {
|
|
if let count = Int(match.1) {
|
|
return (.parsingHeader, .parsedStack(Stack(self.lines), count))
|
|
} else {
|
|
fatalError("Failed to convert '\(match.1)' to an Int")
|
|
}
|
|
} else {
|
|
let trimmed = line.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
let split = trimmed.split(separator: "+0x")
|
|
self.lines.append(split.first.map { String($0) } ?? trimmed)
|
|
return (.parsingStack(self), .needsNextLine)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Parses input which looks roughly like the following:
|
|
///
|
|
/// ```
|
|
/// =====
|
|
/// This will collect stack shots of allocations and print it when you exit dtrace.
|
|
/// So go ahead, run your tests and then press Ctrl+C in this window to see the aggregated result
|
|
/// =====
|
|
/// 20896284696
|
|
///
|
|
/// libsystem_malloc.dylib`malloc_type_posix_memalign
|
|
/// libobjc.A.dylib`id2data(objc_object*, SyncKind, usage)+0x1a0
|
|
/// libobjc.A.dylib`_objc_sync_enter_kind+0x1c
|
|
/// libobjc.A.dylib`initializeNonMetaClass+0xa8
|
|
/// ...
|
|
/// libdispatch.dylib`_dispatch_client_callout+0x10
|
|
/// libdispatch.dylib`_dispatch_once_callout+0x20
|
|
/// libswiftCore.dylib`swift_getCanonicalPrespecializedGenericMetadata+0xc8
|
|
/// libswiftCore.dylib`__swift_instantiateCanonicalPrespecializedGenericMetadata+0x28
|
|
/// libswiftCore.dylib`_ContiguousArrayBuffer .init(_uninitializedCount:minimumCapacity:)+0x3c
|
|
/// do-some-allocs`specialized static do_some_allocs.main()+0x5c
|
|
/// do-some-allocs`do_some_allocs_main+0xc
|
|
/// 1
|
|
///
|
|
/// libsystem_malloc.dylib`malloc_type_posix_memalign
|
|
/// libobjc.A.dylib`id2data(objc_object*, SyncKind, usage)+0x1a0
|
|
/// libobjc.A.dylib`_objc_sync_enter_kind+0x1c
|
|
/// ...
|
|
/// ```
|
|
private mutating func parse(lines: some Sequence<String>) -> [WeightedStack] {
|
|
for line in lines {
|
|
loop: while true {
|
|
switch self.parseNextState(line) {
|
|
case .parsedStack(let stack, let count):
|
|
self.stacks.append(WeightedStack(stack: stack, allocations: count))
|
|
break loop
|
|
|
|
case .continue:
|
|
()
|
|
|
|
case .needsNextLine:
|
|
break loop
|
|
}
|
|
}
|
|
}
|
|
|
|
return self.stacks
|
|
}
|
|
|
|
private mutating func parseNextState(_ line: String) -> ParseResult {
|
|
switch self.state {
|
|
case .parsingHeader:
|
|
if line.hasPrefix(" ") {
|
|
// Loop around and parse the line as a stack.
|
|
self.state = .parsingStack(ParsingStackState())
|
|
return .continue
|
|
} else {
|
|
return .needsNextLine
|
|
}
|
|
|
|
case .parsingStack(var state):
|
|
let (state, result) = state.parse(line)
|
|
self.state = state
|
|
return result
|
|
}
|
|
}
|
|
}
|
|
|
|
extension DTraceParser {
|
|
public static func parse(lines: some Sequence<String>) -> [WeightedStack] {
|
|
var parser = Self()
|
|
return parser.parse(lines: lines)
|
|
}
|
|
}
|