Skip to content

Commit a4768e3

Browse files
committed
New runloop observer
1 parent ae7be93 commit a4768e3

File tree

5 files changed

+199
-6
lines changed

5 files changed

+199
-6
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 = !SentrySDKOverrides.Special.disableDebugMode.boolValue
46+
options.debug = false // !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: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1029,9 +1029,9 @@
10291029
F452438C2DE65BC0003E8F50 /* SentryUseNSExceptionCallstackWrapper.h in Headers */ = {isa = PBXBuildFile; fileRef = F452438B2DE65BC0003E8F50 /* SentryUseNSExceptionCallstackWrapper.h */; };
10301030
F458D1132E180BB00028273E /* SentryFileManagerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = F458D1122E180BB00028273E /* SentryFileManagerProtocol.swift */; };
10311031
F458D1152E1869AD0028273E /* SentryScopePersistentStore+String.swift in Sources */ = {isa = PBXBuildFile; fileRef = F458D1142E1869AD0028273E /* SentryScopePersistentStore+String.swift */; };
1032-
F46DA6C32E1DBCA000DF6E3B /* SentryScopePersistentStore+Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = F46DA6C22E1DBCA000DF6E3B /* SentryScopePersistentStore+Helper.swift */; };
10331032
F458D1172E186DF20028273E /* SentryScopePersistentStore+Fingerprint.swift in Sources */ = {isa = PBXBuildFile; fileRef = F458D1162E186DF20028273E /* SentryScopePersistentStore+Fingerprint.swift */; };
10341033
F458D1192E186E000028273E /* SentryScopePersistentStore+Extras.swift in Sources */ = {isa = PBXBuildFile; fileRef = F458D1182E186E000028273E /* SentryScopePersistentStore+Extras.swift */; };
1034+
F46DA6C32E1DBCA000DF6E3B /* SentryScopePersistentStore+Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = F46DA6C22E1DBCA000DF6E3B /* SentryScopePersistentStore+Helper.swift */; };
10351035
F49D41982DEA27AF00D9244E /* SentryUseNSExceptionCallstackWrapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F49D41972DEA27AF00D9244E /* SentryUseNSExceptionCallstackWrapperTests.swift */; };
10361036
F49D419A2DEA2FB000D9244E /* SentryCrashExceptionApplicationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F49D41992DEA2FB000D9244E /* SentryCrashExceptionApplicationTests.swift */; };
10371037
F49D419C2DEA30C300D9244E /* SentryCrashExceptionApplicationHelper.h in Headers */ = {isa = PBXBuildFile; fileRef = F49D419B2DEA30B800D9244E /* SentryCrashExceptionApplicationHelper.h */; };
@@ -1091,6 +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 */; };
10941095
FAEC270E2DF3526000878871 /* SentryUserFeedback.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAEC270D2DF3526000878871 /* SentryUserFeedback.swift */; };
10951096
FAEC273D2DF3933A00878871 /* NSData+Unzip.m in Sources */ = {isa = PBXBuildFile; fileRef = FAEC273C2DF3933200878871 /* NSData+Unzip.m */; };
10961097
/* End PBXBuildFile section */
@@ -2299,9 +2300,9 @@
22992300
F452438B2DE65BC0003E8F50 /* SentryUseNSExceptionCallstackWrapper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryUseNSExceptionCallstackWrapper.h; path = include/SentryUseNSExceptionCallstackWrapper.h; sourceTree = "<group>"; };
23002301
F458D1122E180BB00028273E /* SentryFileManagerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryFileManagerProtocol.swift; sourceTree = "<group>"; };
23012302
F458D1142E1869AD0028273E /* SentryScopePersistentStore+String.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SentryScopePersistentStore+String.swift"; sourceTree = "<group>"; };
2302-
F46DA6C22E1DBCA000DF6E3B /* SentryScopePersistentStore+Helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SentryScopePersistentStore+Helper.swift"; sourceTree = "<group>"; };
23032303
F458D1162E186DF20028273E /* SentryScopePersistentStore+Fingerprint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SentryScopePersistentStore+Fingerprint.swift"; sourceTree = "<group>"; };
23042304
F458D1182E186E000028273E /* SentryScopePersistentStore+Extras.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SentryScopePersistentStore+Extras.swift"; sourceTree = "<group>"; };
2305+
F46DA6C22E1DBCA000DF6E3B /* SentryScopePersistentStore+Helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SentryScopePersistentStore+Helper.swift"; sourceTree = "<group>"; };
23052306
F49D41972DEA27AF00D9244E /* SentryUseNSExceptionCallstackWrapperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryUseNSExceptionCallstackWrapperTests.swift; sourceTree = "<group>"; };
23062307
F49D41992DEA2FB000D9244E /* SentryCrashExceptionApplicationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryCrashExceptionApplicationTests.swift; sourceTree = "<group>"; };
23072308
F49D419B2DEA30B800D9244E /* SentryCrashExceptionApplicationHelper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryCrashExceptionApplicationHelper.h; path = include/SentryCrashExceptionApplicationHelper.h; sourceTree = "<group>"; };
@@ -2361,6 +2362,7 @@
23612362
FAB359972E05D7E90083D5E3 /* SentryEventSwiftHelper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryEventSwiftHelper.h; path = include/SentryEventSwiftHelper.h; sourceTree = "<group>"; };
23622363
FAB359992E05D8080083D5E3 /* SentryEventSwiftHelper.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryEventSwiftHelper.m; sourceTree = "<group>"; };
23632364
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>"; };
23642366
FAEC270D2DF3526000878871 /* SentryUserFeedback.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryUserFeedback.swift; sourceTree = "<group>"; };
23652367
FAEC273C2DF3933200878871 /* NSData+Unzip.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSData+Unzip.m"; sourceTree = "<group>"; };
23662368
FAEC273E2DF393E000878871 /* NSData+Unzip.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSData+Unzip.h"; sourceTree = "<group>"; };
@@ -4058,7 +4060,6 @@
40584060
F41362102E1C55AF00B84443 /* SentryScopePersistentStore+Tags.swift */,
40594061
F41362122E1C566100B84443 /* SentryScopePersistentStore+User.swift */,
40604062
F46DA6C22E1DBCA000DF6E3B /* SentryScopePersistentStore+Helper.swift */,
4061-
F458D11A2E186E0D0028273E /* SentryScopePersistentStore+Level.swift */,
40624063
F458D1182E186E000028273E /* SentryScopePersistentStore+Extras.swift */,
40634064
F458D1162E186DF20028273E /* SentryScopePersistentStore+Fingerprint.swift */,
40644065
);
@@ -4164,6 +4165,7 @@
41644165
D800942328F82E8D005D3943 /* Swift */ = {
41654166
isa = PBXGroup;
41664167
children = (
4168+
FAE2DABB2E1F55C100262307 /* RunloopObserver.swift */,
41674169
FA67DCF32DDBD4EA00896B02 /* Core */,
41684170
D8CAC02D2BA0663E00E38F34 /* Integrations */,
41694171
621D9F2D2B9B030E003D94DE /* Helper */,
@@ -5541,6 +5543,7 @@
55415543
63FE70FD20DA4C1000CDBAE8 /* SentryCrashCachedData.c in Sources */,
55425544
A8F17B2E2901765900990B25 /* SentryRequest.m in Sources */,
55435545
7BE1E33424F7E3CB009D3AD0 /* SentryMigrateSessionInit.m in Sources */,
5546+
FAE2DABC2E1F55C500262307 /* RunloopObserver.swift in Sources */,
55445547
D80299502BA83A88000F0081 /* SentryPixelBuffer.swift in Sources */,
55455548
15E0A8F22411A45A00F044E3 /* SentrySession.m in Sources */,
55465549
844EDCE62947DC3100C86F34 /* SentryNSTimerFactory.m in Sources */,
@@ -5665,7 +5668,6 @@
56655668
D85852BA27EDDC5900C6D8AE /* SentryUIApplication.m in Sources */,
56665669
FAB3599A2E05D8080083D5E3 /* SentryEventSwiftHelper.m in Sources */,
56675670
F41362112E1C55AF00B84443 /* SentryScopePersistentStore+Tags.swift in Sources */,
5668-
F458D11B2E186E0D0028273E /* SentryScopePersistentStore+Level.swift in Sources */,
56695671
7B4E375F258231FC00059C93 /* SentryAttachment.m in Sources */,
56705672
636085141ED47BE600E8599E /* SentryFileManager.m in Sources */,
56715673
63FE710B20DA4C1000CDBAE8 /* SentryCrashMach.c in Sources */,

Sources/Sentry/SentryDependencyContainer.m

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,22 @@ @interface SentryDependencyContainer ()
8585

8686
@property (nonatomic, strong) id<SentryANRTracker> anrTracker;
8787

88+
@property (nonatomic, strong) SentryDependencyScope *scope;
89+
90+
@property (nonatomic, strong) RunLoopObserverObjcBridge *observer;
91+
92+
@end
93+
94+
@interface SentryDebugImageProviderWorkaround: NSObject <DebugImageCache>
95+
96+
@end
97+
98+
@implementation SentryDebugImageProviderWorkaround
99+
100+
- (NSArray<SentryDebugMeta *> * _Nonnull)getDebugImagesFromCacheFor:(NSArray<SentryThread *> * _Nullable)threads {
101+
return [SentryDependencyContainer.sharedInstance.debugImageProvider getDebugImagesFromCacheForThreads:threads];
102+
}
103+
88104
@end
89105

90106
@implementation SentryDependencyContainer
@@ -163,7 +179,9 @@ - (instancetype)init
163179
_random = [[SentryRandom alloc] init];
164180
_threadWrapper = [[SentryThreadWrapper alloc] init];
165181
_binaryImageCache = [[SentryBinaryImageCache alloc] init];
166-
_dateProvider = [[SentryDefaultCurrentDateProvider alloc] init];
182+
_scope = [[SentryDependencyScope alloc] initWithOptions:SentrySDK.options debugImageCache:[[SentryDebugImageProviderWorkaround alloc] init]];
183+
_observer = [[RunLoopObserverObjcBridge alloc] initWithDependencies:_scope];
184+
_dateProvider = _scope.dateProvider;
167185

168186
_notificationCenterWrapper = [NSNotificationCenter defaultCenter];
169187
#if SENTRY_HAS_UIKIT

Sources/Sentry/include/SentryPrivate.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,4 @@
3030
#import "SentrySpanOperation.h"
3131
#import "SentryTraceHeader.h"
3232
#import "SentryTraceOrigin.h"
33+
#import "SentryThreadInspector.h"

Sources/Swift/RunloopObserver.swift

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
@_implementationOnly import _SentryPrivate
2+
3+
protocol ThreadInspector {
4+
func getCurrentThreadsWithStackTrace() -> [SentryThread]
5+
}
6+
7+
@objc @_spi(Private) public protocol DebugImageCache {
8+
func getDebugImagesFromCache(for threads: [SentryThread]?) -> [DebugMeta]
9+
}
10+
11+
protocol ThreadInspectorProviding {
12+
var threadInspector: ThreadInspector { get }
13+
}
14+
15+
protocol SentryCurrentDateProviding {
16+
var dateProvider: SentryCurrentDateProvider { get }
17+
}
18+
19+
protocol DebugImageCacheProviding {
20+
var debugImageCache: DebugImageCache { get }
21+
}
22+
23+
typealias RunLoopObserverDependencies = SentryCurrentDateProviding & ThreadInspectorProviding & DebugImageCacheProviding
24+
25+
final class RunloopObserver {
26+
let dependencies: RunLoopObserverDependencies
27+
init(dependencies: RunLoopObserverDependencies, minHangTime: TimeInterval) {
28+
self.dependencies = dependencies
29+
self.lastFrameTime = 0
30+
self.minHangTime = minHangTime
31+
}
32+
33+
// This queue is used to detect main thread hangs, they need to be detected on a background thread
34+
// since the main thread is hanging.
35+
let queue = DispatchQueue(label: "io.sentry.runloop-observer-checker")
36+
var semaphore = DispatchSemaphore(value: 0)
37+
let minHangTime: TimeInterval
38+
39+
// MARK: Main queue
40+
41+
var lastFrameTime: TimeInterval
42+
var running = false
43+
var frameStatistics = [(startTime: TimeInterval, delayTime: TimeInterval)]()
44+
45+
func start() {
46+
let observer = CFRunLoopObserverCreateWithHandler(nil, CFRunLoopActivity.beforeWaiting.rawValue | CFRunLoopActivity.afterWaiting.rawValue | CFRunLoopActivity.beforeSources.rawValue, true, CFIndex(INT_MAX)) { [weak self] _, activity in
47+
guard let self else { return }
48+
49+
switch activity {
50+
case .beforeWaiting:
51+
updateFrameStatistics()
52+
running = false
53+
case .afterWaiting, .beforeSources:
54+
updateFrameStatistics()
55+
semaphore = DispatchSemaphore(value: 0)
56+
running = true
57+
let timeout = DispatchTime.now() + DispatchTimeInterval.milliseconds(Int(minHangTime * 1_000))
58+
let localSemaphore = semaphore
59+
queue.async { [weak self] in
60+
let result = localSemaphore.wait(timeout: timeout)
61+
switch result {
62+
case .timedOut:
63+
print("[HANG] Timeout, hang detected")
64+
self?.hangStarted()
65+
case .success:
66+
break
67+
}
68+
}
69+
// print("[HANG] Woken up")
70+
default:
71+
fatalError()
72+
}
73+
}
74+
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, .commonModes)
75+
}
76+
77+
func updateFrameStatistics() {
78+
dispatchPrecondition(condition: .onQueue(.main))
79+
80+
let currentTime = dependencies.dateProvider.systemUptime()
81+
defer {
82+
lastFrameTime = currentTime
83+
}
84+
// Only consider frames that were within 2x the minHangTime
85+
frameStatistics = frameStatistics.filter { $0.startTime > currentTime - minHangTime * 2 }
86+
87+
semaphore.signal()
88+
if running {
89+
let expectedFrameTime = lastFrameTime + 1.0 / 60.0
90+
let frameDelay = currentTime - expectedFrameTime
91+
if frameDelay > minHangTime {
92+
print("[HANG] Hang detected \(frameDelay)s")
93+
queue.async { [weak self] in
94+
self?.recordHang(duration: frameDelay)
95+
}
96+
frameStatistics.removeAll()
97+
} else if frameDelay > 0 {
98+
frameStatistics.append((startTime: lastFrameTime, delayTime: frameDelay))
99+
}
100+
let totalTime = frameStatistics.map({ $0.delayTime }).reduce(0, +)
101+
if totalTime > minHangTime * 0.99 {
102+
print("[HANG] Detected non-blocking hang")
103+
// TODO: Keep on recording until blocking period is over (or some max time)
104+
// TODO: Get stacktraces from when the individual blocking events occured
105+
// TODO: Send each event
106+
}
107+
}
108+
}
109+
110+
// MARK: Background queue
111+
112+
var threads: [SentryThread]?
113+
114+
func hangStarted() {
115+
dispatchPrecondition(condition: .onQueue(queue))
116+
117+
// TODO: Write to disk to record fatal hangs on app start
118+
119+
// Record threads at start of hang
120+
threads = dependencies.threadInspector.getCurrentThreadsWithStackTrace()
121+
}
122+
123+
func recordHang(duration: TimeInterval) {
124+
dispatchPrecondition(condition: .onQueue(queue))
125+
126+
guard let threads, !threads.isEmpty else {
127+
return
128+
}
129+
130+
let event = Event()
131+
SentryLevelBridge.setBreadcrumbLevelOn(event, level: SentryLevel.error.rawValue)
132+
let exceptionType = SentryAppHangTypeMapper.getExceptionType(anrType: .fullyBlocking)
133+
let exception = Exception(value: "App hanging for \(duration) seconds.", type: exceptionType)
134+
let mechanism = Mechanism(type: "AppHang")
135+
exception.mechanism = mechanism
136+
exception.stacktrace = threads[0].stacktrace
137+
exception.stacktrace?.snapshot = true
138+
139+
threads.forEach { $0.current = false }
140+
threads[0].current = true
141+
142+
event.exceptions = [exception]
143+
event.threads = threads
144+
145+
event.debugMeta = dependencies.debugImageCache.getDebugImagesFromCache(for: event.threads)
146+
SentrySDK.capture(event: event)
147+
}
148+
}
149+
150+
@objc
151+
@_spi(Private) public final class RunLoopObserverObjcBridge: NSObject {
152+
@_spi(Private) @objc public init(dependencies: SentryDependencyScope) {
153+
observer = RunloopObserver(dependencies: dependencies, minHangTime: 2)
154+
observer.start()
155+
}
156+
let observer: RunloopObserver
157+
158+
}
159+
160+
@objc
161+
@_spi(Private) public class SentryDependencyScope: NSObject, SentryCurrentDateProviding, DebugImageCacheProviding, ThreadInspectorProviding {
162+
@objc @_spi(Private) public init(options: Options, debugImageCache: DebugImageCache) {
163+
self.threadInspector = SentryThreadInspector(options: options)
164+
self.debugImageCache = debugImageCache
165+
}
166+
167+
@_spi(Private) @objc public let dateProvider: SentryCurrentDateProvider = SentryDefaultCurrentDateProvider()
168+
let threadInspector: ThreadInspector
169+
let debugImageCache: DebugImageCache
170+
}
171+
172+
extension SentryThreadInspector: ThreadInspector { }

0 commit comments

Comments
 (0)