mirror of
https://github.com/apple/swift-nio.git
synced 2026-05-20 20:30:36 +00:00
e84b2ce05a
Motivation: Our alloc regression workflows are somewhat disparate: on macOS we have dtrace and a script to diff two ouputs. On Linux we have heaptrack and bpftrace but no way to analyze their outputs. Diffing output from these tools can be quite tedious if the stacks vary even a little (because the compiler decided to not inline a function, for example). Most of the tedium in this workflow is from pairing up equivalent stacks across the two inputs. We can make this easier to do by ranking suggestions based on how similar they are and letting the user decide whether to accept the match or not. Modifications: - Add a subpackage with some internals for parsing heaptrack, aggregating stacks, and measuring the similarity between them - The CLI took is currently a no-op, it'll be added in subsequent PRs Result: Easier to diagnose alloc regressions
139 lines
4.5 KiB
Swift
139 lines
4.5 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 `heaptrack analyze`.
|
|
public struct HeaptrackParser: StackParser {
|
|
private let regex = /^(\d+) calls with/
|
|
private var state: ParseState
|
|
private var stacks: [WeightedStack]
|
|
|
|
private init() {
|
|
self.stacks = []
|
|
self.state = .parsingHeader
|
|
}
|
|
|
|
private enum ParseResult {
|
|
case needsNextLine
|
|
case parsedStack(Stack, Int)
|
|
}
|
|
|
|
private enum ParseState {
|
|
case parsingHeader
|
|
case parsingStack(ParsingStackState)
|
|
}
|
|
|
|
private struct ParsingStackState {
|
|
var count: Int
|
|
var lines: [String]
|
|
|
|
init(count: Int) {
|
|
self.count = count
|
|
self.lines = []
|
|
}
|
|
|
|
private static var prefixesToIgnore: [String] {
|
|
[" in", " at", " 0x"]
|
|
}
|
|
|
|
mutating func parse(_ line: String) -> (ParseState, ParseResult) {
|
|
if Self.prefixesToIgnore.contains(where: { line.contains($0) }) {
|
|
return (.parsingStack(self), .needsNextLine)
|
|
} else if line.starts(with: " ") {
|
|
self.lines.append(line.trimmingCharacters(in: .whitespacesAndNewlines))
|
|
return (.parsingStack(self), .needsNextLine)
|
|
} else {
|
|
return (.parsingHeader, .parsedStack(Stack(self.lines), self.count))
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Parses input which looks roughly like the following:
|
|
///
|
|
/// ```
|
|
/// reading file "heaptrack.simple-handshake.b.gz" - please wait, this might take some time...
|
|
/// Debuggee command was: ./.build/release/simple-handshake
|
|
/// finished reading file, now analyzing data:
|
|
///
|
|
/// MOST CALLS TO ALLOCATION FUNCTIONS
|
|
/// 488867 calls to allocation functions with 28.16K peak consumption from
|
|
/// CNIOBoringSSL_OPENSSL_malloc
|
|
/// at Sources/CNIOBoringSSL/crypto/mem.cc:249
|
|
/// in /code/.build/aarch64-unknown-linux-gnu/release/simple-handshake
|
|
/// 3000 calls with 80B peak consumption from:
|
|
/// CNIOBoringSSL_OPENSSL_zalloc
|
|
/// at Sources/CNIOBoringSSL/crypto/mem.cc:266
|
|
/// in /code/.build/aarch64-unknown-linux-gnu/release/simple-handshake
|
|
/// CNIOBoringSSL_ASN1_item_ex_new
|
|
/// at Sources/CNIOBoringSSL/crypto/asn1/tasn_new.cc:153
|
|
/// ...
|
|
/// __libc_start_main
|
|
/// in /lib/aarch64-linux-gnu/libc.so.6
|
|
/// 3000 calls with 96B peak consumption from:
|
|
/// CNIOBoringSSL_ASN1_STRING_type_new
|
|
/// at Sources/CNIOBoringSSL/crypto/asn1/asn1_lib.cc:329
|
|
/// in /code/.build/aarch64-unknown-linux-gnu/release/simple-handshake
|
|
/// ASN1_primitive_new(ASN1_VALUE_st**, ASN1_ITEM_st const*)
|
|
/// ...
|
|
/// ```
|
|
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))
|
|
|
|
case .needsNextLine:
|
|
break loop
|
|
}
|
|
}
|
|
}
|
|
|
|
return self.stacks
|
|
}
|
|
|
|
private mutating func parseNextState(_ line: String) -> ParseResult {
|
|
switch self.state {
|
|
case .parsingHeader:
|
|
return self.parseHeader(line)
|
|
|
|
case .parsingStack(var state):
|
|
let (state, result) = state.parse(line)
|
|
self.state = state
|
|
return result
|
|
}
|
|
}
|
|
|
|
private mutating func parseHeader(_ line: String) -> ParseResult {
|
|
if let match = line.firstMatch(of: self.regex) {
|
|
// At the start of a stack.
|
|
guard let count = Int(match.1) else {
|
|
fatalError("Failed to convert '\(match.1)' to an Int")
|
|
}
|
|
|
|
self.state = .parsingStack(ParsingStackState(count: count))
|
|
}
|
|
|
|
return .needsNextLine
|
|
}
|
|
}
|
|
|
|
extension HeaptrackParser {
|
|
public static func parse(lines: some Sequence<String>) -> [WeightedStack] {
|
|
var parser = Self()
|
|
return parser.parse(lines: lines)
|
|
}
|
|
}
|