Skip to content

Commit 075a044

Browse files
fix: Cleanup AppHangTracking properly when closing SDK (#2671)
When closing the SDK directly after starting it, it could happen that the ANRTracker didn't start its watchdog thread yet. Canceling the ANRTrackers thread required the instance of the watchdog thread and didn't work correctly. This is fixed now by using simple state management in the ANRTracker. While this is an edge case, it can help with flaky tests, as the test logs showed signs of the ANRTracker running in the background.
1 parent ddc9b9a commit 075a044

File tree

6 files changed

+100
-22
lines changed

6 files changed

+100
-22
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
## Unreleased
4+
5+
### Fixes
6+
7+
- Cleanup AppHangTracking properly when closing SDK (#2671)
8+
39
## 8.1.0
410

511
### Features

Sources/Sentry/SentryANRTracker.m

Lines changed: 43 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@
66

77
NS_ASSUME_NONNULL_BEGIN
88

9+
typedef NS_ENUM(NSInteger, SentryANRTrackerState) {
10+
kSentryANRTrackerNotRunning = 1,
11+
kSentryANRTrackerRunning,
12+
kSentryANRTrackerStarting,
13+
kSentryANRTrackerStopping
14+
};
15+
916
@interface
1017
SentryANRTracker ()
1118

@@ -16,13 +23,11 @@
1623
@property (nonatomic, strong) NSMutableSet<id<SentryANRTrackerDelegate>> *listeners;
1724
@property (nonatomic, assign) NSTimeInterval timeoutInterval;
1825

19-
@property (weak, nonatomic) NSThread *thread;
20-
2126
@end
2227

2328
@implementation SentryANRTracker {
2429
NSObject *threadLock;
25-
BOOL running;
30+
SentryANRTrackerState state;
2631
}
2732

2833
- (instancetype)initWithTimeoutInterval:(NSTimeInterval)timeoutInterval
@@ -37,25 +42,43 @@ - (instancetype)initWithTimeoutInterval:(NSTimeInterval)timeoutInterval
3742
self.crashWrapper = crashWrapper;
3843
self.dispatchQueueWrapper = dispatchQueueWrapper;
3944
self.threadWrapper = threadWrapper;
40-
self.listeners = [NSMutableSet new];
45+
self.listeners = [NSMutableSet set];
4146
threadLock = [[NSObject alloc] init];
42-
running = NO;
47+
state = kSentryANRTrackerNotRunning;
4348
}
4449
return self;
4550
}
4651

4752
- (void)detectANRs
4853
{
49-
NSThread.currentThread.name = @"io.sentry.app-hang-tracker";
50-
self.thread = NSThread.currentThread;
54+
NSUUID *threadID = [NSUUID UUID];
55+
56+
@synchronized(threadLock) {
57+
[self.threadWrapper threadStarted:threadID];
58+
59+
if (state != kSentryANRTrackerStarting) {
60+
[self.threadWrapper threadFinished:threadID];
61+
return;
62+
}
63+
64+
NSThread.currentThread.name = @"io.sentry.app-hang-tracker";
65+
state = kSentryANRTrackerRunning;
66+
}
5167

5268
__block NSInteger ticksSinceUiUpdate = 0;
5369
__block BOOL reported = NO;
5470

5571
NSInteger reportThreshold = 5;
5672
NSTimeInterval sleepInterval = self.timeoutInterval / reportThreshold;
5773

58-
while (![self.thread isCancelled]) {
74+
// Canceling the thread can take up to sleepInterval.
75+
while (true) {
76+
@synchronized(threadLock) {
77+
if (state != kSentryANRTrackerRunning) {
78+
break;
79+
}
80+
}
81+
5982
NSDate *blockDeadline =
6083
[[self.currentDate date] dateByAddingTimeInterval:self.timeoutInterval];
6184

@@ -98,6 +121,11 @@ - (void)detectANRs
98121
[self ANRDetected];
99122
}
100123
}
124+
125+
@synchronized(threadLock) {
126+
state = kSentryANRTrackerNotRunning;
127+
[self.threadWrapper threadFinished:threadID];
128+
}
101129
}
102130

103131
- (void)ANRDetected
@@ -129,10 +157,13 @@ - (void)addListener:(id<SentryANRTrackerDelegate>)listener
129157
@synchronized(self.listeners) {
130158
[self.listeners addObject:listener];
131159

132-
if (self.listeners.count > 0 && !running) {
160+
if (self.listeners.count > 0 && state == kSentryANRTrackerNotRunning) {
133161
@synchronized(threadLock) {
134-
if (!running) {
135-
[self start];
162+
if (state == kSentryANRTrackerNotRunning) {
163+
state = kSentryANRTrackerStarting;
164+
[NSThread detachNewThreadSelector:@selector(detectANRs)
165+
toTarget:self
166+
withObject:nil];
136167
}
137168
}
138169
}
@@ -158,20 +189,11 @@ - (void)clear
158189
}
159190
}
160191

161-
- (void)start
162-
{
163-
@synchronized(threadLock) {
164-
[NSThread detachNewThreadSelector:@selector(detectANRs) toTarget:self withObject:nil];
165-
running = YES;
166-
}
167-
}
168-
169192
- (void)stop
170193
{
171194
@synchronized(threadLock) {
172195
SENTRY_LOG_INFO(@"Stopping ANR detection");
173-
[self.thread cancel];
174-
running = NO;
196+
state = kSentryANRTrackerStopping;
175197
}
176198
}
177199

Sources/Sentry/SentryThreadWrapper.m

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,16 @@ - (void)sleepForTimeInterval:(NSTimeInterval)timeInterval
99
[NSThread sleepForTimeInterval:timeInterval];
1010
}
1111

12+
- (void)threadStarted:(NSUUID *)threadID;
13+
{
14+
// No op. Only needed for testing.
15+
}
16+
17+
- (void)threadFinished:(NSUUID *)threadID
18+
{
19+
// No op. Only needed for testing.
20+
}
21+
1222
@end
1323

1424
NS_ASSUME_NONNULL_END

Sources/Sentry/include/SentryThreadWrapper.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ NS_ASSUME_NONNULL_BEGIN
99

1010
- (void)sleepForTimeInterval:(NSTimeInterval)timeInterval;
1111

12+
- (void)threadStarted:(NSUUID *)threadID;
13+
14+
- (void)threadFinished:(NSUUID *)threadID;
15+
1216
@end
1317

1418
NS_ASSUME_NONNULL_END
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,26 @@
11
import Foundation
2+
import XCTest
23

34
class SentryTestThreadWrapper: SentryThreadWrapper {
45

6+
var threadFinishedExpectation = XCTestExpectation(description: "Thread Finished Expectation")
7+
var threads: Set<UUID> = Set()
8+
var threadStartedInvocations = Invocations<UUID>()
9+
var threadFinishedInvocations = Invocations<UUID>()
10+
511
override func sleep(forTimeInterval timeInterval: TimeInterval) {
612
// Don't sleep. Do nothing.
713
}
814

15+
override func threadStarted(_ threadID: UUID) {
16+
threadStartedInvocations.record(threadID)
17+
threads.insert(threadID)
18+
}
19+
20+
override func threadFinished(_ threadID: UUID) {
21+
threadFinishedInvocations.record(threadID)
22+
threads.remove(threadID)
23+
threadFinishedExpectation.fulfill()
24+
}
25+
926
}

Tests/SentryTests/Integrations/ANR/SentryANRTrackerTests.swift

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,16 @@ class SentryANRTrackerTests: XCTestCase, SentryANRTrackerDelegate {
4141
override func tearDown() {
4242
super.tearDown()
4343
sut.clear()
44+
45+
wait(for: [fixture.threadWrapper.threadFinishedExpectation], timeout: 5)
46+
XCTAssertEqual(0, fixture.threadWrapper.threads.count)
4447
}
4548

4649
func start() {
4750
sut.addListener(self)
4851
}
4952

50-
func testContinousANR_OneReported() {
53+
func testContinuousANR_OneReported() {
5154
fixture.dispatchQueue.blockBeforeMainBlock = {
5255
self.advanceTime(bySeconds: self.fixture.timeoutInterval)
5356
return false
@@ -153,6 +156,22 @@ class SentryANRTrackerTests: XCTestCase, SentryANRTrackerDelegate {
153156

154157
}
155158

159+
func testClearDirectlyAfterStart() {
160+
anrDetectedExpectation.isInverted = true
161+
162+
let invocations = 10
163+
for _ in 0..<invocations {
164+
sut.addListener(self)
165+
sut.clear()
166+
}
167+
168+
wait(for: [anrDetectedExpectation, anrStoppedExpectation], timeout: 1)
169+
170+
XCTAssertEqual(0, fixture.threadWrapper.threads.count)
171+
XCTAssertEqual(1, fixture.threadWrapper.threadStartedInvocations.count)
172+
XCTAssertEqual(1, fixture.threadWrapper.threadFinishedInvocations.count)
173+
}
174+
156175
func anrDetected() {
157176
anrDetectedExpectation.fulfill()
158177
}

0 commit comments

Comments
 (0)