diff --git a/CHANGELOG.md b/CHANGELOG.md index 73c343ef6a5..078f5266d3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ ### Fixes - Remove WebKit optimization check (#1921) +- Detect prewarmed starts with env variable (#1927) ## 7.18.1 diff --git a/Sources/Sentry/SentryAppStartTracker.m b/Sources/Sentry/SentryAppStartTracker.m index badd19f9aaa..8c3b6469a3d 100644 --- a/Sources/Sentry/SentryAppStartTracker.m +++ b/Sources/Sentry/SentryAppStartTracker.m @@ -16,12 +16,13 @@ # import static NSDate *runtimeInit = nil; +static BOOL isActivePrewarm = NO; /** - * The watchdog usually kicks in after an app hanging 10 to 20 seconds. As the app could hang in + * The watchdog usually kicks in after an app hanging for 30 seconds. As the app could hang in * multiple stages during the launch we pick a higher threshold. */ -static const NSTimeInterval SENTRY_APP_START_MAX_DURATION = 60.0; +static const NSTimeInterval SENTRY_APP_START_MAX_DURATION = 180.0; @interface SentryAppStartTracker () @@ -42,6 +43,11 @@ + (void)load { // Invoked whenever this class is added to the Objective-C runtime. runtimeInit = [NSDate date]; + + // The OS sets this environment variable if the app start is pre warmed. There are no official + // docs for this. Found at https://eisel.me/startup. Investigations show that this variable is + // deleted after UIApplicationDidFinishLaunchingNotification, so we have to check it here. + isActivePrewarm = [[NSProcessInfo processInfo].environment[@"ActivePrewarm"] isEqual:@"1"]; } - (instancetype)initWithCurrentDateProvider:(id)currentDateProvider @@ -61,6 +67,21 @@ - (instancetype)initWithCurrentDateProvider:(id)curre return self; } +- (BOOL)isActivePrewarmAvailable +{ +# if TARGET_OS_IOS + // Customer data suggest that app starts are also prewarmed on iOS 14 although this contradicts + // with Apple docs. + if (@available(iOS 14, *)) { + return YES; + } else { + return NO; + } +# else + return NO; +# endif +} + - (void)start { // It can happen that the OS posts the didFinishLaunching notification before we register for it @@ -94,6 +115,15 @@ - (void)buildAppStartMeasurement void (^block)(void) = ^(void) { [self stop]; + // Don't (yet) report pre warmed app starts. + // Check if prewarm is available. Just to be safe to not drop app start data on earlier OS + // verions. + if ([self isActivePrewarmAvailable] && isActivePrewarm) { + [SentryLog logWithMessage:@"The app was prewarmed. Not measuring app start." + andLevel:kSentryLevelInfo]; + return; + } + SentryAppStartType appStartType = [self getStartType]; if (appStartType == SentryAppStartTypeUnknown) { @@ -113,9 +143,9 @@ - (void)buildAppStartMeasurement // According to a talk at WWDC about optimizing app launch // (https://devstreaming-cdn.apple.com/videos/wwdc/2019/423lzf3qsjedrzivc7/423/423_optimizing_app_launch.pdf?dl=1 // slide 17) no process exists for cold and warm launches. Since iOS 15, though, the system - // might decide to pre-warm your app before the user tries to open it. Therefore we use the - // process start timestamp only if it's not too long ago. The process start time returned - // valid values when testing with real devices before iOS 15. See: + // might decide to pre-warm your app before the user tries to open it. The process start + // time returned valid values when testing with real devices if the app start is not + // prewarmed. See: // https://developer.apple.com/documentation/uikit/app_and_environment/responding_to_the_launch_of_your_app/about_the_app_launch_sequence#3894431 // https://developer.apple.com/documentation/metrickit/mxapplaunchmetric, // https://twitter.com/steipete/status/1466013492180312068, @@ -124,11 +154,12 @@ - (void)buildAppStartMeasurement NSTimeInterval appStartDuration = [[self.currentDate date] timeIntervalSinceDate:self.sysctl.processStartTimestamp]; + // Safety check to not report app starts that are completely off. if (appStartDuration >= SENTRY_APP_START_MAX_DURATION) { NSString *message = [NSString stringWithFormat: @"The app start exceeded the max duration of %f seconds. Not measuring app " - @"start.\nThis could be because the OS prewarmed the app's process.", + @"start.", SENTRY_APP_START_MAX_DURATION]; [SentryLog logWithMessage:message andLevel:kSentryLevelInfo]; return; diff --git a/Tests/SentryTests/ClearTestState.swift b/Tests/SentryTests/ClearTestState.swift index 978f22dbcdb..02db84e1294 100644 --- a/Tests/SentryTests/ClearTestState.swift +++ b/Tests/SentryTests/ClearTestState.swift @@ -17,6 +17,9 @@ func clearTestState() { let framesTracker = SentryFramesTracker.sharedInstance() framesTracker.stop() framesTracker.resetFrames() + + setenv("ActivePrewarm", "0", 1) + SentryAppStartTracker.load() #endif SentryDependencyContainer.reset() diff --git a/Tests/SentryTests/Integrations/Performance/AppStartTracking/SentryAppStartTrackerTests.swift b/Tests/SentryTests/Integrations/Performance/AppStartTracking/SentryAppStartTrackerTests.swift index b6a153ffe3c..20a471ac555 100644 --- a/Tests/SentryTests/Integrations/Performance/AppStartTracking/SentryAppStartTrackerTests.swift +++ b/Tests/SentryTests/Integrations/Performance/AppStartTracking/SentryAppStartTrackerTests.swift @@ -128,17 +128,45 @@ class SentryAppStartTrackerTests: XCTestCase { } func testAppLaunches_OSPrewarmedProcess_NoAppStartUp() { - let processStartTime = fixture.currentDate.date().addingTimeInterval(-60) + setenv("ActivePrewarm", "1", 1) + SentryAppStartTracker.load() + givenSystemNotRebooted() + + startApp() + + #if os(iOS) + if #available(iOS 14.0, *) { + assertNoAppStartUp() + } else { + assertValidStart(type: .warm) + } + #else + assertValidStart(type: .warm) + #endif + } + + func testAppLaunches_WrongEnvValue_AppStartUp() { + setenv("ActivePrewarm", "0", 1) + SentryAppStartTracker.load() + givenSystemNotRebooted() + + startApp() + + assertValidStart(type: .warm) + } + + func testAppLaunches_MaximumAppStartDuration_NoAppStart() { + let processStartTime = fixture.currentDate.date().addingTimeInterval(-180) startApp(processStartTimeStamp: processStartTime) assertNoAppStartUp() } func testAppLaunches_OSAlmostPrewarmedProcess_AppStartUp() { - let processStartTime = fixture.currentDate.date().addingTimeInterval(-59) + let processStartTime = fixture.currentDate.date().addingTimeInterval(-179) startApp(processStartTimeStamp: processStartTime) - assertValidStart(type: .cold, expectedDuration: 59.4) + assertValidStart(type: .cold, expectedDuration: 179.4) } func testAppLaunchesBackgroundTask_NoAppStartUp() {