creaturesurvive/postmortem
Crash, hang and performance reports from MetricKit, readable on the device, with no third-party
Why
MetricKit collects crashes, hangs, CPU and disk-write exceptions, slow launches and daily performance metrics on real users' devices, for free. Almost nobody uses it directly, because:
- It delivers raw JSON: stack trees of binary UUIDs and offsets, measurements as strings like
"2500 ms", and Mach exception numbers.
- Each payload is delivered once. If you don't store it, it's gone.
- Nothing symbolicates or explains it.
exceptionType: 6, signal: 5means "a Swift runtime
trap", but you have to know that.
The new typed MetricKit API (MetricManager, CallStackTree) requires iOS 27 or macOS 27. Postmortem works back to iOS 17 and adds the storage, symbolication and UI that MetricKit doesn't provide on any version.
Features
PostmortemCollector: subscribes to MetricKit and importspastDiagnosticPayloadsand
pastPayloads, so reports delivered before you adopted it aren't lost. It has an onReceive hook for uploading.
ReportStore: keeps the raw JSON, so nothing is lost if parsing improves later. It
deduplicates identical payloads and diagnostics re-delivered in later payloads, trims by age and count, and publishes changes.
- Typed models:
- DiagnosticPayload and Diagnostic for crashes, hangs, CPU exceptions, disk write exceptions and slow launches, with all metadata. Unknown keys are kept in extra. - MetricPayload for CPU, memory, launch, resume and hang histograms (with percentile estimates), hitch ratios, disk, network, and exit reasons.
CrashExplanation: turns exception types, signals, termination reasons and Objective-C
exceptions into a title and a plain-language summary. It also knows common termination codes: 0x8badf00d watchdog, 0xdead10cc lock held in the background, thermal kills, and others.
Symbolicator: resolves frames on device when the binary is loaded: the same app build,
or system libraries on the same OS version. - System libraries resolve through dladdr. - Your own binaries resolve through their in-memory symbol table, so Debug and unstripped builds show real function names, including private Swift functions. - Swift names are demangled.
DiagnosticReport:
- text(for:) produces a shareable report in the style of Apple's crash logs. - atosScript(for:) produces a shell script that symbolicates stripped Release builds with your dSYMs, checking each dSYM's UUID first.
- SwiftUI:
- PostmortemView lists everything grouped by kind. - DiagnosticDetailView shows the summary, metadata and symbolicated threads, with sharing. - MetricDetailView shows p50 and p95 launch, resume and hang times, resource use and exit reasons.
- Robust parsing: Foundation's JSON parser gives up at 512 levels of nesting. MetricKit
nests every frame two levels inside its caller, so a crash of more than about 250 frames (for example, runaway recursion) wouldn't parse at all. Postmortem parses and builds stack trees without recursion, and deep trees are compared, hashed and freed with loops, so they're safe on small background-thread stacks. Malformed input is fuzzed in the tests.
Details
let diagnostics = try await ReportStore.shared.diagnostics()
for crash in diagnostics where crash.kind == .crash {
print(crash.title) // "EXC_BAD_ACCESS (SIGSEGV)"
print(crash.summary) // "The app accessed memory it doesn't own, …"
let frames = Symbolicator().symbolicate(crash.primaryFrames)
print(frames.map(\.description).joined(separator: "\n"))
}
let metrics = try await ReportStore.shared.metrics()
if let launch = metrics.first?.launchTime {
print("p95 launch:", launch.percentile(0.95) ?? 0, "s")
}Upload payloads as they arrive:
PostmortemCollector.shared.onReceive = { kind, json in
Task { try await uploader.send(json, kind: kind.rawValue) }
}Installation
Add Postmortem to your Package.swift:
dependencies: [
.package(url: "https://github.com/CreatureSurvive/Postmortem.git", from: "1.0.0"),
],
targets: [
.target(name: "MyApp", dependencies: ["Postmortem"]),
]Or in Xcode, choose File › Add Package Dependencies… and enter https://github.com/CreatureSurvive/Postmortem.
Requirements
| Platform | Minimum | | --- | --- | | iOS | 17.0 | | macOS | 14.0 | | tvOS | 17.0 | | watchOS | 10.0 | | visionOS | 1.0 |
Swift 6.1 (Xcode 16.4) or later, in Swift 6 language mode. No third-party dependencies.
Collecting reports needs MetricKit, so it runs on iOS, macOS and visionOS. The models and store also build on tvOS and watchOS, and the viewers on tvOS, so a tvOS app can show reports gathered elsewhere and a companion app can parse uploaded payloads.
Testing
swift test runs 35 tests. They cover:
- Fixtures: in
Tests/PostmortemTests/Fixtures, generated by MetricKit itself. Scratch
tools in Tools/ build real MXDiagnosticPayload and MXMetricPayload objects through MetricKit's own initializers and let Apple's code serialize them, so the parser is tested against Apple's actual JSON layout.
- Crash explanations and measurement parsing, with and without grouping separators.
- Histogram percentiles.
- The JSON parser: equivalence with Foundation, invalid input, 1,000-frame stacks, and
fuzzing.
- Deep stacks on small threads: a 4,096-frame crash parsed, compared, hashed and freed on a
thread with a 256 KB stack.
- Live symbolication: an exported C function, a local Swift closure found through the symbol
table, and a shared-cache system function, all resolved in the test process.
- The store: deduplication, retention and change notifications.
Example/ is an iOS app with a UI test. It loads the fixtures plus a live crash whose frames point into the running app and UIKit, and checks that the viewer lists every kind, resolves demoLoadUserProfile and UIApplicationMain on device, and shows metrics.
cd Example && xcodegen generate
xcodebuild test -project PostmortemDemo.xcodeproj -scheme PostmortemDemo \
-destination "platform=iOS Simulator,name=iPhone 16"The README screenshots are captured by UI tests in Example/; Scripts/screenshots.sh regenerates them.
Limitations
- MetricKit delivers reports roughly once a day, and only on physical devices. In Xcode, use
Debug > Simulate MetricKit Payloads on a connected device to test delivery.
- Symbolicating Release app code: App Store and TestFlight builds are stripped, so their
frames resolve only to exported symbols on device. Use the atos script with your dSYMs.
- System frames from other OS versions: frames from a different OS version than the one
running can't be resolved on device.
- Event times: MetricKit reports only the period a diagnostic was delivered in, not the
exact time of the event.
Changelog
See CHANGELOG.md. Releases follow Semantic Versioning.
Contributing
Issues and pull requests are welcome. Please run swift test before opening a pull request, and add tests for new behavior.
License
Available under the MIT license. See LICENSE for details.
Package Metadata
Repository: creaturesurvive/postmortem
Default branch: main
README: README.md