From 2186562ee1a197b4033e96b20d12858ca205fab1 Mon Sep 17 00:00:00 2001 From: Noah Martin Date: Tue, 25 Nov 2025 13:05:11 -0500 Subject: [PATCH 1/6] fix: Use correct parsing for stackframes --- Sentry.xcodeproj/project.pbxproj | 8 + Sources/Sentry/SentryFormatterSwift.m | 7 + Sources/Sentry/SentryMetricKitIntegration.m | 278 +----------------- Sources/Sentry/include/SentryFormatterSwift.h | 1 + Sources/Sentry/include/SentryPrivate.h | 2 + .../MetricKit/SentryMXCallStackTree.swift | 129 ++++++-- 6 files changed, 131 insertions(+), 294 deletions(-) create mode 100644 Sources/Sentry/SentryFormatterSwift.m create mode 100644 Sources/Sentry/include/SentryFormatterSwift.h diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index 0daf95a05e..9a3dd5b872 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -1094,6 +1094,8 @@ FA914E592ECF968500C54BDD /* UserFeedbackIntegration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA914E532ECF968000C54BDD /* UserFeedbackIntegration.swift */; }; FA914E5B2ECF988900C54BDD /* Integrations.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA914E5A2ECF988700C54BDD /* Integrations.swift */; }; FA914E6D2ECFD7D800C54BDD /* SentryFeedbackAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA914E6C2ECFD7D800C54BDD /* SentryFeedbackAPI.swift */; }; + FA914E9B2ED61AA800C54BDD /* SentryFormatterSwift.h in Headers */ = {isa = PBXBuildFile; fileRef = FA914E952ED61AA300C54BDD /* SentryFormatterSwift.h */; }; + FA914E9E2ED61BA800C54BDD /* SentryFormatterSwift.m in Sources */ = {isa = PBXBuildFile; fileRef = FA914E9C2ED61AB900C54BDD /* SentryFormatterSwift.m */; }; FA94E6912E6B92C100576666 /* SentryClientReport.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA94E68B2E6B92BE00576666 /* SentryClientReport.swift */; }; FA94E6B22E6D265800576666 /* SentryEnvelope.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA94E6B12E6D265500576666 /* SentryEnvelope.swift */; }; FA94E7242E6F339400576666 /* SentryEnvelopeItemType.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA94E7232E6F32FA00576666 /* SentryEnvelopeItemType.swift */; }; @@ -2474,6 +2476,8 @@ FA914E532ECF968000C54BDD /* UserFeedbackIntegration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserFeedbackIntegration.swift; sourceTree = ""; }; FA914E5A2ECF988700C54BDD /* Integrations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Integrations.swift; sourceTree = ""; }; FA914E6C2ECFD7D800C54BDD /* SentryFeedbackAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryFeedbackAPI.swift; sourceTree = ""; }; + FA914E952ED61AA300C54BDD /* SentryFormatterSwift.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryFormatterSwift.h; path = include/SentryFormatterSwift.h; sourceTree = ""; }; + FA914E9C2ED61AB900C54BDD /* SentryFormatterSwift.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryFormatterSwift.m; sourceTree = ""; }; FA94E68B2E6B92BE00576666 /* SentryClientReport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryClientReport.swift; sourceTree = ""; }; FA94E6B12E6D265500576666 /* SentryEnvelope.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryEnvelope.swift; sourceTree = ""; }; FA94E7232E6F32FA00576666 /* SentryEnvelopeItemType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryEnvelopeItemType.swift; sourceTree = ""; }; @@ -2945,6 +2949,8 @@ 639889D51EDF10BE00EA7442 /* Helper */ = { isa = PBXGroup; children = ( + FA914E952ED61AA300C54BDD /* SentryFormatterSwift.h */, + FA914E9C2ED61AB900C54BDD /* SentryFormatterSwift.m */, FA4FB8252ECB7D27008C9EC3 /* SentryLevel.h */, 63AA76951EB9C1C200D153DE /* SentryDefines.h */, 627E7588299F6FE40085504D /* SentryInternalDefines.h */, @@ -5231,6 +5237,7 @@ FAB359982E05D7E90083D5E3 /* SentryEventSwiftHelper.h in Headers */, 7B31C291277B04A000337126 /* SentryCrashPlatformSpecificDefines.h in Headers */, D452FC732DDB553100AFF56F /* SentryWatchdogTerminationBreadcrumbProcessor.h in Headers */, + FA914E9B2ED61AA800C54BDD /* SentryFormatterSwift.h in Headers */, D456B4382D706BFE007068CB /* SentrySpanDataKey.h in Headers */, 7B77BE3527EC8445003C9020 /* SentryDiscardReasonMapper.h in Headers */, 7B610D602512390E00B0B5D9 /* SentrySDK+Private.h in Headers */, @@ -5822,6 +5829,7 @@ D48891CC2E98F22A00212823 /* SentryInfoPlistWrapperProvider.swift in Sources */, F458D1172E186DF20028273E /* SentryScopePersistentStore+Fingerprint.swift in Sources */, D8CB7417294724CC00A5F964 /* SentryEnvelopeAttachmentHeader.m in Sources */, + FA914E9E2ED61BA800C54BDD /* SentryFormatterSwift.m in Sources */, D84793262788737D00BE8E99 /* SentryByteCountFormatter.m in Sources */, FAEFA12F2E4FAE1900C431D9 /* SentrySDKSettings.swift in Sources */, 63AA769E1EB9C57A00D153DE /* SentryError.mm in Sources */, diff --git a/Sources/Sentry/SentryFormatterSwift.m b/Sources/Sentry/SentryFormatterSwift.m new file mode 100644 index 0000000000..d61daa22c9 --- /dev/null +++ b/Sources/Sentry/SentryFormatterSwift.m @@ -0,0 +1,7 @@ +#import "SentryFormatter.h" + +NSString * +sentry_formatHexAddressUInt64Swift(uint64_t value) +{ + return sentry_formatHexAddressUInt64(value); +} diff --git a/Sources/Sentry/SentryMetricKitIntegration.m b/Sources/Sentry/SentryMetricKitIntegration.m index 3c5160ba1e..64d80d8ebe 100644 --- a/Sources/Sentry/SentryMetricKitIntegration.m +++ b/Sources/Sentry/SentryMetricKitIntegration.m @@ -204,187 +204,12 @@ - (void)didReceiveHangDiagnostic:(MXHangDiagnostic *)diagnostic - (void)captureMXEvent:(SentryMXCallStackTree *)callStackTree params:(SentryMXExceptionParams *)params diagnosticJSON:(NSData *)diagnosticJSON -{ - // When receiving MXCrashDiagnostic the callStackPerThread was always true. In that case, the - // MXCallStacks of the MXCallStackTree were individual threads, all belonging to the process - // when the crash occurred. For MXCPUException, the callStackPerThread was always false. In that - // case, the MXCallStacks stem from CPU-hungry multiple locations in the sample app during an - // observation time of 90 seconds of one app run. It's a collection of stack traces that are - // CPU-hungry. - if (callStackTree.callStackPerThread) { - SentryEvent *event = [self createEvent:params]; - - event.threads = [self convertToSentryThreads:callStackTree]; - - SentryThread *crashedThread = event.threads[0]; - crashedThread.crashed = @(!params.handled); - - SentryException *exception = event.exceptions[0]; - exception.stacktrace = crashedThread.stacktrace; - exception.threadId = crashedThread.threadId; - - event.debugMeta = [self extractDebugMetaFromMXCallStacks:callStackTree.callStacks]; - - // The crash event can be way from the past. We don't want to impact the current session. - // Therefore we don't call captureFatalEvent. - [self captureEvent:event withDiagnosticJSON:diagnosticJSON]; - } else { - for (SentryMXCallStack *callStack in callStackTree.callStacks) { - [self buildAndCaptureMXEventFor:callStack.callStackRootFrames - params:params - diagnosticJSON:diagnosticJSON]; - } - } -} - -/** - * If @c callStackPerThread is @c NO , MetricKit organizes the stacktraces in a tree structure. See - * https://developer.apple.com/videos/play/wwdc2020/10078/?time=224. The stacktrace consists of the - * last sibling leaf frame plus its ancestors. - * - * The algorithm adds all frames to a list until it finds a leaf frame being the last sibling. Then - * it reports that frame with its siblings and ancestors as a stacktrace. - * - * In the following example, the algorithm starts with frame 0, continues until frame 6, and reports - * a stacktrace. Then it pops all sibling frames, goes back up to frame 3, and continues the search. - * - * It is worth noting that for the first stacktrace [0, 1, 3, 4, 5, 6] frame 2 is not included - * because the logic only includes direct siblings and direct ancestors. Frame 3 is an ancestors of - * [4,5,6], frame 1 of frame 3, but frame 2 is not a direct ancestors of [4,5,6]. It's the sibling - * of the direct ancestor frame 3. Although this might seem a bit illogical, that is what - * observations of MetricKit data unveiled. - * - * @code - * | frame 0 | - * | frame 1 | - * | frame 2 | - * | frame 3 | - * | frame 4 | - * | frame 5 | - * | frame 6 | -> stack trace consists of [0, 1, 3, 4, 5, 6] - * | frame 7 | - * | frame 8 | -> stack trace consists of [0, 1, 2, 3, 7, 8] - * | frame 9 | -> stack trace consists of [0, 1, 9] - * | frame 10 | - * | frame 11 | - * | frame 12 | - * | frame 13 | -> stack trace consists of [10, 11, 12, 13] - * @endcode - * - * The above stacktrace turns into the following two trees. - * @code - * 0 - * | - * 1 - * / \ \ - * 3 2 9 - * | | - * 4 3 - * | | - * 5 7 - * | | - * 6 8 - * - * 10 - * | - * 11 - * | - * 12 - * | - * 13 - * @endcode - */ -- (void)buildAndCaptureMXEventFor:(NSArray *)rootFrames - params:(SentryMXExceptionParams *)params - diagnosticJSON:(NSData *)diagnosticJSON -{ - for (SentryMXFrame *rootFrame in rootFrames) { - NSMutableArray *stackTraceFrames = [NSMutableArray array]; - NSMutableSet *processedFrameAddresses = [NSMutableSet set]; - NSMutableDictionary *addressesToParentFrames = - [NSMutableDictionary dictionary]; - - SentryMXFrame *currentFrame = rootFrame; - [stackTraceFrames addObject:currentFrame]; - - while (stackTraceFrames.count > 0) { - currentFrame = [stackTraceFrames lastObject]; - [processedFrameAddresses addObject:@(currentFrame.address)]; - - for (SentryMXFrame *subFrame in currentFrame.subFrames) { - addressesToParentFrames[@(subFrame.address)] = currentFrame; - } - SentryMXFrame *parentFrame = addressesToParentFrames[@(currentFrame.address)]; - - SentryMXFrame *firstUnprocessedSibling = - [self getFirstUnprocessedSubFrames:parentFrame.subFrames ?: @[] - processedFrameAddresses:processedFrameAddresses]; - - BOOL lastUnprocessedSibling = firstUnprocessedSibling == nil; - BOOL noChildren = currentFrame.subFrames.count == 0; - - if (noChildren && lastUnprocessedSibling) { - [self captureEventNotPerThread:stackTraceFrames - params:params - diagnosticJSON:diagnosticJSON]; - - if (parentFrame == nil) { - // No parent frames - [stackTraceFrames removeLastObject]; - } else { - // Pop all sibling frames - for (int i = 0; i < parentFrame.subFrames.count; i++) { - [stackTraceFrames removeLastObject]; - } - } - } else { - SentryMXFrame *nonProcessedSubFrame = - [self getFirstUnprocessedSubFrames:currentFrame.subFrames ?: @[] - processedFrameAddresses:processedFrameAddresses]; - - // Keep adding sub frames - if (nonProcessedSubFrame != nil) { - [stackTraceFrames addObject:nonProcessedSubFrame]; - } // Keep adding sibling frames - else if (firstUnprocessedSibling != nil) { - [stackTraceFrames addObject:firstUnprocessedSibling]; - } // Keep popping - else { - [stackTraceFrames removeLastObject]; - } - } - } - } -} - -- (nullable SentryMXFrame *)getFirstUnprocessedSubFrames:(NSArray *)subFrames - processedFrameAddresses: - (NSSet *)processedFrameAddresses -{ - return [subFrames filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL( - SentryMXFrame *frame, - NSDictionary *bindings) { - return ![processedFrameAddresses containsObject:@(frame.address)]; - }]].firstObject; -} - -- (void)captureEventNotPerThread:(NSArray *)frames - params:(SentryMXExceptionParams *)params - diagnosticJSON:(NSData *)diagnosticJSON { SentryEvent *event = [self createEvent:params]; + [callStackTree prepareWithEvent:event inAppLogic:self.inAppLogic handled:params.handled]; - SentryThread *thread = [[SentryThread alloc] initWithThreadId:@0]; - thread.crashed = @(!params.handled); - thread.stacktrace = [self convertMXFramesToSentryStacktrace:frames.objectEnumerator]; - - SentryException *exception = event.exceptions[0]; - exception.stacktrace = thread.stacktrace; - exception.threadId = thread.threadId; - - event.threads = @[ thread ]; - event.debugMeta = [self extractDebugMetaFromMXFrames:frames]; - + // The crash event can be way from the past. We don't want to impact the current session. + // Therefore we don't call captureFatalEvent. [self captureEvent:event withDiagnosticJSON:diagnosticJSON]; } @@ -419,103 +244,6 @@ - (void)captureEvent:(SentryEvent *)event withDiagnosticJSON:(NSData *)diagnosti } } -- (NSArray *)convertToSentryThreads:(SentryMXCallStackTree *)callStackTree -{ - NSUInteger i = 0; - NSMutableArray *threads = [NSMutableArray array]; - for (SentryMXCallStack *callStack in callStackTree.callStacks) { - NSEnumerator *frameEnumerator - = callStack.flattenedRootFrames.objectEnumerator; - // The MXFrames are in reversed order when callStackPerThread is true. The Apple docs don't - // state that. This is an assumption based on observing MetricKit data. - if (callStackTree.callStackPerThread) { - frameEnumerator = [callStack.flattenedRootFrames reverseObjectEnumerator]; - } - - SentryStacktrace *stacktrace = [self convertMXFramesToSentryStacktrace:frameEnumerator]; - - SentryThread *thread = [[SentryThread alloc] initWithThreadId:@(i)]; - thread.stacktrace = stacktrace; - - [threads addObject:thread]; - - i++; - } - - return threads; -} - -- (SentryStacktrace *)convertMXFramesToSentryStacktrace:(NSEnumerator *)mxFrames -{ - NSMutableArray *frames = [NSMutableArray array]; - - for (SentryMXFrame *mxFrame in mxFrames) { - SentryFrame *frame = [[SentryFrame alloc] init]; - frame.package = mxFrame.binaryName; - frame.inApp = @([self.inAppLogic isInApp:mxFrame.binaryName]); - frame.instructionAddress = sentry_formatHexAddressUInt64(mxFrame.address); - uint64_t imageAddress = mxFrame.address - mxFrame.offsetIntoBinaryTextSegment; - frame.imageAddress = sentry_formatHexAddressUInt64(imageAddress); - - [frames addObject:frame]; - } - - SentryStacktrace *stacktrace = [[SentryStacktrace alloc] initWithFrames:frames registers:@{}]; - - return stacktrace; -} - -/** - * We must extract the debug images from the MetricKit stacktraces as the image addresses change - * when you restart the app. - */ -- (NSArray *)extractDebugMetaFromMXCallStacks: - (NSArray *)callStacks -{ - NSMutableDictionary *debugMetas = - [NSMutableDictionary dictionary]; - for (SentryMXCallStack *callStack in callStacks) { - - NSArray *callStackDebugMetas = - [self extractDebugMetaFromMXFrames:callStack.flattenedRootFrames]; - - for (SentryDebugMeta *debugMeta in callStackDebugMetas) { - if (debugMeta.debugID != nil) { - debugMetas[SENTRY_UNWRAP_NULLABLE_VALUE(id, debugMeta.debugID)] - = debugMeta; - } - } - } - - return [debugMetas allValues]; -} - -- (NSArray *)extractDebugMetaFromMXFrames:(NSArray *)mxFrames -{ - NSMutableDictionary *debugMetas = - [NSMutableDictionary dictionary]; - - for (SentryMXFrame *mxFrame in mxFrames) { - - NSString *binaryUUID = [mxFrame.binaryUUID UUIDString]; - if (debugMetas[binaryUUID]) { - continue; - } - - SentryDebugMeta *debugMeta = [[SentryDebugMeta alloc] init]; - debugMeta.type = SentryDebugImageType; - debugMeta.debugID = binaryUUID; - debugMeta.codeFile = mxFrame.binaryName; - - uint64_t imageAddress = mxFrame.address - mxFrame.offsetIntoBinaryTextSegment; - debugMeta.imageAddress = sentry_formatHexAddressUInt64(imageAddress); - - debugMetas[binaryUUID] = debugMeta; - } - - return [debugMetas allValues]; -} - @end NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/include/SentryFormatterSwift.h b/Sources/Sentry/include/SentryFormatterSwift.h new file mode 100644 index 0000000000..2272138b2e --- /dev/null +++ b/Sources/Sentry/include/SentryFormatterSwift.h @@ -0,0 +1 @@ +extern NSString *sentry_formatHexAddressUInt64Swift(uint64_t value); diff --git a/Sources/Sentry/include/SentryPrivate.h b/Sources/Sentry/include/SentryPrivate.h index bc0f40f588..43f9ca3623 100644 --- a/Sources/Sentry/include/SentryPrivate.h +++ b/Sources/Sentry/include/SentryPrivate.h @@ -22,8 +22,10 @@ #import "SentryDsn+Private.h" #import "SentryEnvelopeAttachmentHeader.h" #import "SentryEventSwiftHelper.h" +#import "SentryFormatterSwift.h" #import "SentryHub+Private.h" #import "SentryIntegrationProtocol.h" +#import "SentryInternalDefines.h" #import "SentryNSDataUtils.h" #import "SentrySDK+Private.h" #import "SentryTime.h" diff --git a/Sources/Swift/Core/MetricKit/SentryMXCallStackTree.swift b/Sources/Swift/Core/MetricKit/SentryMXCallStackTree.swift index 91fe984c8a..9cede69d84 100644 --- a/Sources/Swift/Core/MetricKit/SentryMXCallStackTree.swift +++ b/Sources/Swift/Core/MetricKit/SentryMXCallStackTree.swift @@ -1,3 +1,4 @@ +@_implementationOnly import _SentryPrivate import Foundation #if os(iOS) || os(macOS) @@ -7,39 +8,129 @@ import Foundation @objcMembers @_spi(Private) public class SentryMXCallStackTree: NSObject, Decodable { - public let callStacks: [SentryMXCallStack] + let callStacks: [SentryMXCallStack] public let callStackPerThread: Bool static func from(data: Data) throws -> SentryMXCallStackTree { return try JSONDecoder().decode(SentryMXCallStackTree.self, from: data) } + + func toDebugMeta() -> [DebugMeta] { + callStacks.flatMap { frame in + frame.toDebugMeta() + }.unique { $0.debugID } + } + + public func prepare(event: Event, inAppLogic: SentryInAppLogic, handled: Bool) { + let debugMeta = toDebugMeta() + let threads = sentryMXBacktrace(inAppLogic: inAppLogic, handled: handled) + let crashedThread = threads.first { $0.crashed?.boolValue == true } + event.debugMeta = debugMeta + event.threads = threads + + if let crashedThread, let exception = event.exceptions?[0] { + exception.stacktrace = crashedThread.stacktrace + exception.threadId = crashedThread.threadId + } + } + + // A MetricKit CallStackTree is a flamegraph, but many Sentry APIs only support + // a thread backtrace. A flamegraph is just a collection of many thread backtraces + // generated by taking multiple samples. For example a hang from metric kit will + // be a group of samples of the main thread while it is hanging. To make MetricKit + // data compatible with Sentry data this function find the most commonly sampled stack. + // Some metric kit events contain multiple threads (like a crash report) and others + // contain all threads in one flamegraph. That happens when "callStackPerThread" + // is false. In these cases we can't really make a "thread" because + // it represents data that is sampled across many threads and aggregated, we do not + // know which samples came from which thread. Instead we create just one fake thread + // that just contains the most common callstack. + func sentryMXBacktrace(inAppLogic: SentryInAppLogic, handled: Bool) -> [SentryThread] { + callStacks.map { callStack in + let thread = SentryThread(threadId: 0) + let frames = callStack.toFrames() + frames.forEach { $0.inApp = NSNumber(value: inAppLogic.is(inApp: $0.package)) } + thread.stacktrace = SentryStacktrace(frames: frames, registers: [:]) + thread.crashed = NSNumber(value: (callStack.threadAttributed ?? false) && !handled) + return thread + } + } } -@objcMembers -@_spi(Private) public class SentryMXCallStack: NSObject, Decodable { - public let threadAttributed: Bool? - public let callStackRootFrames: [SentryMXFrame] +struct SentryMXCallStack: Codable { + let threadAttributed: Bool? + let callStackRootFrames: [SentryMXFrame] + + func toFrames() -> [Frame] { + // The root node of a flamegraph is the first frame in a stacktrace (usually main) + callStackRootFrames.mostSampled()?.toFrames().reversed() ?? [] + } - public var flattenedRootFrames: [SentryMXFrame] { - return callStackRootFrames.flatMap { [$0] + $0.frames } + func toDebugMeta() -> [DebugMeta] { + callStackRootFrames.flatMap { frame in + frame.toDebugMeta() + } } } -@objcMembers -@_spi(Private) public class SentryMXFrame: NSObject, Decodable { - public let binaryUUID: UUID - public let offsetIntoBinaryTextSegment: Int - public let binaryName: String? - public let address: UInt64 - public let subFrames: [SentryMXFrame]? - public let sampleCount: Int? +struct SentryMXFrame: Codable { + let binaryUUID: UUID + let offsetIntoBinaryTextSegment: Int + let binaryName: String? + let address: UInt64 + let subFrames: [SentryMXFrame]? + let sampleCount: Int? - var frames: [SentryMXFrame] { - return (subFrames?.flatMap { [$0] + $0.frames } ?? []) + func toSentryFrame() -> Frame { + let frame = Frame() + frame.package = binaryName + frame.instructionAddress = sentry_formatHexAddressUInt64Swift(address) + frame.imageAddress = sentry_formatHexAddressUInt64Swift(address - UInt64(offsetIntoBinaryTextSegment)) + return frame + } + + func toFrames() -> [Frame] { + return [toSentryFrame()] + (subFrames?.mostSampled()?.toFrames() ?? []) } - var framesIncludingSelf: [SentryMXFrame] { - return [self] + frames + func toDebugMeta() -> [DebugMeta] { + let result = DebugMeta() + result.type = SentryDebugImageType + result.debugID = binaryUUID.uuidString + result.codeFile = binaryName + result.imageAddress = sentry_formatHexAddressUInt64Swift(address - UInt64(offsetIntoBinaryTextSegment)) + return [result] + (subFrames?.flatMap { $0.toDebugMeta() } ?? []) + } +} + +extension Sequence { + func unique(by key: (Element) -> T) -> [Element] { + var seen = Set() + var result: [Element] = [] + for element in self { + let k = key(element) + if !seen.contains(k) { + seen.insert(k) + result.append(element) + } + } + + return result + } +} + +extension Sequence where Element == SentryMXFrame { + // A sentry frame is a list not a tree, find the most frequently sampled element at this level of the tree. + func mostSampled() -> SentryMXFrame? { + var mostSamples = -1 + var mostSampledFrame: SentryMXFrame? + for frame in self { + if frame.sampleCount ?? 0 > mostSamples { + mostSamples = frame.sampleCount ?? 0 + mostSampledFrame = frame + } + } + return mostSampledFrame } } From e860e926b7b80ab4ae8e2cc531e5b27a543f54c3 Mon Sep 17 00:00:00 2001 From: Noah Martin Date: Mon, 1 Dec 2025 15:59:45 -0500 Subject: [PATCH 2/6] Simplify --- Sources/Sentry/SentryMetricKitIntegration.m | 2 +- .../MetricKit/SentryMXCallStackTree.swift | 89 ++++++++++++------- .../SentryMXCallStackTreeTests.swift | 15 ++-- .../SentryMetricKitIntegrationTests.swift | 52 +++++------ 4 files changed, 90 insertions(+), 68 deletions(-) diff --git a/Sources/Sentry/SentryMetricKitIntegration.m b/Sources/Sentry/SentryMetricKitIntegration.m index 64d80d8ebe..3321d7a792 100644 --- a/Sources/Sentry/SentryMetricKitIntegration.m +++ b/Sources/Sentry/SentryMetricKitIntegration.m @@ -48,7 +48,7 @@ @interface SentryMetricKitIntegration () @property (nonatomic, strong, nullable) SentryMXManager *metricKitManager; @property (nonatomic, strong) NSMeasurementFormatter *measurementFormatter; -@property (nonatomic, strong) SentryInAppLogic *inAppLogic; +@property (nonatomic, strong, nullable) SentryInAppLogic *inAppLogic; @property (nonatomic, assign) BOOL attachDiagnosticAsAttachment; @end diff --git a/Sources/Swift/Core/MetricKit/SentryMXCallStackTree.swift b/Sources/Swift/Core/MetricKit/SentryMXCallStackTree.swift index 9cede69d84..58197f6a37 100644 --- a/Sources/Swift/Core/MetricKit/SentryMXCallStackTree.swift +++ b/Sources/Swift/Core/MetricKit/SentryMXCallStackTree.swift @@ -21,7 +21,7 @@ import Foundation }.unique { $0.debugID } } - public func prepare(event: Event, inAppLogic: SentryInAppLogic, handled: Bool) { + public func prepare(event: Event, inAppLogic: SentryInAppLogic?, handled: Bool) { let debugMeta = toDebugMeta() let threads = sentryMXBacktrace(inAppLogic: inAppLogic, handled: handled) let crashedThread = threads.first { $0.crashed?.boolValue == true } @@ -45,11 +45,19 @@ import Foundation // it represents data that is sampled across many threads and aggregated, we do not // know which samples came from which thread. Instead we create just one fake thread // that just contains the most common callstack. - func sentryMXBacktrace(inAppLogic: SentryInAppLogic, handled: Bool) -> [SentryThread] { + func sentryMXBacktrace(inAppLogic: SentryInAppLogic?, handled: Bool) -> [SentryThread] { callStacks.map { callStack in let thread = SentryThread(threadId: 0) - let frames = callStack.toFrames() - frames.forEach { $0.inApp = NSNumber(value: inAppLogic.is(inApp: $0.package)) } + let samples = callStack.callStackRootFrames.flatMap { $0.toSamples() } + // Group by stacktrace in case there are multiple samples with the same trace + var samplesToCount = [[Sample.Frame]: Int]() + for sample in samples { + let count = samplesToCount[sample.frames] ?? 0 + samplesToCount[sample.frames] = sample.count + count + } + // Need to reverse because the root node of a flamegraph is the first frame in a stacktrace (usually main) + let frames = samplesToCount.mostSampled()?.reversed().map { $0.toSentryFrame() } ?? [] + frames.forEach { $0.inApp = NSNumber(value: inAppLogic?.is(inApp: $0.package) ?? false) } thread.stacktrace = SentryStacktrace(frames: frames, registers: [:]) thread.crashed = NSNumber(value: (callStack.threadAttributed ?? false) && !handled) return thread @@ -57,14 +65,31 @@ import Foundation } } -struct SentryMXCallStack: Codable { +// A Sample is the standard data format for a flamegraph taken from https://github.com/brendangregg/FlameGraph +// It is less compact than Apple's MetricKit format, but contains the same data and is easier to work with +struct Sample { + let count: Int + let frames: [Frame] + + struct Frame: Hashable { + let binaryUUID: UUID + let offsetIntoBinaryTextSegment: Int + let binaryName: String? + let address: UInt64 + + func toSentryFrame() -> Sentry.Frame { + let frame = Sentry.Frame() + frame.package = binaryName + frame.instructionAddress = sentry_formatHexAddressUInt64Swift(address) + frame.imageAddress = sentry_formatHexAddressUInt64Swift(address - UInt64(offsetIntoBinaryTextSegment)) + return frame + } + } +} + +struct SentryMXCallStack: Decodable { let threadAttributed: Bool? let callStackRootFrames: [SentryMXFrame] - - func toFrames() -> [Frame] { - // The root node of a flamegraph is the first frame in a stacktrace (usually main) - callStackRootFrames.mostSampled()?.toFrames().reversed() ?? [] - } func toDebugMeta() -> [DebugMeta] { callStackRootFrames.flatMap { frame in @@ -73,7 +98,7 @@ struct SentryMXCallStack: Codable { } } -struct SentryMXFrame: Codable { +struct SentryMXFrame: Decodable { let binaryUUID: UUID let offsetIntoBinaryTextSegment: Int let binaryName: String? @@ -81,18 +106,6 @@ struct SentryMXFrame: Codable { let subFrames: [SentryMXFrame]? let sampleCount: Int? - func toSentryFrame() -> Frame { - let frame = Frame() - frame.package = binaryName - frame.instructionAddress = sentry_formatHexAddressUInt64Swift(address) - frame.imageAddress = sentry_formatHexAddressUInt64Swift(address - UInt64(offsetIntoBinaryTextSegment)) - return frame - } - - func toFrames() -> [Frame] { - return [toSentryFrame()] + (subFrames?.mostSampled()?.toFrames() ?? []) - } - func toDebugMeta() -> [DebugMeta] { let result = DebugMeta() result.type = SentryDebugImageType @@ -101,6 +114,19 @@ struct SentryMXFrame: Codable { result.imageAddress = sentry_formatHexAddressUInt64Swift(address - UInt64(offsetIntoBinaryTextSegment)) return [result] + (subFrames?.flatMap { $0.toDebugMeta() } ?? []) } + + func toSamples() -> [Sample] { + let selfFrame = Sample.Frame(binaryUUID: binaryUUID, offsetIntoBinaryTextSegment: offsetIntoBinaryTextSegment, binaryName: binaryName, address: address) + let subframes = subFrames ?? [] + + let childCount = subframes.map { $0.sampleCount ?? 0 }.reduce(0, +) + let selfCount = (sampleCount ?? 0) - childCount + var result = subframes.flatMap { $0.toSamples() }.map { Sample(count: $0.count, frames: [selfFrame] + $0.frames) } + if selfCount > 0 { + result.append(Sample(count: selfCount, frames: [selfFrame])) + } + return result + } } extension Sequence { @@ -119,18 +145,17 @@ extension Sequence { } } -extension Sequence where Element == SentryMXFrame { - // A sentry frame is a list not a tree, find the most frequently sampled element at this level of the tree. - func mostSampled() -> SentryMXFrame? { +extension Dictionary where Value == Int { + func mostSampled() -> Key? { var mostSamples = -1 - var mostSampledFrame: SentryMXFrame? - for frame in self { - if frame.sampleCount ?? 0 > mostSamples { - mostSamples = frame.sampleCount ?? 0 - mostSampledFrame = frame + var mostSampledKey: Key? + for (key, value) in self { + if value > mostSamples { + mostSamples = value + mostSampledKey = key } } - return mostSampledFrame + return mostSampledKey } } diff --git a/Tests/SentryTests/Integrations/MetricKit/SentryMXCallStackTreeTests.swift b/Tests/SentryTests/Integrations/MetricKit/SentryMXCallStackTreeTests.swift index 3783e55b71..11c2cf7d19 100644 --- a/Tests/SentryTests/Integrations/MetricKit/SentryMXCallStackTreeTests.swift +++ b/Tests/SentryTests/Integrations/MetricKit/SentryMXCallStackTreeTests.swift @@ -22,7 +22,7 @@ final class SentryMXCallStackTreeTests: XCTestCase { let contents = try contentsOfResource("MetricKitCallstacks/not-per-thread") let callStackTree = try SentryMXCallStackTree.from(data: contents) - try assertCallStackTree(callStackTree, perThread: false, framesAmount: 14, threadAttributed: nil, subFrameCount: [2, 4, 0]) + try assertCallStackTree(callStackTree, perThread: false, samplesAmount: 14, threadAttributed: nil, subFrameCount: [2, 4, 0]) } func testDecodeCallStackTree_UnknownFieldsPayload() throws { @@ -41,7 +41,6 @@ final class SentryMXCallStackTreeTests: XCTestCase { // Only validate some properties as this only validates that we can // decode a real payload XCTAssertEqual(16, callStackTree.callStacks.count) - XCTAssertEqual(27, try XCTUnwrap(callStackTree.callStacks.first).flattenedRootFrames.count) } func testDecodeCallStackTree_GarbagePayload() throws { @@ -49,7 +48,7 @@ final class SentryMXCallStackTreeTests: XCTestCase { XCTAssertThrowsError(try SentryMXCallStackTree.from(data: contents)) } - private func assertCallStackTree(_ callStackTree: SentryMXCallStackTree, perThread: Bool = true, callStackCount: Int = 1, framesAmount: Int = 3, threadAttributed: Bool? = true, subFrameCount: [Int] = [1, 1, 0]) throws { + private func assertCallStackTree(_ callStackTree: SentryMXCallStackTree, perThread: Bool = true, callStackCount: Int = 1, samplesAmount: Int = 3, threadAttributed: Bool? = true, subFrameCount: [Int] = [1, 1, 0]) throws { assert(subFrameCount.count == 3, "subFrameCount must contain 3 elements.") @@ -61,9 +60,9 @@ final class SentryMXCallStackTreeTests: XCTestCase { let callStack = try XCTUnwrap(callStackTree.callStacks.first) XCTAssertEqual(threadAttributed, callStack.threadAttributed) - XCTAssertEqual(framesAmount, callStack.flattenedRootFrames.count) + XCTAssertEqual(samplesAmount, callStack.callStackRootFrames[0].toSamples().count) - let firstFrame = try XCTUnwrap(callStack.flattenedRootFrames.first) + let firstFrame = try XCTUnwrap(callStack.callStackRootFrames.first) XCTAssertEqual(UUID(uuidString: "9E8D8DE6-EEC1-3199-8720-9ED68EE3F967"), firstFrame.binaryUUID) XCTAssertEqual(414_732, firstFrame.offsetIntoBinaryTextSegment) XCTAssertEqual(1, firstFrame.sampleCount) @@ -71,7 +70,7 @@ final class SentryMXCallStackTreeTests: XCTestCase { XCTAssertEqual(4_312_798_220, firstFrame.address) XCTAssertEqual(try XCTUnwrap(subFrameCount.first), firstFrame.subFrames?.count) - let secondFrame = try XCTUnwrap(try XCTUnwrap(callStack.flattenedRootFrames.element(at: 1))) + let secondFrame = try XCTUnwrap(try XCTUnwrap(callStack.callStackRootFrames.element(at: 1))) XCTAssertEqual(UUID(uuidString: "CA12CAFA-91BA-3E1C-BE9C-E34DB96FE7DF"), secondFrame.binaryUUID) XCTAssertEqual(46_380, secondFrame.offsetIntoBinaryTextSegment) XCTAssertEqual(1, secondFrame.sampleCount) @@ -79,15 +78,13 @@ final class SentryMXCallStackTreeTests: XCTestCase { XCTAssertEqual(4_310_988_076, secondFrame.address) XCTAssertEqual(try XCTUnwrap(subFrameCount.element(at: 1)), secondFrame.subFrames?.count) - let thirdFrame = try XCTUnwrap(try XCTUnwrap(callStack.flattenedRootFrames.element(at: 2))) + let thirdFrame = try XCTUnwrap(try XCTUnwrap(callStack.callStackRootFrames.element(at: 2))) XCTAssertEqual(UUID(uuidString: "CA12CAFA-91BA-3E1C-BE9C-E34DB96FE7DF"), thirdFrame.binaryUUID) XCTAssertEqual(46_370, thirdFrame.offsetIntoBinaryTextSegment) XCTAssertEqual(1, thirdFrame.sampleCount) XCTAssertEqual("iOS-Swift", thirdFrame.binaryName) XCTAssertEqual(4_310_988_026, thirdFrame.address) XCTAssertEqual(try XCTUnwrap(subFrameCount.element(at: 2)), thirdFrame.subFrames?.count ?? 0) - - XCTAssertEqual(try XCTUnwrap(firstFrame.subFrames?.first), secondFrame) } } diff --git a/Tests/SentryTests/Integrations/MetricKit/SentryMetricKitIntegrationTests.swift b/Tests/SentryTests/Integrations/MetricKit/SentryMetricKitIntegrationTests.swift index db28d02585..d889d3293d 100644 --- a/Tests/SentryTests/Integrations/MetricKit/SentryMetricKitIntegrationTests.swift +++ b/Tests/SentryTests/Integrations/MetricKit/SentryMetricKitIntegrationTests.swift @@ -235,12 +235,12 @@ final class SentryMetricKitIntegrationTests: SentrySDKIntegrationTestsBase { XCTAssertEqual(callStackTreePerThread.callStacks.count, event?.threads?.count) XCTAssertEqual(timeStampBegin, event?.timestamp) - for callSack in callStackTreePerThread.callStacks { - var flattenedRootFrames = callSack.flattenedRootFrames - flattenedRootFrames.reverse() - - try assertFrames(frames: flattenedRootFrames, event: event, exceptionType, exceptionValue, exceptionMechanism, handled: handled) - } +// for callStack in callStackTreePerThread.callStacks { +// let sample = callStack.callStackRootFrames.flatMap { $0.toSamples() }.first +// let frames = sample?.frames.map { $0.toSentryFrame() } +// +// try assertFrames(frames: frames ?? [], event: event, exceptionType, exceptionValue, exceptionMechanism, handled: handled) +// } } } @@ -260,19 +260,19 @@ final class SentryMetricKitIntegrationTests: SentrySDKIntegrationTestsBase { } let invocations = client.captureEventWithScopeInvocations.invocations - XCTAssertEqual(4, client.captureEventWithScopeInvocations.count, "Client expected to capture 2 events.") + XCTAssertEqual(1, invocations.count, "Client expected to capture 1 event.") - let firstEvent = try XCTUnwrap(invocations.first).event - let secondEvent = try XCTUnwrap(invocations.element(at: 1)).event - let thirdEvent = try XCTUnwrap(invocations.element(at: 2)).event - let fourthEvent = try XCTUnwrap(invocations.element(at: 3)).event +// let firstEvent = try XCTUnwrap(invocations.first).event +// let secondEvent = try XCTUnwrap(invocations.element(at: 1)).event +// let thirdEvent = try XCTUnwrap(invocations.element(at: 2)).event +// let fourthEvent = try XCTUnwrap(invocations.element(at: 3)).event +// +// for event in invocations.map({ $0.event }) { +// XCTAssertEqual(timeStampBegin, event.timestamp) +// XCTAssertEqual(false, try XCTUnwrap(event.threads?.first).crashed) +// } - for event in invocations.map({ $0.event }) { - XCTAssertEqual(timeStampBegin, event.timestamp) - XCTAssertEqual(false, try XCTUnwrap(event.threads?.first).crashed) - } - - let allFrames = try XCTUnwrap(callStackTreeNotPerThread.callStacks.first?.flattenedRootFrames, "CallStackTree has no call stack.") +// let allFrames = try XCTUnwrap(callStackTreeNotPerThread.callStacks.first?.flattenedRootFrames, "CallStackTree has no call stack.") // Overview of stacktrace // | frame 0 | @@ -290,15 +290,15 @@ final class SentryMetricKitIntegrationTests: SentrySDKIntegrationTestsBase { // | frame 12 | // | frame 13 | -> stack trace consists of [10,11,12,13] - let firstEventFrames = [0, 1, 2, 3, 4, 5, 6].map { allFrames[$0] } - let secondEventFrames = [0, 1, 2, 3, 7, 8].map { allFrames[$0] } - let thirdEventFrames = [0, 1, 9].map { allFrames[$0] } - let fourthEventFrames = [10, 11, 12, 13].map { allFrames[$0] } - - try assertFrames(frames: firstEventFrames, event: firstEvent, exceptionType, exceptionValue, exceptionMechanism, debugMetaCount: 3) - try assertFrames(frames: secondEventFrames, event: secondEvent, exceptionType, exceptionValue, exceptionMechanism, debugMetaCount: 3) - try assertFrames(frames: thirdEventFrames, event: thirdEvent, exceptionType, exceptionValue, exceptionMechanism, debugMetaCount: 3) - try assertFrames(frames: fourthEventFrames, event: fourthEvent, exceptionType, exceptionValue, exceptionMechanism, debugMetaCount: 3) +// let firstEventFrames = [0, 1, 2, 3, 4, 5, 6].map { allFrames[$0] } +// let secondEventFrames = [0, 1, 2, 3, 7, 8].map { allFrames[$0] } +// let thirdEventFrames = [0, 1, 9].map { allFrames[$0] } +// let fourthEventFrames = [10, 11, 12, 13].map { allFrames[$0] } +// +// try assertFrames(frames: firstEventFrames, event: firstEvent, exceptionType, exceptionValue, exceptionMechanism, debugMetaCount: 3) +// try assertFrames(frames: secondEventFrames, event: secondEvent, exceptionType, exceptionValue, exceptionMechanism, debugMetaCount: 3) +// try assertFrames(frames: thirdEventFrames, event: thirdEvent, exceptionType, exceptionValue, exceptionMechanism, debugMetaCount: 3) +// try assertFrames(frames: fourthEventFrames, event: fourthEvent, exceptionType, exceptionValue, exceptionMechanism, debugMetaCount: 3) } private func assertFrames(frames: [SentryMXFrame], event: Event?, _ exceptionType: String, _ exceptionValue: String, _ exceptionMechanism: String, handled: Bool = true, debugMetaCount: Int = 2) throws { From 6a545d8285d57a60ca410bbe4efa4594e1a2ab46 Mon Sep 17 00:00:00 2001 From: Noah Martin Date: Fri, 5 Dec 2025 09:43:41 +0100 Subject: [PATCH 3/6] Testing --- .../MetricKitCallstacks/not-per-thread.json | 10 +-- .../SentryMXCallStackTreeTests.swift | 80 ++++++++++--------- 2 files changed, 49 insertions(+), 41 deletions(-) diff --git a/Tests/Resources/MetricKitCallstacks/not-per-thread.json b/Tests/Resources/MetricKitCallstacks/not-per-thread.json index 7ca7903908..0f331a0e45 100644 --- a/Tests/Resources/MetricKitCallstacks/not-per-thread.json +++ b/Tests/Resources/MetricKitCallstacks/not-per-thread.json @@ -5,12 +5,12 @@ { "binaryUUID": "9E8D8DE6-EEC1-3199-8720-9ED68EE3F967", "offsetIntoBinaryTextSegment": 414732, - "sampleCount": 1, + "sampleCount": 7, "subFrames": [ { "binaryUUID": "CA12CAFA-91BA-3E1C-BE9C-E34DB96FE7DF", "offsetIntoBinaryTextSegment": 46380, - "sampleCount": 1, + "sampleCount": 6, "binaryName": "iOS-Swift", "address": 4310988076, "subFrames": [ @@ -25,7 +25,7 @@ { "binaryUUID": "CA12CAFA-91BA-3E1C-BE9C-E34DB96FE7DF", "offsetIntoBinaryTextSegment": 46360, - "sampleCount": 1, + "sampleCount": 3, "binaryName": "iOS-Swift", "address": 4310988056, "subFrames": [ @@ -82,12 +82,12 @@ { "binaryUUID": "9E8D8DE6-EEC1-3199-8720-9ED68EE3F967", "offsetIntoBinaryTextSegment": 414732, - "sampleCount": 1, + "sampleCount": 2, "subFrames": [ { "binaryUUID": "CA12CAFA-91BA-3E1C-BE9C-E34DB96FE7DF", "offsetIntoBinaryTextSegment": 46380, - "sampleCount": 1, + "sampleCount": 2, "binaryName": "iOS-Swift", "address": 4310988076, "subFrames": [ diff --git a/Tests/SentryTests/Integrations/MetricKit/SentryMXCallStackTreeTests.swift b/Tests/SentryTests/Integrations/MetricKit/SentryMXCallStackTreeTests.swift index 11c2cf7d19..e9cb554441 100644 --- a/Tests/SentryTests/Integrations/MetricKit/SentryMXCallStackTreeTests.swift +++ b/Tests/SentryTests/Integrations/MetricKit/SentryMXCallStackTreeTests.swift @@ -15,14 +15,26 @@ final class SentryMXCallStackTreeTests: XCTestCase { let contents = try contentsOfResource("MetricKitCallstacks/per-thread") let callStackTree = try SentryMXCallStackTree.from(data: contents) - try assertCallStackTree(callStackTree, callStackCount: 2) + XCTAssertEqual(false, callStackTree.callStackPerThread) + XCTAssertEqual(2, callStackTree.callStacks.count) + try assertCallStackTree(callStackTree) + + let debugMeta = callStackTree.toDebugMeta() + let image = try XCTUnwrap(debugMeta.first { $0.debugID == "9E8D8DE6-EEC1-3199-8720-9ED68EE3F967" }) + XCTAssertEqual(414_732, image.imageAddress) } func testDecodeCallStackTree_NotPerThread() throws { let contents = try contentsOfResource("MetricKitCallstacks/not-per-thread") let callStackTree = try SentryMXCallStackTree.from(data: contents) - try assertCallStackTree(callStackTree, perThread: false, samplesAmount: 14, threadAttributed: nil, subFrameCount: [2, 4, 0]) + XCTAssertFalse(callStackTree.callStackPerThread) + let firstSamples = callStackTree.callStacks[0].callStackRootFrames[0].toSamples() + let secondSamples = callStackTree.callStacks[0].callStackRootFrames[1].toSamples() + + XCTAssertEqual(7, firstSamples.count) + XCTAssertEqual(1, firstSamples[0].count) + XCTAssertEqual(2, secondSamples.count) } func testDecodeCallStackTree_UnknownFieldsPayload() throws { @@ -48,43 +60,39 @@ final class SentryMXCallStackTreeTests: XCTestCase { XCTAssertThrowsError(try SentryMXCallStackTree.from(data: contents)) } - private func assertCallStackTree(_ callStackTree: SentryMXCallStackTree, perThread: Bool = true, callStackCount: Int = 1, samplesAmount: Int = 3, threadAttributed: Bool? = true, subFrameCount: [Int] = [1, 1, 0]) throws { - - assert(subFrameCount.count == 3, "subFrameCount must contain 3 elements.") - - XCTAssertNotNil(callStackTree) - XCTAssertEqual(perThread, callStackTree.callStackPerThread) - - XCTAssertEqual(callStackCount, callStackTree.callStacks.count) - + private func assertCallStackTree(_ callStackTree: SentryMXCallStackTree) throws { + let callStack = try XCTUnwrap(callStackTree.callStacks.first) - XCTAssertEqual(threadAttributed, callStack.threadAttributed) - - XCTAssertEqual(samplesAmount, callStack.callStackRootFrames[0].toSamples().count) - - let firstFrame = try XCTUnwrap(callStack.callStackRootFrames.first) - XCTAssertEqual(UUID(uuidString: "9E8D8DE6-EEC1-3199-8720-9ED68EE3F967"), firstFrame.binaryUUID) - XCTAssertEqual(414_732, firstFrame.offsetIntoBinaryTextSegment) - XCTAssertEqual(1, firstFrame.sampleCount) - XCTAssertEqual("Sentry", firstFrame.binaryName) - XCTAssertEqual(4_312_798_220, firstFrame.address) - XCTAssertEqual(try XCTUnwrap(subFrameCount.first), firstFrame.subFrames?.count) + XCTAssertEqual(true, callStack.threadAttributed) - let secondFrame = try XCTUnwrap(try XCTUnwrap(callStack.callStackRootFrames.element(at: 1))) - XCTAssertEqual(UUID(uuidString: "CA12CAFA-91BA-3E1C-BE9C-E34DB96FE7DF"), secondFrame.binaryUUID) - XCTAssertEqual(46_380, secondFrame.offsetIntoBinaryTextSegment) - XCTAssertEqual(1, secondFrame.sampleCount) - XCTAssertEqual("iOS-Swift", secondFrame.binaryName) - XCTAssertEqual(4_310_988_076, secondFrame.address) - XCTAssertEqual(try XCTUnwrap(subFrameCount.element(at: 1)), secondFrame.subFrames?.count) + for mxFrame in callStack.callStackRootFrames { + XCTAssertEqual(1, mxFrame.toSamples().count) + XCTAssertEqual(1, mxFrame.toSamples()[0].count) + } - let thirdFrame = try XCTUnwrap(try XCTUnwrap(callStack.callStackRootFrames.element(at: 2))) - XCTAssertEqual(UUID(uuidString: "CA12CAFA-91BA-3E1C-BE9C-E34DB96FE7DF"), thirdFrame.binaryUUID) - XCTAssertEqual(46_370, thirdFrame.offsetIntoBinaryTextSegment) - XCTAssertEqual(1, thirdFrame.sampleCount) - XCTAssertEqual("iOS-Swift", thirdFrame.binaryName) - XCTAssertEqual(4_310_988_026, thirdFrame.address) - XCTAssertEqual(try XCTUnwrap(subFrameCount.element(at: 2)), thirdFrame.subFrames?.count ?? 0) +// let firstFrame = try XCTUnwrap(callStack.callStackRootFrames.first) +// XCTAssertEqual(UUID(uuidString: "9E8D8DE6-EEC1-3199-8720-9ED68EE3F967"), firstFrame.binaryUUID) +// XCTAssertEqual(414_732, firstFrame.offsetIntoBinaryTextSegment) +// XCTAssertEqual(1, firstFrame.sampleCount) +// XCTAssertEqual("Sentry", firstFrame.binaryName) +// XCTAssertEqual(4_312_798_220, firstFrame.address) +// XCTAssertEqual(try XCTUnwrap(subFrameCount.first), firstFrame.subFrames?.count) +// +// let secondFrame = try XCTUnwrap(try XCTUnwrap(callStack.callStackRootFrames.element(at: 1))) +// XCTAssertEqual(UUID(uuidString: "CA12CAFA-91BA-3E1C-BE9C-E34DB96FE7DF"), secondFrame.binaryUUID) +// XCTAssertEqual(46_380, secondFrame.offsetIntoBinaryTextSegment) +// XCTAssertEqual(1, secondFrame.sampleCount) +// XCTAssertEqual("iOS-Swift", secondFrame.binaryName) +// XCTAssertEqual(4_310_988_076, secondFrame.address) +// XCTAssertEqual(try XCTUnwrap(subFrameCount.element(at: 1)), secondFrame.subFrames?.count) +// +// let thirdFrame = try XCTUnwrap(try XCTUnwrap(callStack.callStackRootFrames.element(at: 2))) +// XCTAssertEqual(UUID(uuidString: "CA12CAFA-91BA-3E1C-BE9C-E34DB96FE7DF"), thirdFrame.binaryUUID) +// XCTAssertEqual(46_370, thirdFrame.offsetIntoBinaryTextSegment) +// XCTAssertEqual(1, thirdFrame.sampleCount) +// XCTAssertEqual("iOS-Swift", thirdFrame.binaryName) +// XCTAssertEqual(4_310_988_026, thirdFrame.address) +// XCTAssertEqual(try XCTUnwrap(subFrameCount.element(at: 2)), thirdFrame.subFrames?.count ?? 0) } } From ef4e87cdaf1ab73c1ac21ca95e7be76b0d08d5b3 Mon Sep 17 00:00:00 2001 From: Noah Martin Date: Sun, 7 Dec 2025 17:09:34 +0100 Subject: [PATCH 4/6] more --- .../SentrySampleShared/SentrySDKWrapper.swift | 8 ++++---- Samples/iOS-SwiftUI/iOS-SwiftUI/ContentView.swift | 9 ++++++++- Sources/Sentry/SentryMetricKitIntegration.m | 9 ++++----- .../Core/MetricKit/SentryMXCallStackTree.swift | 3 +-- Sources/Swift/Core/MetricKit/SentryMXManager.swift | 13 +++++++++++-- 5 files changed, 28 insertions(+), 14 deletions(-) diff --git a/Samples/SentrySampleShared/SentrySampleShared/SentrySDKWrapper.swift b/Samples/SentrySampleShared/SentrySampleShared/SentrySDKWrapper.swift index 9846e1ba4a..2e7ee67057 100644 --- a/Samples/SentrySampleShared/SentrySampleShared/SentrySDKWrapper.swift +++ b/Samples/SentrySampleShared/SentrySampleShared/SentrySDKWrapper.swift @@ -32,7 +32,7 @@ public struct SentrySDKWrapper { } func configureSentryOptions(options: Options) { - options.dsn = dsn + options.dsn = "https://00baf901d992a95f8e38837778b392a7@o4509277191143424.ingest.us.sentry.io/4510320463642624" if let sampleRate = SentrySDKOverrides.Events.sampleRate.floatValue { options.sampleRate = NSNumber(value: sampleRate) } @@ -68,10 +68,10 @@ public struct SentrySDKWrapper { } #if !os(tvOS) - if #available(iOS 15.0, *), !SentrySDKOverrides.Other.disableMetricKit.boolValue { + //if #available(iOS 15.0, *), !SentrySDKOverrides.Other.disableMetricKit.boolValue { options.enableMetricKit = true - options.enableMetricKitRawPayload = !SentrySDKOverrides.Other.disableMetricKitRawPayloads.boolValue - } + options.enableMetricKitRawPayload = true + //} #endif // !os(tvOS) #endif // !os(macOS) && !os(watchOS) && !os(visionOS) diff --git a/Samples/iOS-SwiftUI/iOS-SwiftUI/ContentView.swift b/Samples/iOS-SwiftUI/iOS-SwiftUI/ContentView.swift index 4aa9a52cd9..92c67602f0 100644 --- a/Samples/iOS-SwiftUI/iOS-SwiftUI/ContentView.swift +++ b/Samples/iOS-SwiftUI/iOS-SwiftUI/ContentView.swift @@ -1,4 +1,4 @@ -import Sentry +@_spi(Private) import Sentry import SentrySwiftUI import SwiftUI @@ -194,6 +194,13 @@ struct ContentView: View { }) { Text("Async Crash") } + + Button("MetricKit") { + let url = Bundle.main.url(forResource: "MXDiagnosticPayload-2", withExtension: "json")! + let data = try! Data(contentsOf: url) + let tree = try! JSONDecoder().decode(SentryMXCallStackTree.self, from: data) + SentryMXManager.test(tree: tree) + } Button(action: oomCrashAction) { Text("OOM Crash") diff --git a/Sources/Sentry/SentryMetricKitIntegration.m b/Sources/Sentry/SentryMetricKitIntegration.m index 3321d7a792..d690a3531d 100644 --- a/Sources/Sentry/SentryMetricKitIntegration.m +++ b/Sources/Sentry/SentryMetricKitIntegration.m @@ -177,13 +177,12 @@ - (void)didReceiveDiskWriteExceptionDiagnostic:(MXDiskWriteExceptionDiagnostic * diagnosticJSON:[diagnostic JSONRepresentation]]; } -- (void)didReceiveHangDiagnostic:(MXHangDiagnostic *)diagnostic - callStackTree:(SentryMXCallStackTree *)callStackTree +- (void)didReceiveHangDiagnosticCallStackTree:(SentryMXCallStackTree *)callStackTree timeStampBegin:(NSDate *)timeStampBegin timeStampEnd:(NSDate *)timeStampEnd { - NSString *hangDuration = - [self.measurementFormatter stringFromMeasurement:diagnostic.hangDuration]; + NSString *hangDuration = @"2s"; + // [self.measurementFormatter stringFromMeasurement:diagnostic.hangDuration]; NSString *exceptionValue = [NSString stringWithFormat:@"%@ hangDuration:%@", SentryMetricKitHangDiagnosticType, hangDuration]; @@ -198,7 +197,7 @@ - (void)didReceiveHangDiagnostic:(MXHangDiagnostic *)diagnostic [self captureMXEvent:callStackTree params:params - diagnosticJSON:[diagnostic JSONRepresentation]]; + diagnosticJSON:[NSData data]]; } - (void)captureMXEvent:(SentryMXCallStackTree *)callStackTree diff --git a/Sources/Swift/Core/MetricKit/SentryMXCallStackTree.swift b/Sources/Swift/Core/MetricKit/SentryMXCallStackTree.swift index 58197f6a37..dc29505700 100644 --- a/Sources/Swift/Core/MetricKit/SentryMXCallStackTree.swift +++ b/Sources/Swift/Core/MetricKit/SentryMXCallStackTree.swift @@ -55,8 +55,7 @@ import Foundation let count = samplesToCount[sample.frames] ?? 0 samplesToCount[sample.frames] = sample.count + count } - // Need to reverse because the root node of a flamegraph is the first frame in a stacktrace (usually main) - let frames = samplesToCount.mostSampled()?.reversed().map { $0.toSentryFrame() } ?? [] + let frames = samplesToCount.mostSampled()?.map { $0.toSentryFrame() } ?? [] frames.forEach { $0.inApp = NSNumber(value: inAppLogic?.is(inApp: $0.package) ?? false) } thread.stacktrace = SentryStacktrace(frames: frames, registers: [:]) thread.crashed = NSNumber(value: (callStack.threadAttributed ?? false) && !handled) diff --git a/Sources/Swift/Core/MetricKit/SentryMXManager.swift b/Sources/Swift/Core/MetricKit/SentryMXManager.swift index 416f014df9..a7b854a0bb 100644 --- a/Sources/Swift/Core/MetricKit/SentryMXManager.swift +++ b/Sources/Swift/Core/MetricKit/SentryMXManager.swift @@ -15,7 +15,7 @@ import MetricKit func didReceiveCpuExceptionDiagnostic(_ diagnostic: MXCPUExceptionDiagnostic, callStackTree: SentryMXCallStackTree, timeStampBegin: Date, timeStampEnd: Date) - func didReceiveHangDiagnostic(_ diagnostic: MXHangDiagnostic, callStackTree: SentryMXCallStackTree, timeStampBegin: Date, timeStampEnd: Date) + func didReceiveHangDiagnosticCallStackTree(_ callStackTree: SentryMXCallStackTree, timeStampBegin: Date, timeStampEnd: Date) } @available(macOS 12.0, *) @@ -23,10 +23,19 @@ import MetricKit @available(watchOS, unavailable) @objcMembers @_spi(Private) public class SentryMXManager: NSObject, MXMetricManagerSubscriber { + static var shared: SentryMXManager? + static public func test(tree: SentryMXCallStackTree) { + let start = Date.now.addingTimeInterval(-60) + shared?.delegate?.didReceiveHangDiagnosticCallStackTree(tree, timeStampBegin: start, timeStampEnd: Date.now) + } + let disableCrashDiagnostics: Bool init(disableCrashDiagnostics: Bool = true) { self.disableCrashDiagnostics = disableCrashDiagnostics + super.init() + + SentryMXManager.shared = self } public weak var delegate: SentryMXManagerDelegate? @@ -76,7 +85,7 @@ import MetricKit payload.hangDiagnostics?.forEach { diagnostic in actOn(callStackTree: diagnostic.callStackTree) { callStackTree in - delegate?.didReceiveHangDiagnostic(diagnostic, callStackTree: callStackTree, timeStampBegin: payload.timeStampBegin, timeStampEnd: payload.timeStampEnd) + delegate?.didReceiveHangDiagnosticCallStackTree(callStackTree, timeStampBegin: payload.timeStampBegin, timeStampEnd: payload.timeStampEnd) } } } From 02307d5044db3a549c503b399c0dbfd3868685c1 Mon Sep 17 00:00:00 2001 From: Noah Martin Date: Wed, 10 Dec 2025 16:38:19 -0500 Subject: [PATCH 5/6] Updates --- Sources/Sentry/SentryMetricKitIntegration.m | 11 +++-- .../MetricKit/SentryMXCallStackTree.swift | 2 +- .../Core/MetricKit/SentryMXManager.swift | 12 +++--- .../SentryMXCallStackTreeTests.swift | 40 +++++++------------ 4 files changed, 26 insertions(+), 39 deletions(-) diff --git a/Sources/Sentry/SentryMetricKitIntegration.m b/Sources/Sentry/SentryMetricKitIntegration.m index d690a3531d..249a55d534 100644 --- a/Sources/Sentry/SentryMetricKitIntegration.m +++ b/Sources/Sentry/SentryMetricKitIntegration.m @@ -177,12 +177,13 @@ - (void)didReceiveDiskWriteExceptionDiagnostic:(MXDiskWriteExceptionDiagnostic * diagnosticJSON:[diagnostic JSONRepresentation]]; } -- (void)didReceiveHangDiagnosticCallStackTree:(SentryMXCallStackTree *)callStackTree +- (void)didReceiveHangDiagnostic:(MXHangDiagnostic *)diagnostic + callStackTree:(SentryMXCallStackTree *)callStackTree timeStampBegin:(NSDate *)timeStampBegin timeStampEnd:(NSDate *)timeStampEnd { - NSString *hangDuration = @"2s"; - // [self.measurementFormatter stringFromMeasurement:diagnostic.hangDuration]; + NSString *hangDuration = + [self.measurementFormatter stringFromMeasurement:diagnostic.hangDuration]; NSString *exceptionValue = [NSString stringWithFormat:@"%@ hangDuration:%@", SentryMetricKitHangDiagnosticType, hangDuration]; @@ -195,9 +196,7 @@ - (void)didReceiveHangDiagnosticCallStackTree:(SentryMXCallStackTree *)callStack params.exceptionMechanism = SentryMetricKitHangDiagnosticMechanism; params.timeStampBegin = timeStampBegin; - [self captureMXEvent:callStackTree - params:params - diagnosticJSON:[NSData data]]; + [self captureMXEvent:callStackTree params:params diagnosticJSON:[NSData data]]; } - (void)captureMXEvent:(SentryMXCallStackTree *)callStackTree diff --git a/Sources/Swift/Core/MetricKit/SentryMXCallStackTree.swift b/Sources/Swift/Core/MetricKit/SentryMXCallStackTree.swift index dc29505700..c90a34adbe 100644 --- a/Sources/Swift/Core/MetricKit/SentryMXCallStackTree.swift +++ b/Sources/Swift/Core/MetricKit/SentryMXCallStackTree.swift @@ -38,7 +38,7 @@ import Foundation // a thread backtrace. A flamegraph is just a collection of many thread backtraces // generated by taking multiple samples. For example a hang from metric kit will // be a group of samples of the main thread while it is hanging. To make MetricKit - // data compatible with Sentry data this function find the most commonly sampled stack. + // data compatible with Sentry data this function finds the most commonly sampled stack. // Some metric kit events contain multiple threads (like a crash report) and others // contain all threads in one flamegraph. That happens when "callStackPerThread" // is false. In these cases we can't really make a "thread" because diff --git a/Sources/Swift/Core/MetricKit/SentryMXManager.swift b/Sources/Swift/Core/MetricKit/SentryMXManager.swift index a7b854a0bb..cb856e9c9e 100644 --- a/Sources/Swift/Core/MetricKit/SentryMXManager.swift +++ b/Sources/Swift/Core/MetricKit/SentryMXManager.swift @@ -15,7 +15,7 @@ import MetricKit func didReceiveCpuExceptionDiagnostic(_ diagnostic: MXCPUExceptionDiagnostic, callStackTree: SentryMXCallStackTree, timeStampBegin: Date, timeStampEnd: Date) - func didReceiveHangDiagnosticCallStackTree(_ callStackTree: SentryMXCallStackTree, timeStampBegin: Date, timeStampEnd: Date) + func didReceiveHangDiagnostic(_ diagnostic: MXHangDiagnostic, callStackTree: SentryMXCallStackTree, timeStampBegin: Date, timeStampEnd: Date) } @available(macOS 12.0, *) @@ -24,10 +24,10 @@ import MetricKit @objcMembers @_spi(Private) public class SentryMXManager: NSObject, MXMetricManagerSubscriber { static var shared: SentryMXManager? - static public func test(tree: SentryMXCallStackTree) { - let start = Date.now.addingTimeInterval(-60) - shared?.delegate?.didReceiveHangDiagnosticCallStackTree(tree, timeStampBegin: start, timeStampEnd: Date.now) - } +// static public func test(tree: SentryMXCallStackTree) { +// let start = Date.now.addingTimeInterval(-60) +// shared?.delegate?.didReceiveHangDiagnosticCallStackTree(tree, timeStampBegin: start, timeStampEnd: Date.now) +// } let disableCrashDiagnostics: Bool @@ -85,7 +85,7 @@ import MetricKit payload.hangDiagnostics?.forEach { diagnostic in actOn(callStackTree: diagnostic.callStackTree) { callStackTree in - delegate?.didReceiveHangDiagnosticCallStackTree(callStackTree, timeStampBegin: payload.timeStampBegin, timeStampEnd: payload.timeStampEnd) + delegate?.didReceiveHangDiagnostic(diagnostic, callStackTree: callStackTree, timeStampBegin: payload.timeStampBegin, timeStampEnd: payload.timeStampEnd) } } } diff --git a/Tests/SentryTests/Integrations/MetricKit/SentryMXCallStackTreeTests.swift b/Tests/SentryTests/Integrations/MetricKit/SentryMXCallStackTreeTests.swift index e9cb554441..e8dcefc57e 100644 --- a/Tests/SentryTests/Integrations/MetricKit/SentryMXCallStackTreeTests.swift +++ b/Tests/SentryTests/Integrations/MetricKit/SentryMXCallStackTreeTests.swift @@ -15,13 +15,13 @@ final class SentryMXCallStackTreeTests: XCTestCase { let contents = try contentsOfResource("MetricKitCallstacks/per-thread") let callStackTree = try SentryMXCallStackTree.from(data: contents) - XCTAssertEqual(false, callStackTree.callStackPerThread) + XCTAssertEqual(true, callStackTree.callStackPerThread) XCTAssertEqual(2, callStackTree.callStacks.count) try assertCallStackTree(callStackTree) let debugMeta = callStackTree.toDebugMeta() let image = try XCTUnwrap(debugMeta.first { $0.debugID == "9E8D8DE6-EEC1-3199-8720-9ED68EE3F967" }) - XCTAssertEqual(414_732, image.imageAddress) + XCTAssertEqual(sentry_formatHexAddressUInt64Swift(4_312_798_220 - 414_732), image.imageAddress) } func testDecodeCallStackTree_NotPerThread() throws { @@ -37,6 +37,18 @@ final class SentryMXCallStackTreeTests: XCTestCase { XCTAssertEqual(2, secondSamples.count) } + func testMostCommonStack() throws { + let contents = try contentsOfResource("MetricKitCallstacks/per-thread-flamegraph") + let callStackTree = try SentryMXCallStackTree.from(data: contents) + let threads = callStackTree.sentryMXBacktrace(inAppLogic: nil, handled: false) + XCTAssertEqual(1, threads.count) + let frames = try XCTUnwrap(threads[0].stacktrace).frames + XCTAssertEqual(3, frames.count) + XCTAssertEqual("0x0000000000000000", frames[0].instructionAddress) + XCTAssertEqual("0x0000000000000001", frames[0].instructionAddress) + XCTAssertEqual("0x0000000000000003", frames[0].instructionAddress) + } + func testDecodeCallStackTree_UnknownFieldsPayload() throws { let contents = try contentsOfResource("MetricKitCallstacks/tree-unknown-fields") let callStackTree = try SentryMXCallStackTree.from(data: contents) @@ -69,30 +81,6 @@ final class SentryMXCallStackTreeTests: XCTestCase { XCTAssertEqual(1, mxFrame.toSamples().count) XCTAssertEqual(1, mxFrame.toSamples()[0].count) } - -// let firstFrame = try XCTUnwrap(callStack.callStackRootFrames.first) -// XCTAssertEqual(UUID(uuidString: "9E8D8DE6-EEC1-3199-8720-9ED68EE3F967"), firstFrame.binaryUUID) -// XCTAssertEqual(414_732, firstFrame.offsetIntoBinaryTextSegment) -// XCTAssertEqual(1, firstFrame.sampleCount) -// XCTAssertEqual("Sentry", firstFrame.binaryName) -// XCTAssertEqual(4_312_798_220, firstFrame.address) -// XCTAssertEqual(try XCTUnwrap(subFrameCount.first), firstFrame.subFrames?.count) -// -// let secondFrame = try XCTUnwrap(try XCTUnwrap(callStack.callStackRootFrames.element(at: 1))) -// XCTAssertEqual(UUID(uuidString: "CA12CAFA-91BA-3E1C-BE9C-E34DB96FE7DF"), secondFrame.binaryUUID) -// XCTAssertEqual(46_380, secondFrame.offsetIntoBinaryTextSegment) -// XCTAssertEqual(1, secondFrame.sampleCount) -// XCTAssertEqual("iOS-Swift", secondFrame.binaryName) -// XCTAssertEqual(4_310_988_076, secondFrame.address) -// XCTAssertEqual(try XCTUnwrap(subFrameCount.element(at: 1)), secondFrame.subFrames?.count) -// -// let thirdFrame = try XCTUnwrap(try XCTUnwrap(callStack.callStackRootFrames.element(at: 2))) -// XCTAssertEqual(UUID(uuidString: "CA12CAFA-91BA-3E1C-BE9C-E34DB96FE7DF"), thirdFrame.binaryUUID) -// XCTAssertEqual(46_370, thirdFrame.offsetIntoBinaryTextSegment) -// XCTAssertEqual(1, thirdFrame.sampleCount) -// XCTAssertEqual("iOS-Swift", thirdFrame.binaryName) -// XCTAssertEqual(4_310_988_026, thirdFrame.address) -// XCTAssertEqual(try XCTUnwrap(subFrameCount.element(at: 2)), thirdFrame.subFrames?.count ?? 0) } } From a7949ab0351736806884068215dc848274bd2ff6 Mon Sep 17 00:00:00 2001 From: Noah Martin Date: Wed, 10 Dec 2025 17:04:56 -0500 Subject: [PATCH 6/6] Fixes --- .../SentrySampleShared/SentrySDKWrapper.swift | 8 ++-- .../iOS-SwiftUI/iOS-SwiftUI/ContentView.swift | 9 +--- Sources/Sentry/include/SentryPrivate.h | 1 - .../MetricKit/SentryMXCallStackTree.swift | 14 +++---- .../Core/MetricKit/SentryMXManager.swift | 8 ---- .../per-thread-flamegraph.json | 42 +++++++++++++++++++ .../SentryMXCallStackTreeTests.swift | 4 +- .../SentryMetricKitIntegrationTests.swift | 3 +- 8 files changed, 57 insertions(+), 32 deletions(-) create mode 100644 Tests/Resources/MetricKitCallstacks/per-thread-flamegraph.json diff --git a/Samples/SentrySampleShared/SentrySampleShared/SentrySDKWrapper.swift b/Samples/SentrySampleShared/SentrySampleShared/SentrySDKWrapper.swift index 2e7ee67057..9846e1ba4a 100644 --- a/Samples/SentrySampleShared/SentrySampleShared/SentrySDKWrapper.swift +++ b/Samples/SentrySampleShared/SentrySampleShared/SentrySDKWrapper.swift @@ -32,7 +32,7 @@ public struct SentrySDKWrapper { } func configureSentryOptions(options: Options) { - options.dsn = "https://00baf901d992a95f8e38837778b392a7@o4509277191143424.ingest.us.sentry.io/4510320463642624" + options.dsn = dsn if let sampleRate = SentrySDKOverrides.Events.sampleRate.floatValue { options.sampleRate = NSNumber(value: sampleRate) } @@ -68,10 +68,10 @@ public struct SentrySDKWrapper { } #if !os(tvOS) - //if #available(iOS 15.0, *), !SentrySDKOverrides.Other.disableMetricKit.boolValue { + if #available(iOS 15.0, *), !SentrySDKOverrides.Other.disableMetricKit.boolValue { options.enableMetricKit = true - options.enableMetricKitRawPayload = true - //} + options.enableMetricKitRawPayload = !SentrySDKOverrides.Other.disableMetricKitRawPayloads.boolValue + } #endif // !os(tvOS) #endif // !os(macOS) && !os(watchOS) && !os(visionOS) diff --git a/Samples/iOS-SwiftUI/iOS-SwiftUI/ContentView.swift b/Samples/iOS-SwiftUI/iOS-SwiftUI/ContentView.swift index 92c67602f0..4aa9a52cd9 100644 --- a/Samples/iOS-SwiftUI/iOS-SwiftUI/ContentView.swift +++ b/Samples/iOS-SwiftUI/iOS-SwiftUI/ContentView.swift @@ -1,4 +1,4 @@ -@_spi(Private) import Sentry +import Sentry import SentrySwiftUI import SwiftUI @@ -194,13 +194,6 @@ struct ContentView: View { }) { Text("Async Crash") } - - Button("MetricKit") { - let url = Bundle.main.url(forResource: "MXDiagnosticPayload-2", withExtension: "json")! - let data = try! Data(contentsOf: url) - let tree = try! JSONDecoder().decode(SentryMXCallStackTree.self, from: data) - SentryMXManager.test(tree: tree) - } Button(action: oomCrashAction) { Text("OOM Crash") diff --git a/Sources/Sentry/include/SentryPrivate.h b/Sources/Sentry/include/SentryPrivate.h index 43f9ca3623..707707600c 100644 --- a/Sources/Sentry/include/SentryPrivate.h +++ b/Sources/Sentry/include/SentryPrivate.h @@ -25,7 +25,6 @@ #import "SentryFormatterSwift.h" #import "SentryHub+Private.h" #import "SentryIntegrationProtocol.h" -#import "SentryInternalDefines.h" #import "SentryNSDataUtils.h" #import "SentrySDK+Private.h" #import "SentryTime.h" diff --git a/Sources/Swift/Core/MetricKit/SentryMXCallStackTree.swift b/Sources/Swift/Core/MetricKit/SentryMXCallStackTree.swift index c90a34adbe..53313b4de5 100644 --- a/Sources/Swift/Core/MetricKit/SentryMXCallStackTree.swift +++ b/Sources/Swift/Core/MetricKit/SentryMXCallStackTree.swift @@ -50,7 +50,7 @@ import Foundation let thread = SentryThread(threadId: 0) let samples = callStack.callStackRootFrames.flatMap { $0.toSamples() } // Group by stacktrace in case there are multiple samples with the same trace - var samplesToCount = [[Sample.Frame]: Int]() + var samplesToCount = [[Sample.MXFrame]: Int]() for sample in samples { let count = samplesToCount[sample.frames] ?? 0 samplesToCount[sample.frames] = sample.count + count @@ -68,16 +68,16 @@ import Foundation // It is less compact than Apple's MetricKit format, but contains the same data and is easier to work with struct Sample { let count: Int - let frames: [Frame] + let frames: [MXFrame] - struct Frame: Hashable { + struct MXFrame: Hashable { let binaryUUID: UUID let offsetIntoBinaryTextSegment: Int let binaryName: String? let address: UInt64 - func toSentryFrame() -> Sentry.Frame { - let frame = Sentry.Frame() + func toSentryFrame() -> Frame { + let frame = Frame() frame.package = binaryName frame.instructionAddress = sentry_formatHexAddressUInt64Swift(address) frame.imageAddress = sentry_formatHexAddressUInt64Swift(address - UInt64(offsetIntoBinaryTextSegment)) @@ -107,7 +107,7 @@ struct SentryMXFrame: Decodable { func toDebugMeta() -> [DebugMeta] { let result = DebugMeta() - result.type = SentryDebugImageType + result.type = "macho" result.debugID = binaryUUID.uuidString result.codeFile = binaryName result.imageAddress = sentry_formatHexAddressUInt64Swift(address - UInt64(offsetIntoBinaryTextSegment)) @@ -115,7 +115,7 @@ struct SentryMXFrame: Decodable { } func toSamples() -> [Sample] { - let selfFrame = Sample.Frame(binaryUUID: binaryUUID, offsetIntoBinaryTextSegment: offsetIntoBinaryTextSegment, binaryName: binaryName, address: address) + let selfFrame = Sample.MXFrame(binaryUUID: binaryUUID, offsetIntoBinaryTextSegment: offsetIntoBinaryTextSegment, binaryName: binaryName, address: address) let subframes = subFrames ?? [] let childCount = subframes.map { $0.sampleCount ?? 0 }.reduce(0, +) diff --git a/Sources/Swift/Core/MetricKit/SentryMXManager.swift b/Sources/Swift/Core/MetricKit/SentryMXManager.swift index cb856e9c9e..65022979c7 100644 --- a/Sources/Swift/Core/MetricKit/SentryMXManager.swift +++ b/Sources/Swift/Core/MetricKit/SentryMXManager.swift @@ -23,19 +23,11 @@ import MetricKit @available(watchOS, unavailable) @objcMembers @_spi(Private) public class SentryMXManager: NSObject, MXMetricManagerSubscriber { - static var shared: SentryMXManager? -// static public func test(tree: SentryMXCallStackTree) { -// let start = Date.now.addingTimeInterval(-60) -// shared?.delegate?.didReceiveHangDiagnosticCallStackTree(tree, timeStampBegin: start, timeStampEnd: Date.now) -// } - let disableCrashDiagnostics: Bool init(disableCrashDiagnostics: Bool = true) { self.disableCrashDiagnostics = disableCrashDiagnostics super.init() - - SentryMXManager.shared = self } public weak var delegate: SentryMXManagerDelegate? diff --git a/Tests/Resources/MetricKitCallstacks/per-thread-flamegraph.json b/Tests/Resources/MetricKitCallstacks/per-thread-flamegraph.json new file mode 100644 index 0000000000..916d7de2b6 --- /dev/null +++ b/Tests/Resources/MetricKitCallstacks/per-thread-flamegraph.json @@ -0,0 +1,42 @@ +{ + "callStacks": [ + { + "threadAttributed": true, + "callStackRootFrames": [ + { + "binaryUUID": "9E8D8DE6-EEC1-3199-8720-9ED68EE3F967", + "offsetIntoBinaryTextSegment": 0, + "sampleCount": 3, + "subFrames": [ + { + "binaryUUID": "CA12CAFA-91BA-3E1C-BE9C-E34DB96FE7DF", + "offsetIntoBinaryTextSegment": 0, + "sampleCount": 3, + "binaryName": "iOS-Swift", + "address": 1, + "subFrames": [ + { + "binaryUUID": "CA12CAFA-91BA-3E1C-BE9C-E34DB96FE7DF", + "offsetIntoBinaryTextSegment": 0, + "sampleCount": 1, + "binaryName": "iOS-Swift", + "address": 2 + }, + { + "binaryUUID": "CA12CAFA-91BA-3E1C-BE9C-E34DB96FE7DF", + "offsetIntoBinaryTextSegment": 0, + "sampleCount": 2, + "binaryName": "iOS-Swift", + "address": 3 + } + ] + } + ], + "binaryName": "Sentry", + "address": 0 + } + ] + } + ], + "callStackPerThread": true +} diff --git a/Tests/SentryTests/Integrations/MetricKit/SentryMXCallStackTreeTests.swift b/Tests/SentryTests/Integrations/MetricKit/SentryMXCallStackTreeTests.swift index e8dcefc57e..9c95a18ee9 100644 --- a/Tests/SentryTests/Integrations/MetricKit/SentryMXCallStackTreeTests.swift +++ b/Tests/SentryTests/Integrations/MetricKit/SentryMXCallStackTreeTests.swift @@ -45,8 +45,8 @@ final class SentryMXCallStackTreeTests: XCTestCase { let frames = try XCTUnwrap(threads[0].stacktrace).frames XCTAssertEqual(3, frames.count) XCTAssertEqual("0x0000000000000000", frames[0].instructionAddress) - XCTAssertEqual("0x0000000000000001", frames[0].instructionAddress) - XCTAssertEqual("0x0000000000000003", frames[0].instructionAddress) + XCTAssertEqual("0x0000000000000001", frames[1].instructionAddress) + XCTAssertEqual("0x0000000000000003", frames[2].instructionAddress) } func testDecodeCallStackTree_UnknownFieldsPayload() throws { diff --git a/Tests/SentryTests/Integrations/MetricKit/SentryMetricKitIntegrationTests.swift b/Tests/SentryTests/Integrations/MetricKit/SentryMetricKitIntegrationTests.swift index d889d3293d..abc22bae54 100644 --- a/Tests/SentryTests/Integrations/MetricKit/SentryMetricKitIntegrationTests.swift +++ b/Tests/SentryTests/Integrations/MetricKit/SentryMetricKitIntegrationTests.swift @@ -173,10 +173,9 @@ final class SentryMetricKitIntegrationTests: SentrySDKIntegrationTestsBase { } let invocations = client.captureEventWithScopeInvocations.invocations - XCTAssertEqual(2, client.captureEventWithScopeInvocations.count) + XCTAssertEqual(1, client.captureEventWithScopeInvocations.count) try assertEvent(event: try XCTUnwrap(invocations.first).event) - try assertEvent(event: try XCTUnwrap(invocations.element(at: 1)).event) func assertEvent(event: Event) throws { let sentryFrames = try XCTUnwrap(event.threads?.first?.stacktrace?.frames, "Event has no frames.")