Skip to content

Commit de85790

Browse files
committed
Add options
1 parent 7f1cf81 commit de85790

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
@@ -1098,7 +1098,7 @@
10981098
FAB359982E05D7E90083D5E3 /* SentryEventSwiftHelper.h in Headers */ = {isa = PBXBuildFile; fileRef = FAB359972E05D7E90083D5E3 /* SentryEventSwiftHelper.h */; };
10991099
FAB3599A2E05D8080083D5E3 /* SentryEventSwiftHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = FAB359992E05D8080083D5E3 /* SentryEventSwiftHelper.m */; };
11001100
FAC62B652E15A4100003909D /* SentrySDKThreadTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC62B642E15A40C0003909D /* SentrySDKThreadTests.swift */; };
1101-
FAE2DABC2E1F55C500262307 /* RunloopObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAE2DABB2E1F55C100262307 /* RunloopObserver.swift */; };
1101+
FAE2DABC2E1F55C500262307 /* HangTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAE2DABB2E1F55C100262307 /* HangTracker.swift */; };
11021102
FAEC270E2DF3526000878871 /* SentryUserFeedback.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAEC270D2DF3526000878871 /* SentryUserFeedback.swift */; };
11031103
FAEC273D2DF3933A00878871 /* NSData+Unzip.m in Sources */ = {isa = PBXBuildFile; fileRef = FAEC273C2DF3933200878871 /* NSData+Unzip.m */; };
11041104
/* End PBXBuildFile section */
@@ -2377,7 +2377,7 @@
23772377
FAB359972E05D7E90083D5E3 /* SentryEventSwiftHelper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryEventSwiftHelper.h; path = include/SentryEventSwiftHelper.h; sourceTree = "<group>"; };
23782378
FAB359992E05D8080083D5E3 /* SentryEventSwiftHelper.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryEventSwiftHelper.m; sourceTree = "<group>"; };
23792379
FAC62B642E15A40C0003909D /* SentrySDKThreadTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentrySDKThreadTests.swift; sourceTree = "<group>"; };
2380-
FAE2DABB2E1F55C100262307 /* RunloopObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunloopObserver.swift; sourceTree = "<group>"; };
2380+
FAE2DABB2E1F55C100262307 /* HangTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HangTracker.swift; sourceTree = "<group>"; };
23812381
FAEC270D2DF3526000878871 /* SentryUserFeedback.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryUserFeedback.swift; sourceTree = "<group>"; };
23822382
FAEC273C2DF3933200878871 /* NSData+Unzip.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSData+Unzip.m"; sourceTree = "<group>"; };
23832383
FAEC273E2DF393E000878871 /* NSData+Unzip.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSData+Unzip.h"; sourceTree = "<group>"; };
@@ -4184,7 +4184,7 @@
41844184
D800942328F82E8D005D3943 /* Swift */ = {
41854185
isa = PBXGroup;
41864186
children = (
4187-
FAE2DABB2E1F55C100262307 /* RunloopObserver.swift */,
4187+
FAE2DABB2E1F55C100262307 /* HangTracker.swift */,
41884188
FA67DCF32DDBD4EA00896B02 /* Core */,
41894189
D8CAC02D2BA0663E00E38F34 /* Integrations */,
41904190
621D9F2D2B9B030E003D94DE /* Helper */,
@@ -5569,7 +5569,7 @@
55695569
63FE70FD20DA4C1000CDBAE8 /* SentryCrashCachedData.c in Sources */,
55705570
A8F17B2E2901765900990B25 /* SentryRequest.m in Sources */,
55715571
7BE1E33424F7E3CB009D3AD0 /* SentryMigrateSessionInit.m in Sources */,
5572-
FAE2DABC2E1F55C500262307 /* RunloopObserver.swift in Sources */,
5572+
FAE2DABC2E1F55C500262307 /* HangTracker.swift in Sources */,
55735573
D80299502BA83A88000F0081 /* SentryPixelBuffer.swift in Sources */,
55745574
15E0A8F22411A45A00F044E3 /* SentrySession.m in Sources */,
55755575
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
@@ -240,7 +240,6 @@ + (void)startWithOptions:(SentryOptions *)options
240240
NSLog(@"[SENTRY] [WARNING] SentrySDK not started. Running from Xcode preview.");
241241
return;
242242
}
243-
[SentryDependencyContainer.sharedInstance.observer start];
244243

245244
[SentrySDKLogSupport configure:options.debug diagnosticLevel:options.diagnosticLevel];
246245

@@ -288,6 +287,10 @@ + (void)startWithOptions:(SentryOptions *)options
288287
[SentryCrashWrapper.sharedInstance startBinaryImageCache];
289288
[SentryDependencyContainer.sharedInstance.binaryImageCache start];
290289

290+
if (options.experimental.enableRunLoopObserverAppHangs) {
291+
[SentryDependencyContainer.sharedInstance.hangTracker start];
292+
}
293+
291294
[SentrySDK installIntegrations];
292295

293296
#if SENTRY_TARGET_PROFILING_SUPPORTED
@@ -603,20 +606,28 @@ + (void)reportFullyDisplayed
603606

604607
+ (void)pauseAppHangTracking
605608
{
606-
SentryANRTrackingIntegration *anrTrackingIntegration
607-
= (SentryANRTrackingIntegration *)[SentrySDK.currentHub
608-
getInstalledIntegration:[SentryANRTrackingIntegration class]];
609+
if (currentHub.client.options.experimental.enableRunLoopObserverAppHangs) {
610+
[SentryDependencyContainer.sharedInstance.hangTracker stop];
611+
} else {
612+
SentryANRTrackingIntegration *anrTrackingIntegration
613+
= (SentryANRTrackingIntegration *)[SentrySDK.currentHub
614+
getInstalledIntegration:[SentryANRTrackingIntegration class]];
609615

610-
[anrTrackingIntegration pauseAppHangTracking];
616+
[anrTrackingIntegration pauseAppHangTracking];
617+
}
611618
}
612619

613620
+ (void)resumeAppHangTracking
614621
{
615-
SentryANRTrackingIntegration *anrTrackingIntegration
616-
= (SentryANRTrackingIntegration *)[SentrySDK.currentHub
617-
getInstalledIntegration:[SentryANRTrackingIntegration class]];
622+
if (currentHub.client.options.experimental.enableRunLoopObserverAppHangs) {
623+
[SentryDependencyContainer.sharedInstance.hangTracker start];
624+
} else {
625+
SentryANRTrackingIntegration *anrTrackingIntegration
626+
= (SentryANRTrackingIntegration *)[SentrySDK.currentHub
627+
getInstalledIntegration:[SentryANRTrackingIntegration class]];
618628

619-
[anrTrackingIntegration resumeAppHangTracking];
629+
[anrTrackingIntegration resumeAppHangTracking];
630+
}
620631
}
621632

622633
+ (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
@@ -29,6 +29,5 @@
2929
#import "SentrySession.h"
3030
#import "SentrySpanDataKey.h"
3131
#import "SentrySpanOperation.h"
32-
#import "SentryThreadInspector.h"
3332
#import "SentryTraceHeader.h"
3433
#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
@@ -31,6 +31,8 @@ public class SentryExperimentalOptions: NSObject {
3131
*/
3232
public var enableUnhandledCPPExceptionsV2 = false
3333

34+
public var enableRunLoopObserverAppHangs = false
35+
3436
/**
3537
* Logs are considered beta.
3638
*/

0 commit comments

Comments
 (0)