diff --git a/CHANGELOG.md b/CHANGELOG.md index 2fb2b42a1b..ae5c178197 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ > make sure you follow our [migration guide](https://docs.sentry.io/platforms/react-native/migration/) first. +## Unreleased + +### Fixes + +- Fix iOS crash (EXC_BAD_ACCESS) in time-to-initial-display when navigating between screens ([#5887](https://github.com/getsentry/sentry-react-native/pull/5887)) + ## 8.6.0 ### Fixes diff --git a/packages/core/ios/RNSentryTimeToDisplay.m b/packages/core/ios/RNSentryTimeToDisplay.m index 41044b347c..a28fdc9f02 100644 --- a/packages/core/ios/RNSentryTimeToDisplay.m +++ b/packages/core/ios/RNSentryTimeToDisplay.m @@ -2,6 +2,8 @@ #import #import +// All static state below is accessed from the main thread (CADisplayLink, UI) and from the +// React Native bridge / JS thread (setActiveSpanId, pop). Synchronize every access. @implementation RNSentryTimeToDisplay { CADisplayLink *displayLink; RCTResponseSenderBlock resolveBlock; @@ -13,6 +15,37 @@ @implementation RNSentryTimeToDisplay { static NSString *activeSpanId; ++ (void)putTimeToDisplayForLocked:(NSString *)screenId value:(NSNumber *)value +{ + if (!screenId) { + return; + } + + // If key already exists, just update the value, + // this should never happen as TTD is recorded once per navigation + // We avoid updating the age to avoid the age array shift + if ([screenIdToRenderDuration objectForKey:screenId]) { + [screenIdToRenderDuration setObject:value forKey:screenId]; + return; + } + + // If we haven't reached capacity yet, just append + if (screenIdAge.count < TIME_TO_DISPLAY_ENTRIES_MAX_SIZE) { + [screenIdToRenderDuration setObject:value forKey:screenId]; + [screenIdAge addObject:screenId]; + } else { + // Remove oldest entry, in most case will already be removed by pop + NSString *oldestKey = screenIdAge[screenIdCurrentIndex]; + [screenIdToRenderDuration removeObjectForKey:oldestKey]; + + [screenIdToRenderDuration setObject:value forKey:screenId]; + screenIdAge[screenIdCurrentIndex] = screenId; + + // Update circular index, point to the new oldest + screenIdCurrentIndex = (screenIdCurrentIndex + 1) % TIME_TO_DISPLAY_ENTRIES_MAX_SIZE; + } +} + + (void)initialize { if (self == [RNSentryTimeToDisplay class]) { @@ -27,51 +60,34 @@ + (void)initialize + (void)setActiveSpanId:(NSString *)spanId { - activeSpanId = spanId; + @synchronized([RNSentryTimeToDisplay class]) { + activeSpanId = spanId != nil ? [spanId copy] : nil; + } } + (NSNumber *)popTimeToDisplayFor:(NSString *)screenId { - NSNumber *value = screenIdToRenderDuration[screenId]; - [screenIdToRenderDuration removeObjectForKey:screenId]; - return value; + @synchronized([RNSentryTimeToDisplay class]) { + NSNumber *value = screenIdToRenderDuration[screenId]; + [screenIdToRenderDuration removeObjectForKey:screenId]; + return value; + } } + (void)putTimeToInitialDisplayForActiveSpan:(NSNumber *)value { - if (activeSpanId != nil) { - NSString *prefixedSpanId = [@"ttid-navigation-" stringByAppendingString:activeSpanId]; - [self putTimeToDisplayFor:prefixedSpanId value:value]; + @synchronized([RNSentryTimeToDisplay class]) { + if (activeSpanId != nil) { + NSString *prefixedSpanId = [@"ttid-navigation-" stringByAppendingString:activeSpanId]; + [self putTimeToDisplayForLocked:prefixedSpanId value:value]; + } } } + (void)putTimeToDisplayFor:(NSString *)screenId value:(NSNumber *)value { - if (!screenId) - return; - - // If key already exists, just update the value, - // this should never happen as TTD is recorded once per navigation - // We avoid updating the age to avoid the age array shift - if ([screenIdToRenderDuration objectForKey:screenId]) { - [screenIdToRenderDuration setObject:value forKey:screenId]; - return; - } - - // If we haven't reached capacity yet, just append - if (screenIdAge.count < TIME_TO_DISPLAY_ENTRIES_MAX_SIZE) { - [screenIdToRenderDuration setObject:value forKey:screenId]; - [screenIdAge addObject:screenId]; - } else { - // Remove oldest entry, in most case will already be removed by pop - NSString *oldestKey = screenIdAge[screenIdCurrentIndex]; - [screenIdToRenderDuration removeObjectForKey:oldestKey]; - - [screenIdToRenderDuration setObject:value forKey:screenId]; - screenIdAge[screenIdCurrentIndex] = screenId; - - // Update circular index, point to the new oldest - screenIdCurrentIndex = (screenIdCurrentIndex + 1) % TIME_TO_DISPLAY_ENTRIES_MAX_SIZE; + @synchronized([RNSentryTimeToDisplay class]) { + [self putTimeToDisplayForLocked:screenId value:value]; } }