Skip to content

Commit db41d1f

Browse files
committed
feat: Hang tracker updates
1 parent c7beb0c commit db41d1f

File tree

7 files changed

+268
-27
lines changed

7 files changed

+268
-27
lines changed

Sentry.xcodeproj/project.pbxproj

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1119,6 +1119,8 @@
11191119
FACEED132E3179A10007B4AC /* SentyOptionsInternal.m in Sources */ = {isa = PBXBuildFile; fileRef = FACEED122E3179A10007B4AC /* SentyOptionsInternal.m */; };
11201120
FAE2DAB82E1F317900262307 /* SentryProfilingSwiftHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = FAE2DAB72E1F317900262307 /* SentryProfilingSwiftHelpers.m */; };
11211121
FAE2DABA2E1F318900262307 /* SentryProfilingSwiftHelpers.h in Headers */ = {isa = PBXBuildFile; fileRef = FAE2DAB92E1F318900262307 /* SentryProfilingSwiftHelpers.h */; };
1122+
FAE579D52E7F238100B710F9 /* HangTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAE579CF2E7F237A00B710F9 /* HangTracker.swift */; };
1123+
FAE57A3C2E81C55900B710F9 /* SentryWatchdogTerminationTrackingIntegrationSwift.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAE57A362E81C54F00B710F9 /* SentryWatchdogTerminationTrackingIntegrationSwift.swift */; };
11221124
FAE80C242E4695B40010A595 /* SentryEvent+Serialize.h in Headers */ = {isa = PBXBuildFile; fileRef = FAE80C232E4695AE0010A595 /* SentryEvent+Serialize.h */; };
11231125
FAEC270E2DF3526000878871 /* SentryUserFeedback.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAEC270D2DF3526000878871 /* SentryUserFeedback.swift */; };
11241126
FAEC273D2DF3933A00878871 /* NSData+Unzip.m in Sources */ = {isa = PBXBuildFile; fileRef = FAEC273C2DF3933200878871 /* NSData+Unzip.m */; };
@@ -2466,6 +2468,8 @@
24662468
FACEED122E3179A10007B4AC /* SentyOptionsInternal.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentyOptionsInternal.m; sourceTree = "<group>"; };
24672469
FAE2DAB72E1F317900262307 /* SentryProfilingSwiftHelpers.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryProfilingSwiftHelpers.m; sourceTree = "<group>"; };
24682470
FAE2DAB92E1F318900262307 /* SentryProfilingSwiftHelpers.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryProfilingSwiftHelpers.h; path = Sources/Sentry/include/SentryProfilingSwiftHelpers.h; sourceTree = SOURCE_ROOT; };
2471+
FAE579CF2E7F237A00B710F9 /* HangTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HangTracker.swift; sourceTree = "<group>"; };
2472+
FAE57A362E81C54F00B710F9 /* SentryWatchdogTerminationTrackingIntegrationSwift.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryWatchdogTerminationTrackingIntegrationSwift.swift; sourceTree = "<group>"; };
24692473
FAE80C232E4695AE0010A595 /* SentryEvent+Serialize.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "SentryEvent+Serialize.h"; path = "include/SentryEvent+Serialize.h"; sourceTree = "<group>"; };
24702474
FAEC270D2DF3526000878871 /* SentryUserFeedback.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryUserFeedback.swift; sourceTree = "<group>"; };
24712475
FAEC273C2DF3933200878871 /* NSData+Unzip.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSData+Unzip.m"; sourceTree = "<group>"; };
@@ -4239,6 +4243,7 @@
42394243
D452FCBC2DDB6FA800AFF56F /* WatchdogTerminations */ = {
42404244
isa = PBXGroup;
42414245
children = (
4246+
FAE57A362E81C54F00B710F9 /* SentryWatchdogTerminationTrackingIntegrationSwift.swift */,
42424247
D452FCBD2DDB6FC200AFF56F /* Processors */,
42434248
);
42444249
path = WatchdogTerminations;
@@ -4336,6 +4341,7 @@
43364341
D800942328F82E8D005D3943 /* Swift */ = {
43374342
isa = PBXGroup;
43384343
children = (
4344+
FAE579CF2E7F237A00B710F9 /* HangTracker.swift */,
43394345
F4FE9E062E6248BB0014FED5 /* SentryCrash */,
43404346
FABB48B22E59310D0071397E /* Transaction */,
43414347
FAAB29F02E3D252000ACD577 /* SentrySession.swift */,
@@ -5877,6 +5883,7 @@
58775883
FA01BCB22E69352A00968DFA /* SentryDiscardedEvent.swift in Sources */,
58785884
7BAF3DCE243DCBFE008A5414 /* SentryTransportFactory.m in Sources */,
58795885
F4E3DCCB2E1579240093CB80 /* SentryScopePersistentStore.swift in Sources */,
5886+
FAE57A3C2E81C55900B710F9 /* SentryWatchdogTerminationTrackingIntegrationSwift.swift in Sources */,
58805887
7D65260E237F649E00113EA2 /* SentryScope.m in Sources */,
58815888
D4EDF9842D0B2A210071E7B3 /* Data+SentryTracing.swift in Sources */,
58825889
84281C472A57905700EE88F2 /* SentrySample.m in Sources */,
@@ -5900,6 +5907,7 @@
59005907
0A2D8DA9289BC905008720F6 /* SentryViewHierarchyProvider.m in Sources */,
59015908
D84D2CDD2C2BF7370011AF8A /* SentryReplayEvent.swift in Sources */,
59025909
D8BC28CC2BFF78220054DA4D /* SentryRRWebTouchEvent.swift in Sources */,
5910+
FAE579D52E7F238100B710F9 /* HangTracker.swift in Sources */,
59035911
D452FC592DDB4B1700AFF56F /* SentryWatchdogTerminationBreadcrumbProcessor.m in Sources */,
59045912
FAE2DAB82E1F317900262307 /* SentryProfilingSwiftHelpers.m in Sources */,
59055913
8EA1ED0B2668F8C400E62B98 /* SentryUIViewControllerSwizzling.m in Sources */,

Sources/Sentry/SentryDependencyContainer.m

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,9 @@ - (instancetype)init
143143
if (self = [super init]) {
144144
isInitialializingDependencyContainer = YES;
145145

146+
_hangTracker = [[SentryHangTrackerObjcBridge alloc]
147+
initWithDateProvider:SentryDependencies.dateProvider];
148+
146149
_dispatchQueueWrapper = SentryDependencies.dispatchQueueWrapper;
147150
_random = [[SentryRandom alloc] init];
148151
_threadWrapper = [[SentryThreadWrapper alloc] init];

Sources/Sentry/SentryWatchdogTerminationTrackingIntegration.m

Lines changed: 25 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,13 @@
1818
# import <SentryWatchdogTerminationTracker.h>
1919
NS_ASSUME_NONNULL_BEGIN
2020

21-
@interface SentryWatchdogTerminationTrackingIntegration () <SentryANRTrackerDelegate>
21+
@interface SentryWatchdogTerminationTrackingIntegration ()
2222

2323
@property (nonatomic, strong) SentryWatchdogTerminationTracker *tracker;
24-
@property (nonatomic, strong) id<SentryANRTracker> anrTracker;
2524
@property (nullable, nonatomic, copy) NSString *testConfigurationFilePath;
2625
@property (nonatomic, strong) SentryAppStateManager *appStateManager;
26+
@property (nonatomic, strong) SentryWatchdogTerminationTrackingIntegrationSwift *swiftImpl;
27+
@property (nonatomic, strong) SentryDispatchQueueWrapper *queue;
2728

2829
@end
2930

@@ -36,6 +37,15 @@ - (instancetype)init
3637
= SentryDependencyContainer.sharedInstance.processInfoWrapper;
3738
self.testConfigurationFilePath
3839
= processInfoWrapper.environment[@"XCTestConfigurationFilePath"];
40+
dispatch_queue_attr_t attributes = dispatch_queue_attr_make_with_qos_class(
41+
DISPATCH_QUEUE_SERIAL, DISPATCH_QUEUE_PRIORITY_HIGH, 0);
42+
self.queue = [[SentryDispatchQueueWrapper alloc]
43+
initWithName:"io.sentry.watchdog-termination-tracker"
44+
attributes:attributes];
45+
self.swiftImpl = [[SentryWatchdogTerminationTrackingIntegrationSwift alloc]
46+
initWithHangTracker:SentryDependencyContainer.sharedInstance.hangTracker
47+
hangStarted:^{ [self hangStarted]; }
48+
hangStopped:^{ [self hangStopped]; }];
3949
}
4050
return self;
4151
}
@@ -50,12 +60,6 @@ - (BOOL)installWithOptions:(SentryOptions *)options
5060
return NO;
5161
}
5262

53-
dispatch_queue_attr_t attributes = dispatch_queue_attr_make_with_qos_class(
54-
DISPATCH_QUEUE_SERIAL, DISPATCH_QUEUE_PRIORITY_HIGH, 0);
55-
SentryDispatchQueueWrapper *dispatchQueueWrapper =
56-
[[SentryDispatchQueueWrapper alloc] initWithName:"io.sentry.watchdog-termination-tracker"
57-
attributes:attributes];
58-
5963
SentryFileManager *fileManager = [[[SentrySDKInternal currentHub] getClient] fileManager];
6064
SentryAppStateManager *appStateManager =
6165
[SentryDependencyContainer sharedInstance].appStateManager;
@@ -70,22 +74,12 @@ - (BOOL)installWithOptions:(SentryOptions *)options
7074
self.tracker = [[SentryWatchdogTerminationTracker alloc] initWithOptions:options
7175
watchdogTerminationLogic:logic
7276
appStateManager:appStateManager
73-
dispatchQueueWrapper:dispatchQueueWrapper
77+
dispatchQueueWrapper:self.queue
7478
fileManager:fileManager
7579
scopePersistentStore:scopeStore];
7680

7781
[self.tracker start];
78-
79-
# if SDK_V9
80-
BOOL isV2Enabled = YES;
81-
# else
82-
BOOL isV2Enabled = options.enableAppHangTrackingV2;
83-
# endif // SDK_V9
84-
85-
self.anrTracker =
86-
[SentryDependencyContainer.sharedInstance getANRTracker:options.appHangTimeoutInterval
87-
isV2Enabled:isV2Enabled];
88-
[self.anrTracker addListener:self];
82+
[self.swiftImpl start];
8983

9084
self.appStateManager = appStateManager;
9185

@@ -125,19 +119,23 @@ - (void)uninstall
125119
if (nil != self.tracker) {
126120
[self.tracker stop];
127121
}
128-
[self.anrTracker removeListener:self];
122+
[self.swiftImpl stop];
129123
}
130124

131-
- (void)anrDetectedWithType:(enum SentryANRType)type
125+
- (void)hangStarted
132126
{
133-
[self.appStateManager
134-
updateAppState:^(SentryAppState *appState) { appState.isANROngoing = YES; }];
127+
[self.queue dispatchAsyncWithBlock:^{
128+
[self.appStateManager
129+
updateAppState:^(SentryAppState *appState) { appState.isANROngoing = YES; }];
130+
}];
135131
}
136132

137-
- (void)anrStoppedWithResult:(SentryANRStoppedResult *_Nullable)result
133+
- (void)hangStopped
138134
{
139-
[self.appStateManager
140-
updateAppState:^(SentryAppState *appState) { appState.isANROngoing = NO; }];
135+
[self.queue dispatchAsyncWithBlock:^{
136+
[self.appStateManager
137+
updateAppState:^(SentryAppState *appState) { appState.isANROngoing = NO; }];
138+
}];
141139
}
142140

143141
@end

Sources/Sentry/include/HybridPublic/SentryDependencyContainer.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
@class SentryOptions;
2626
@class SentrySessionTracker;
2727
@class SentryGlobalEventProcessor;
28+
@class SentryHangTrackerObjcBridge;
2829

2930
@protocol SentryANRTracker;
3031
@protocol SentryRandomProtocol;
@@ -95,6 +96,7 @@ SENTRY_NO_INIT
9596
@property (nonatomic, strong) id<SentryRateLimits> rateLimits;
9697
@property (nonatomic, strong) id<SentryApplication> application;
9798
@property (nonatomic, strong) SentryThreadsafeApplication *threadsafeApplication;
99+
@property (nonatomic, strong) SentryHangTrackerObjcBridge *hangTracker;
98100

99101
#if SENTRY_HAS_REACHABILITY
100102
@property (nonatomic, strong) SentryReachability *reachability;

Sources/Sentry/include/SentryPrivate.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
// Headers that also import SentryDefines should be at the end of this list
2222
// otherwise it wont compile
23+
#import "SentryAppStateManager.h"
2324
#import "SentryAsyncLog.h"
2425
#import "SentryClient+Logs.h"
2526
#import "SentryCrash.h"

Sources/Swift/HangTracker.swift

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
@_implementationOnly import _SentryPrivate
2+
#if canImport(UIKit) && !SENTRY_NO_UIKIT
3+
import UIKit
4+
#endif
5+
6+
struct RunLoopIteration {
7+
let startTime: TimeInterval
8+
let endTime: TimeInterval
9+
}
10+
11+
protocol HangTrackerProtocol {
12+
// The callback must be called on a background thread, because the main thread is blocked
13+
func addLateRunLoopObserver(handler: @escaping (UUID, TimeInterval) -> Void) -> UUID
14+
15+
func removeLateRunLoopObserver(id: UUID)
16+
17+
// The callback is always called on the main thread
18+
func addFinishedRunLoopObserver(handler: @escaping (RunLoopIteration) -> Void) -> UUID
19+
20+
func removeFinishedRunLoopObserver(id: UUID)
21+
}
22+
23+
final class HangTracker: HangTrackerProtocol {
24+
25+
private let dateProvider: SentryCurrentDateProvider
26+
27+
init(
28+
dateProvider: SentryCurrentDateProvider) {
29+
self.dateProvider = dateProvider
30+
#if canImport(UIKit) && !SENTRY_NO_UIKIT && !os(visionOS) && !os(watchOS)
31+
var maxFPS = 60.0
32+
if #available(iOS 13.0, tvOS 13.0, *) {
33+
let window = UIApplication.shared.connectedScenes.flatMap { ($0 as? UIWindowScene)?.windows ?? [] }.first { $0.isKeyWindow }
34+
maxFPS = Double(window?.screen.maximumFramesPerSecond ?? 60)
35+
} else {
36+
maxFPS = Double(UIScreen.main.maximumFramesPerSecond)
37+
}
38+
#else
39+
let maxFPS: Double = 60.0
40+
#endif
41+
let expectedFrameDuration = 1.0 / maxFPS
42+
hangNotifyThreshold = expectedFrameDuration * 1.5
43+
}
44+
45+
func addLateRunLoopObserver(handler: @escaping (UUID, TimeInterval) -> Void) -> UUID {
46+
let id = UUID()
47+
queue.async { [weak self] in
48+
self?.lateRunLoop[id] = handler
49+
DispatchQueue.main.async {
50+
self?.startIfNecessary()
51+
}
52+
}
53+
return id
54+
}
55+
56+
func removeLateRunLoopObserver(id: UUID) {
57+
queue.async { [weak self] in
58+
guard let self else { return }
59+
lateRunLoop.removeValue(forKey: id)
60+
if lateRunLoop.isEmpty {
61+
DispatchQueue.main.async { [weak self] in
62+
if self?.finishedRunLoop.isEmpty ?? false {
63+
self?.stop()
64+
}
65+
}
66+
}
67+
}
68+
}
69+
70+
func addFinishedRunLoopObserver(handler: @escaping (RunLoopIteration) -> Void) -> UUID {
71+
let id = UUID()
72+
finishedRunLoop[id] = handler
73+
startIfNecessary()
74+
return id
75+
}
76+
77+
func removeFinishedRunLoopObserver(id: UUID) {
78+
finishedRunLoop.removeValue(forKey: id)
79+
if finishedRunLoop.isEmpty {
80+
queue.async { [weak self] in
81+
if self?.lateRunLoop.isEmpty ?? false {
82+
DispatchQueue.main.async {
83+
if self?.finishedRunLoop.isEmpty ?? false {
84+
self?.stop()
85+
}
86+
}
87+
}
88+
}
89+
}
90+
}
91+
92+
// This queue is used to detect main thread hangs, they need to be detected on a background thread
93+
// since the main thread is hanging.
94+
private let queue = DispatchQueue(label: "io.sentry.runloop-observer-checker")
95+
private let hangNotifyThreshold: TimeInterval
96+
97+
// MARK: Main queue
98+
99+
private var observer: CFRunLoopObserver?
100+
private var finishedRunLoop = [UUID: (RunLoopIteration) -> Void]()
101+
private var semaphore: DispatchSemaphore?
102+
private var loopStartTime: TimeInterval?
103+
104+
private func startIfNecessary() {
105+
guard observer == nil else {
106+
// Already running
107+
return
108+
}
109+
110+
let observer = CFRunLoopObserverCreateWithHandler(nil, CFRunLoopActivity.beforeWaiting.rawValue | CFRunLoopActivity.afterWaiting.rawValue, true, CFIndex(INT_MAX)) { [weak self] _, activity in
111+
guard let self else { return }
112+
113+
let currentTime = dateProvider.systemUptime()
114+
switch activity {
115+
case .beforeWaiting:
116+
print("before")
117+
semaphore?.signal()
118+
if let loopStartTime {
119+
for handler in finishedRunLoop.values {
120+
handler(RunLoopIteration(startTime: loopStartTime, endTime: currentTime))
121+
}
122+
}
123+
case .afterWaiting:
124+
print("after")
125+
let started = currentTime
126+
loopStartTime = currentTime
127+
let localSemaphore = DispatchSemaphore(value: 0)
128+
semaphore = localSemaphore
129+
queue.async { [weak self] in
130+
self?.waitForHang(semaphore: localSemaphore, started: started, isStarting: true)
131+
}
132+
default:
133+
fatalError()
134+
}
135+
}
136+
self.observer = observer
137+
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, .commonModes)
138+
}
139+
140+
private func stop() {
141+
dispatchPrecondition(condition: .onQueue(.main))
142+
143+
guard let observer else {
144+
return
145+
}
146+
CFRunLoopRemoveObserver(CFRunLoopGetMain(), observer, .commonModes)
147+
self.observer = nil
148+
}
149+
150+
// MARK: Background queue
151+
152+
private var lateRunLoop = [UUID: (UUID, TimeInterval) -> Void]()
153+
private var hangId = UUID()
154+
155+
private func waitForHang(semaphore: DispatchSemaphore, started: TimeInterval, isStarting: Bool) {
156+
dispatchPrecondition(condition: .onQueue(queue))
157+
158+
let timeout = DispatchTime.now() + DispatchTimeInterval.milliseconds(Int(hangNotifyThreshold * 1_000))
159+
let result = semaphore.wait(timeout: timeout)
160+
switch result {
161+
case .timedOut:
162+
print("[HANG] Timeout, hang detected")
163+
if isStarting {
164+
hangId = UUID()
165+
}
166+
lateRunLoop.values.forEach { $0(hangId, dateProvider.systemUptime() - started) }
167+
waitForHang(semaphore: semaphore, started: started, isStarting: false)
168+
case .success:
169+
break
170+
}
171+
}
172+
}
173+
174+
@objc
175+
@_spi(Private) public final class SentryHangTrackerObjcBridge: NSObject {
176+
177+
let tracker: HangTracker
178+
179+
@objc public init(
180+
dateProvider: SentryCurrentDateProvider,
181+
) {
182+
tracker = HangTracker(
183+
dateProvider: dateProvider)
184+
}
185+
}

0 commit comments

Comments
 (0)