Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -35,21 +35,29 @@ - (instancetype)init {
} else {
_traceBackgroundState = FPRTraceStateForegroundOnly;
}
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(applicationDidBecomeActive:)
name:UIApplicationDidBecomeActiveNotification
object:[UIApplication sharedApplication]];

[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(applicationDidEnterBackground:)
name:UIApplicationDidEnterBackgroundNotification
object:[UIApplication sharedApplication]];
});
if ([NSThread isMainThread]) {
[self registerNotificationObservers];
} else {
dispatch_sync(dispatch_get_main_queue(), ^{
[self registerNotificationObservers];
});
}
}
return self;
}

- (void)registerNotificationObservers {
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(applicationDidBecomeActive:)
name:UIApplicationDidBecomeActiveNotification
object:[UIApplication sharedApplication]];

[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(applicationDidEnterBackground:)
name:UIApplicationDidEnterBackgroundNotification
object:[UIApplication sharedApplication]];
}

- (void)dealloc {
// Remove all the notification observers registered.
[[NSNotificationCenter defaultCenter] removeObserver:self];
Expand Down
209 changes: 209 additions & 0 deletions FirebasePerformance/Tests/Unit/FPRTraceBackgroundActivityTrackerTest.m
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,213 @@ - (void)testBackgroundTracking {
}];
}

/** Tests that synchronous observer registration works correctly and observers are immediately
* available. */
- (void)testObservers_synchronousRegistrationAddsObserver {
NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
FPRTraceBackgroundActivityTracker *tracker = [[FPRTraceBackgroundActivityTracker alloc] init];
XCTAssertNotNil(tracker);

[notificationCenter postNotificationName:UIApplicationDidBecomeActiveNotification
object:[UIApplication sharedApplication]];
XCTAssertEqual(tracker.traceBackgroundState, FPRTraceStateForegroundOnly);

tracker = nil;
XCTAssertNil(tracker);
XCTAssertNoThrow([notificationCenter postNotificationName:UIApplicationDidBecomeActiveNotification
object:[UIApplication sharedApplication]]);
XCTAssertNoThrow([notificationCenter
postNotificationName:UIApplicationDidEnterBackgroundNotification
object:[UIApplication sharedApplication]]);
}

/** Tests rapid creation and deallocation to verify race condition fix. */
- (void)testRapidCreationAndDeallocation_noRaceCondition {
// This test simulates the real crash scenario by forcing async dispatch timing
XCTestExpectation *expectation =
[self expectationWithDescription:@"All async operations complete"];

__block int completedOperations = 0;
const int totalOperations = 50;

for (int i = 0; i < totalOperations; i++) {
@autoreleasepool {
FPRTraceBackgroundActivityTracker *tracker = [[FPRTraceBackgroundActivityTracker alloc] init];
XCTAssertNotNil(tracker);

// Force multiple runloop cycles to increase chance of race condition
dispatch_async(dispatch_get_main_queue(), ^{
// This would crash with old async registration if tracker is deallocated
[[NSNotificationCenter defaultCenter]
postNotificationName:UIApplicationDidBecomeActiveNotification
object:[UIApplication sharedApplication]];

// Increment counter and fulfill expectation when done
completedOperations++;
if (completedOperations == totalOperations) {
[expectation fulfill];
}
});

// Tracker deallocates here immediately due to @autoreleasepool
}
}

// Wait for all async operations to complete
[self waitForExpectationsWithTimeout:10.0
handler:^(NSError *error) {
XCTAssertNil(
error, @"Operations timed out - potential deadlock or crash");
}];

// Additional safety check - post more notifications after everything is done
XCTAssertNoThrow([[NSNotificationCenter defaultCenter]
postNotificationName:UIApplicationDidBecomeActiveNotification
object:[UIApplication sharedApplication]]);
XCTAssertNoThrow([[NSNotificationCenter defaultCenter]
postNotificationName:UIApplicationDidEnterBackgroundNotification
object:[UIApplication sharedApplication]]);
}

/** Tests that observers are registered immediately after init on main thread. */
- (void)testObservers_immediateRegistrationOnMainThread {
XCTAssertTrue([NSThread isMainThread]);

FPRTraceBackgroundActivityTracker *tracker = [[FPRTraceBackgroundActivityTracker alloc] init];

[[NSNotificationCenter defaultCenter]
postNotificationName:UIApplicationDidBecomeActiveNotification
object:[UIApplication sharedApplication]];

XCTAssertEqual(tracker.traceBackgroundState, FPRTraceStateForegroundOnly);
}

/** Tests observer registration when created from background thread. */
- (void)testObservers_registrationFromBackgroundThread {
XCTestExpectation *expectation = [self expectationWithDescription:@"Background thread creation"];

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
FPRTraceBackgroundActivityTracker *tracker = [[FPRTraceBackgroundActivityTracker alloc] init];
XCTAssertNotNil(tracker);

dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter]
postNotificationName:UIApplicationDidBecomeActiveNotification
object:[UIApplication sharedApplication]];

XCTAssertEqual(tracker.traceBackgroundState, FPRTraceStateForegroundOnly);
[expectation fulfill];
});
});

[self waitForExpectationsWithTimeout:5.0
handler:^(NSError *error) {
XCTAssertNil(error, @"Test timed out");
}];
}

/** Tests the exact crash scenario with async dispatch timing that would crash with old
* implementation. */
- (void)testAsyncDispatch_wouldCrashWithOldImplementation {
// This test simulates what the OLD code would do and should crash with async registration
// With the NEW synchronous code, this should pass safely

XCTestExpectation *expectation = [self expectationWithDescription:@"Async crash test complete"];

__block int remainingOperations = 200;

for (int i = 0; i < 200; i++) {
@autoreleasepool {
FPRTraceBackgroundActivityTracker *tracker = [[FPRTraceBackgroundActivityTracker alloc] init];
XCTAssertNotNil(tracker);

// Simulate the old problematic pattern
__weak typeof(tracker) weakTracker = tracker;

// This mimics what the OLD async registration would do
dispatch_async(dispatch_get_main_queue(), ^{
// In old code: tracker might be deallocated here → CRASH
// In new code: observers already registered synchronously → SAFE

if (weakTracker) {
[[NSNotificationCenter defaultCenter]
postNotificationName:UIApplicationDidBecomeActiveNotification
object:[UIApplication sharedApplication]];
}

remainingOperations--;
if (remainingOperations == 0) {
[expectation fulfill];
}
});

// Immediately deallocate tracker - this creates the race condition window
}

// Force runloop processing to increase race condition likelihood
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.001]];
}

[self waitForExpectationsWithTimeout:5.0
handler:^(NSError *error) {
XCTAssertNil(error, @"Async crash test failed");
}];
}

@end

/**
* CRASH REPRODUCTION TEST - Only use this to verify the original bug exists
* This simulates the original async registration pattern that would cause crashes
*/

@interface CrashReproductionTracker : NSObject
@property(nonatomic, readwrite) int traceBackgroundState;
@end

@implementation CrashReproductionTracker

- (instancetype)init {
self = [super init];
if (self) {
_traceBackgroundState = 0;

// This is the ORIGINAL problematic code that would crash
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(handleNotification:)
name:UIApplicationDidBecomeActiveNotification
object:[UIApplication sharedApplication]];
});
}
return self;
}

- (void)handleNotification:(NSNotification *)notification {
_traceBackgroundState = 1;
}

- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
}

/**
* CRASH REPRODUCTION TEST - Only use this to verify the original bug exists
* This simulates the original async registration pattern that would cause crashes
* WARNING: This test is commented out because it WILL crash with the original async pattern
*/

- (void)testCrashReproduction_originalAsyncBug {
// This test WILL crash with the original async pattern
for (int i = 0; i < 100; i++) {
@autoreleasepool {
CrashReproductionTracker *tracker = [[CrashReproductionTracker alloc] init];
// tracker deallocates here, but async block is still queued → CRASH
}

// Process run loop to execute queued async blocks
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.01]];
}
}

@end