Skip to content

Commit 7e0b439

Browse files
committed
Add options
1 parent 27e7221 commit 7e0b439

File tree

8 files changed

+77
-37
lines changed

8 files changed

+77
-37
lines changed

Samples/SentrySampleShared/SentrySampleShared/SentrySDKWrapper.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ public struct SentrySDKWrapper {
4343
}
4444
options.beforeCaptureScreenshot = { _ in !SentrySDKOverrides.Other.rejectScreenshots.boolValue }
4545
options.beforeCaptureViewHierarchy = { _ in !SentrySDKOverrides.Other.rejectViewHierarchy.boolValue }
46-
options.debug = false // !SentrySDKOverrides.Special.disableDebugMode.boolValue
46+
options.debug = !SentrySDKOverrides.Special.disableDebugMode.boolValue
4747

4848
#if !os(macOS) && !os(watchOS) && !os(visionOS)
4949
if #available(iOS 16.0, *), !SentrySDKOverrides.SessionReplay.disableSessionReplay.boolValue {

Sentry.xcodeproj/project.pbxproj

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1091,7 +1091,7 @@
10911091
FAB359982E05D7E90083D5E3 /* SentryEventSwiftHelper.h in Headers */ = {isa = PBXBuildFile; fileRef = FAB359972E05D7E90083D5E3 /* SentryEventSwiftHelper.h */; };
10921092
FAB3599A2E05D8080083D5E3 /* SentryEventSwiftHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = FAB359992E05D8080083D5E3 /* SentryEventSwiftHelper.m */; };
10931093
FAC62B652E15A4100003909D /* SentrySDKThreadTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC62B642E15A40C0003909D /* SentrySDKThreadTests.swift */; };
1094-
FAE2DABC2E1F55C500262307 /* RunloopObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAE2DABB2E1F55C100262307 /* RunloopObserver.swift */; };
1094+
FAE2DABC2E1F55C500262307 /* HangTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAE2DABB2E1F55C100262307 /* HangTracker.swift */; };
10951095
FAEC270E2DF3526000878871 /* SentryUserFeedback.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAEC270D2DF3526000878871 /* SentryUserFeedback.swift */; };
10961096
FAEC273D2DF3933A00878871 /* NSData+Unzip.m in Sources */ = {isa = PBXBuildFile; fileRef = FAEC273C2DF3933200878871 /* NSData+Unzip.m */; };
10971097
/* End PBXBuildFile section */
@@ -2362,7 +2362,7 @@
23622362
FAB359972E05D7E90083D5E3 /* SentryEventSwiftHelper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryEventSwiftHelper.h; path = include/SentryEventSwiftHelper.h; sourceTree = "<group>"; };
23632363
FAB359992E05D8080083D5E3 /* SentryEventSwiftHelper.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryEventSwiftHelper.m; sourceTree = "<group>"; };
23642364
FAC62B642E15A40C0003909D /* SentrySDKThreadTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentrySDKThreadTests.swift; sourceTree = "<group>"; };
2365-
FAE2DABB2E1F55C100262307 /* RunloopObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunloopObserver.swift; sourceTree = "<group>"; };
2365+
FAE2DABB2E1F55C100262307 /* HangTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HangTracker.swift; sourceTree = "<group>"; };
23662366
FAEC270D2DF3526000878871 /* SentryUserFeedback.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryUserFeedback.swift; sourceTree = "<group>"; };
23672367
FAEC273C2DF3933200878871 /* NSData+Unzip.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSData+Unzip.m"; sourceTree = "<group>"; };
23682368
FAEC273E2DF393E000878871 /* NSData+Unzip.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSData+Unzip.h"; sourceTree = "<group>"; };
@@ -4165,7 +4165,7 @@
41654165
D800942328F82E8D005D3943 /* Swift */ = {
41664166
isa = PBXGroup;
41674167
children = (
4168-
FAE2DABB2E1F55C100262307 /* RunloopObserver.swift */,
4168+
FAE2DABB2E1F55C100262307 /* HangTracker.swift */,
41694169
FA67DCF32DDBD4EA00896B02 /* Core */,
41704170
D8CAC02D2BA0663E00E38F34 /* Integrations */,
41714171
621D9F2D2B9B030E003D94DE /* Helper */,
@@ -5543,7 +5543,7 @@
55435543
63FE70FD20DA4C1000CDBAE8 /* SentryCrashCachedData.c in Sources */,
55445544
A8F17B2E2901765900990B25 /* SentryRequest.m in Sources */,
55455545
7BE1E33424F7E3CB009D3AD0 /* SentryMigrateSessionInit.m in Sources */,
5546-
FAE2DABC2E1F55C500262307 /* RunloopObserver.swift in Sources */,
5546+
FAE2DABC2E1F55C500262307 /* HangTracker.swift in Sources */,
55475547
D80299502BA83A88000F0081 /* SentryPixelBuffer.swift in Sources */,
55485548
15E0A8F22411A45A00F044E3 /* SentrySession.m in Sources */,
55495549
844EDCE62947DC3100C86F34 /* SentryNSTimerFactory.m in Sources */,

Sources/Sentry/SentryDependencyContainer.m

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -262,14 +262,14 @@ - (SentryCrash *)crashReporter SENTRY_THREAD_SANITIZER_DOUBLE_CHECKED_LOCK
262262
[[SentryCrash alloc] initWithBasePath:SentrySDK.options.cacheDirectoryPath]);
263263
}
264264

265-
- (RunLoopObserverObjcBridge *)observer SENTRY_THREAD_SANITIZER_DOUBLE_CHECKED_LOCK
265+
- (HangTrackerObjcBridge *)hangTracker SENTRY_THREAD_SANITIZER_DOUBLE_CHECKED_LOCK
266266
{
267-
SENTRY_LAZY_INIT(_observer,
268-
[[RunLoopObserverObjcBridge alloc] initWithDateProvider:self.dateProvider
269-
threadInspector:self.threadInspector
270-
debugImageCache:self.debugImageProvider
271-
fileManager:self.fileManager
272-
crashWrapper:self.crashWrapper]);
267+
SENTRY_LAZY_INIT(_hangTracker,
268+
[[HangTrackerObjcBridge alloc] initWithDateProvider:self.dateProvider
269+
threadInspector:self.threadInspector
270+
debugImageCache:self.debugImageProvider
271+
fileManager:self.fileManager
272+
crashWrapper:self.crashWrapper]);
273273
}
274274

275275
- (id<SentryANRTracker>)getANRTracker:(NSTimeInterval)timeout

Sources/Sentry/SentrySDK.m

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,6 @@ + (void)startWithOptions:(SentryOptions *)options
217217
NSLog(@"[SENTRY] [WARNING] SentrySDK not started. Running from Xcode preview.");
218218
return;
219219
}
220-
[SentryDependencyContainer.sharedInstance.observer start];
221220

222221
[SentrySDKLogSupport configure:options.debug diagnosticLevel:options.diagnosticLevel];
223222

@@ -265,6 +264,10 @@ + (void)startWithOptions:(SentryOptions *)options
265264
[SentryCrashWrapper.sharedInstance startBinaryImageCache];
266265
[SentryDependencyContainer.sharedInstance.binaryImageCache start];
267266

267+
if (options.experimental.enableRunLoopObserverAppHangs) {
268+
[SentryDependencyContainer.sharedInstance.hangTracker start];
269+
}
270+
268271
[SentrySDK installIntegrations];
269272

270273
#if SENTRY_TARGET_PROFILING_SUPPORTED
@@ -580,20 +583,28 @@ + (void)reportFullyDisplayed
580583

581584
+ (void)pauseAppHangTracking
582585
{
583-
SentryANRTrackingIntegration *anrTrackingIntegration
584-
= (SentryANRTrackingIntegration *)[SentrySDK.currentHub
585-
getInstalledIntegration:[SentryANRTrackingIntegration class]];
586+
if (currentHub.client.options.experimental.enableRunLoopObserverAppHangs) {
587+
[SentryDependencyContainer.sharedInstance.hangTracker stop];
588+
} else {
589+
SentryANRTrackingIntegration *anrTrackingIntegration
590+
= (SentryANRTrackingIntegration *)[SentrySDK.currentHub
591+
getInstalledIntegration:[SentryANRTrackingIntegration class]];
586592

587-
[anrTrackingIntegration pauseAppHangTracking];
593+
[anrTrackingIntegration pauseAppHangTracking];
594+
}
588595
}
589596

590597
+ (void)resumeAppHangTracking
591598
{
592-
SentryANRTrackingIntegration *anrTrackingIntegration
593-
= (SentryANRTrackingIntegration *)[SentrySDK.currentHub
594-
getInstalledIntegration:[SentryANRTrackingIntegration class]];
599+
if (currentHub.client.options.experimental.enableRunLoopObserverAppHangs) {
600+
[SentryDependencyContainer.sharedInstance.hangTracker start];
601+
} else {
602+
SentryANRTrackingIntegration *anrTrackingIntegration
603+
= (SentryANRTrackingIntegration *)[SentrySDK.currentHub
604+
getInstalledIntegration:[SentryANRTrackingIntegration class]];
595605

596-
[anrTrackingIntegration resumeAppHangTracking];
606+
[anrTrackingIntegration resumeAppHangTracking];
607+
}
597608
}
598609

599610
+ (void)flush:(NSTimeInterval)timeout

Sources/Sentry/include/HybridPublic/SentryDependencyContainer.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
@class SentryUIViewControllerPerformanceTracker;
4747
@class SentryWatchdogTerminationScopeObserver;
4848
@class SentryWatchdogTerminationAttributesProcessor;
49-
@class RunLoopObserverObjcBridge;
49+
@class HangTrackerObjcBridge;
5050
@class SentryWatchdogTerminationBreadcrumbProcessor;
5151
#endif // SENTRY_UIKIT_AVAILABLE
5252

@@ -123,7 +123,7 @@ SENTRY_NO_INIT
123123
@property (nonatomic, strong) id<SentryDispatchQueueProviderProtocol> dispatchQueueProvider;
124124
@property (nonatomic, strong) SentryNSTimerFactory *timerFactory;
125125

126-
@property (nonatomic, strong) RunLoopObserverObjcBridge *observer;
126+
@property (nonatomic, strong) HangTrackerObjcBridge *hangTracker;
127127

128128
@property (nonatomic, strong) SentrySwizzleWrapper *swizzleWrapper;
129129
#if SENTRY_UIKIT_AVAILABLE

Sources/Sentry/include/SentryPrivate.h

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,5 @@
2828
#import "SentrySession.h"
2929
#import "SentrySpanDataKey.h"
3030
#import "SentrySpanOperation.h"
31-
#import "SentryThreadInspector.h"
3231
#import "SentryTraceHeader.h"
3332
#import "SentryTraceOrigin.h"

Sources/Swift/RunloopObserver.swift renamed to Sources/Swift/HangTracker.swift

Lines changed: 41 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,21 @@
33
import UIKit
44
#endif
55

6-
final class RunLoopObserver {
6+
// A hang is parameterized by the its minimum duration, HangDuration, and is defined by a period of time lasting longer than HangDuration*2 when the delay
7+
// of all frames started during that time sums to more than HangDuration. If there is only one frame during this time that is delayed then it is a "fullyBlocking"
8+
// hang, otherwise it is a "nonFullyBlocking" hang.
9+
//
10+
// We measure late frames using a runloop observer. Any frame that is more than 50% delayed has its stacktrace sampled once, when we first detect it is delayed.
11+
// After the hang ends (or the app restarts in the case of fatal hangs) the most recently recorded stacktrace is used to report the hang. Since a hang is not one
12+
// stacktrace, like a crash, but rather a period of time, it would make sense to talk about a flamegraph sampled during the hang but the API does not support this.
13+
// Note that stacktraces are always sampled from a background thread, since it’s the main thread that gets sampled.
14+
//
15+
// There are two ways we trigger saving hang data. One is on the main thread, when the hang is over. When each frame finishes rendering we collect its delay and
16+
// if the sum of delays during the last HangDuration*2 is greather than the threshold, we report the last stack trace as a hang. The other way is on a background
17+
// thread. Once a frame is delayed more than 50% we sample the thread and start tracking it. If the total delay becomes more than our threshold the event
18+
// is recorded. If the hang never ends (thus transfering us back to the previous main thread case) then the hang is saved on the filesystem and
19+
// sent as a fatal hang the next time the app is launched.
20+
final class HangTracker {
721

822
static let SentryANRMechanismDataAppHangDuration = "app_hang_duration"
923

@@ -40,7 +54,7 @@ final class RunLoopObserver {
4054
#endif
4155
expectedFrameDuration = 1.0 / maxFPS
4256
thresholdForFrameStacktrace = expectedFrameDuration * 0.5
43-
captureStoredAppHang()
57+
captureStoredAppHang()
4458
}
4559

4660
// This queue is used to detect main thread hangs, they need to be detected on a background thread
@@ -52,6 +66,7 @@ final class RunLoopObserver {
5266

5367
// MARK: Main queue
5468

69+
private var observer: CFRunLoopObserver?
5570
private var semaphore = DispatchSemaphore(value: 0)
5671
private var lastFrameTime: TimeInterval
5772
private var running = false
@@ -79,9 +94,16 @@ final class RunLoopObserver {
7994
fatalError()
8095
}
8196
}
97+
self.observer = observer
8298
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, .commonModes)
8399
}
84100

101+
func stop() {
102+
guard let observer else { return }
103+
104+
CFRunLoopRemoveObserver(CFRunLoopGetMain(), observer, .commonModes)
105+
}
106+
85107
private func updateFrameStatistics() -> Double {
86108
dispatchPrecondition(condition: .onQueue(.main))
87109

@@ -103,19 +125,19 @@ final class RunLoopObserver {
103125
let totalTime = frameStatistics.map({ $0.delayTime }).reduce(0, +)
104126
let type: SentryANRType = frameStatistics.count > 0 ? .nonFullyBlocking : .fullyBlocking
105127
if totalTime > minHangTime {
106-
print("[HANG] Hang detected \(totalTime)")
128+
SentrySDKLog.debug("Detected app hang for \(totalTime) seconds")
107129
let maxTime = max(maxHangTime ?? 0, totalTime)
108130
maxHangTime = maxTime
109131
// Update on disk hang
110132
queue.async { [weak self] in
111133
guard let self, let threads = threads, !threads.isEmpty else { return }
112-
let event = makeEvent(duration: maxTime, threads: threads, type: type)
134+
let event = makeEvent(duration: maxTime, threads: threads, type: type, addMechanismData: true)
113135
fileManager.storeAppHang(event)
114136
}
115137
} else {
116138
if let maxHangTime {
117139
// The hang has ended
118-
print("[HANG] Hang reporting \(maxHangTime)")
140+
SentrySDKLog.debug("Reporting app hang with \(maxHangTime) seconds")
119141
// Note: A non fully blocking hang always has multiple stacktraces
120142
// because it is composed of multpile delayed frames. Each delayed frame has a stacktrace.
121143
// We only support sending one stacktrace per event so we take the most recent one.
@@ -125,7 +147,7 @@ final class RunLoopObserver {
125147
// overal hang event with just one stacktrace.
126148
queue.async { [weak self] in
127149
guard let self, let threads = threads, !threads.isEmpty else { return }
128-
let event = makeEvent(duration: maxHangTime, threads: threads, type: type)
150+
let event = makeEvent(duration: maxHangTime, threads: threads, type: type, addMechanismData: false)
129151
SentrySDK.capture(event: event)
130152
}
131153
}
@@ -198,7 +220,7 @@ final class RunLoopObserver {
198220
dispatchPrecondition(condition: .onQueue(queue))
199221

200222
if isStarting {
201-
// A hang lasts a while, but we only support showing the stacktrace when it was first detected
223+
// A hang lasts a while, we show the stacktrace when it was first detected
202224
threads = threadInspector.getCurrentThreadsWithStackTrace()
203225
threads?.forEach { $0.current = false }
204226
threads?[0].current = true
@@ -208,13 +230,13 @@ final class RunLoopObserver {
208230
if let threads, !threads.isEmpty, duration > minHangTime {
209231
// Hangs detected in the background are always fully blocking
210232
// Otherwise we'd be detecting them on the main thread.
211-
fileManager.storeAppHang(makeEvent(duration: duration, threads: threads, type: .fullyBlocking))
233+
fileManager.storeAppHang(makeEvent(duration: duration, threads: threads, type: .fullyBlocking, addMechanismData: true))
212234
}
213235

214236
}
215237

216238
// Safe to call from any thread
217-
private func makeEvent(duration: TimeInterval, threads: [SentryThread], type: SentryANRType) -> Event {
239+
private func makeEvent(duration: TimeInterval, threads: [SentryThread], type: SentryANRType, addMechanismData: Bool) -> Event {
218240
let event = Event()
219241
SentryLevelBridge.setBreadcrumbLevelOn(event, level: SentryLevel.error.rawValue)
220242
let exceptionType = SentryAppHangTypeMapper.getExceptionType(anrType: type)
@@ -223,7 +245,9 @@ final class RunLoopObserver {
223245
// We only temporarily store the app hang duration info, so we can change the error message
224246
// when either sending a normal or fatal app hang event. Otherwise, we would have to rely on
225247
// string parsing to retrieve the app hang duration info from the error message.
226-
mechanism.data = [Self.SentryANRMechanismDataAppHangDuration: "\(duration) seconds"]
248+
if addMechanismData {
249+
mechanism.data = [Self.SentryANRMechanismDataAppHangDuration: "\(duration) seconds"]
250+
}
227251
exception.mechanism = mechanism
228252
exception.stacktrace = threads[0].stacktrace
229253
exception.stacktrace?.snapshot = true
@@ -237,17 +261,17 @@ final class RunLoopObserver {
237261
}
238262

239263
@objc
240-
@_spi(Private) public final class RunLoopObserverObjcBridge: NSObject {
264+
@_spi(Private) public final class HangTrackerObjcBridge: NSObject {
241265

242-
private let observer: RunLoopObserver
266+
private let observer: HangTracker
243267

244268
@objc public init(
245269
dateProvider: SentryCurrentDateProvider,
246270
threadInspector: ThreadInspector,
247271
debugImageCache: DebugImageCache,
248272
fileManager: SentryFileManager,
249273
crashWrapper: CrashWrapper) {
250-
observer = RunLoopObserver(
274+
observer = HangTracker(
251275
dateProvider: dateProvider,
252276
threadInspector: threadInspector,
253277
debugImageCache: debugImageCache,
@@ -259,6 +283,10 @@ final class RunLoopObserver {
259283
@objc public func start() {
260284
observer.start()
261285
}
286+
287+
@objc public func stop() {
288+
observer.stop()
289+
}
262290
}
263291

264292
@objc @_spi(Private) public protocol ThreadInspector {

Sources/Swift/SentryExperimentalOptions.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ public class SentryExperimentalOptions: NSObject {
3030
* - Experiment: This is an experimental feature and is therefore disabled by default. We'll enable it by default in a future major release.
3131
*/
3232
public var enableUnhandledCPPExceptionsV2 = false
33+
34+
public var enableRunLoopObserverAppHangs = false
3335

3436
@_spi(Private) public func validateOptions(_ options: [String: Any]?) {
3537
}

0 commit comments

Comments
 (0)