diff --git a/CHANGELOG.md b/CHANGELOG.md index 0de68b055a..1bb8aa2067 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Features + +- Automatically profile app launches (#3529) + ### Improvements - Cache installationID async to avoid file IO on the main thread when starting the SDK (#3601) @@ -9,7 +13,7 @@ ### Fixes - Finish TTID span when transaction finishes (#3610) -- Dont take screenshot and view hierarchy for app hanging (#3620) +- Don't take screenshot and view hierarchy for app hanging (#3620) ## 8.20.0 @@ -17,7 +21,6 @@ - Add visionOS as device family (#3548) - Add VisionOS Support for Carthage (#3565) -- Automatically profile app launches (#3529) ### Fixes diff --git a/Samples/iOS-Swift/iOS-Swift.xcodeproj/project.pbxproj b/Samples/iOS-Swift/iOS-Swift.xcodeproj/project.pbxproj index 3ed5c9a5fb..b722db6895 100644 --- a/Samples/iOS-Swift/iOS-Swift.xcodeproj/project.pbxproj +++ b/Samples/iOS-Swift/iOS-Swift.xcodeproj/project.pbxproj @@ -38,7 +38,6 @@ 84B527BD28DD25E400475E8D /* SentryDevice.mm in Sources */ = {isa = PBXBuildFile; fileRef = 84B527BC28DD25E400475E8D /* SentryDevice.mm */; }; 84BE546F287503F100ACC735 /* SentrySDKPerformanceBenchmarkTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 84BE546E287503F100ACC735 /* SentrySDKPerformanceBenchmarkTests.m */; }; 84BE547E287645B900ACC735 /* SentryProcessInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 84BE54792876451D00ACC735 /* SentryProcessInfo.m */; }; - 84DEE88E2B6A4D1200A7BC17 /* AppStartup.m in Sources */ = {isa = PBXBuildFile; fileRef = 84DEE88D2B6A4D1200A7BC17 /* AppStartup.m */; }; 84FB812A284001B800F3A94A /* SentryBenchmarking.mm in Sources */ = {isa = PBXBuildFile; fileRef = 84FB8129284001B800F3A94A /* SentryBenchmarking.mm */; }; 84FB812B284001B800F3A94A /* SentryBenchmarking.mm in Sources */ = {isa = PBXBuildFile; fileRef = 84FB8129284001B800F3A94A /* SentryBenchmarking.mm */; }; 8E8C57AF25EF16E6001CEEFA /* TraceTestViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E8C57AE25EF16E6001CEEFA /* TraceTestViewController.swift */; }; @@ -315,8 +314,6 @@ 84BE546E287503F100ACC735 /* SentrySDKPerformanceBenchmarkTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentrySDKPerformanceBenchmarkTests.m; sourceTree = ""; }; 84BE54782876451D00ACC735 /* SentryProcessInfo.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SentryProcessInfo.h; sourceTree = ""; }; 84BE54792876451D00ACC735 /* SentryProcessInfo.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryProcessInfo.m; sourceTree = ""; }; - 84DEE88C2B6A4D1200A7BC17 /* AppStartup.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppStartup.h; sourceTree = ""; }; - 84DEE88D2B6A4D1200A7BC17 /* AppStartup.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppStartup.m; sourceTree = ""; }; 84FB8125284001B800F3A94A /* SentryBenchmarking.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SentryBenchmarking.h; sourceTree = ""; }; 84FB8129284001B800F3A94A /* SentryBenchmarking.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = SentryBenchmarking.mm; sourceTree = ""; }; 84FB812C2840021B00F3A94A /* iOS-Swift-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "iOS-Swift-Bridging-Header.h"; sourceTree = ""; }; @@ -478,8 +475,6 @@ D8DBDA73274D4DF900007380 /* ViewControllers */, 63F93AA9245AC91600A500DB /* iOS-Swift.entitlements */, 637AFDA9243B02760034958B /* AppDelegate.swift */, - 84DEE88C2B6A4D1200A7BC17 /* AppStartup.h */, - 84DEE88D2B6A4D1200A7BC17 /* AppStartup.m */, 637AFDAD243B02760034958B /* TransactionsViewController.swift */, 84AB90782A50031B0054C99A /* Profiling */, D80D021229EE93630084393D /* ErrorsViewController.swift */, @@ -958,7 +953,6 @@ 7B79000429028C7300A7F467 /* MetricKitManager.swift in Sources */, D8D7BB4A2750067900044146 /* UIAssert.swift in Sources */, D8F3D057274E574200B56F8C /* LoremIpsumViewController.swift in Sources */, - 84DEE88E2B6A4D1200A7BC17 /* AppStartup.m in Sources */, 629EC8AD2B0B537400858855 /* ANRs.swift in Sources */, D8DBDA78274D5FC400007380 /* SplitViewController.swift in Sources */, 84ACC43C2A73CB5900932A18 /* ProfilingNetworkScanner.swift in Sources */, diff --git a/Samples/iOS-Swift/iOS-Swift.xcodeproj/xcshareddata/xcschemes/iOS-Swift.xcscheme b/Samples/iOS-Swift/iOS-Swift.xcodeproj/xcshareddata/xcschemes/iOS-Swift.xcscheme index 72e340ada0..dc3a836c25 100644 --- a/Samples/iOS-Swift/iOS-Swift.xcodeproj/xcshareddata/xcschemes/iOS-Swift.xcscheme +++ b/Samples/iOS-Swift/iOS-Swift.xcodeproj/xcshareddata/xcschemes/iOS-Swift.xcscheme @@ -73,6 +73,14 @@ argument = "--disable-file-io-tracing" isEnabled = "NO"> + + + + @@ -82,8 +90,8 @@ isEnabled = "NO"> + argument = "--profile-app-launches" + isEnabled = "YES"> Bool { - print("[iOS-Swift] launch arguments: \(ProcessInfo.processInfo.arguments)") - print("[iOS-Swift] environment: \(ProcessInfo.processInfo.environment)") + print("[iOS-Swift] [debug] launch arguments: \(ProcessInfo.processInfo.arguments)") + print("[iOS-Swift] [debug] environment: \(ProcessInfo.processInfo.environment)") + maybeWipeData() AppDelegate.startSentry() if #available(iOS 15.0, *) { @@ -162,3 +163,19 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // swiftlint:enable force_cast } } + +private extension AppDelegate { + // previously tried putting this in an AppDelegate.load override in ObjC, but it wouldn't run until after a launch profiler would have an opportunity to run, since SentryProfiler.load would always run first due to being dynamically linked in a framework module. it is sufficient to do it before calling SentrySDK.startWithOptions to clear state for testProfiledAppLaunches because we don't make any assertions on a launch profile the first launch of the app in that test + func maybeWipeData() { + if ProcessInfo.processInfo.arguments.contains("--io.sentry.wipe-data") { + print("[iOS-Swift] [debug] removing app data") + let appSupport = NSSearchPathForDirectoriesInDomains(.applicationSupportDirectory, .userDomainMask, true).first! + let cache = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true).first! + for path in [appSupport, cache] { + for item in FileManager.default.enumerator(atPath: path)! { + try! FileManager.default.removeItem(atPath: (path as NSString).appendingPathComponent((item as! String))) + } + } + } + } +} diff --git a/Samples/iOS-Swift/iOS-Swift/AppStartup.h b/Samples/iOS-Swift/iOS-Swift/AppStartup.h deleted file mode 100644 index 1b7bb41899..0000000000 --- a/Samples/iOS-Swift/iOS-Swift/AppStartup.h +++ /dev/null @@ -1,17 +0,0 @@ -// -// AppStartup.h -// iOS-Swift -// -// Created by Andrew McKnight on 1/31/24. -// Copyright © 2024 Sentry. All rights reserved. -// - -#import - -NS_ASSUME_NONNULL_BEGIN - -@interface AppStartup : NSObject - -@end - -NS_ASSUME_NONNULL_END diff --git a/Samples/iOS-Swift/iOS-Swift/AppStartup.m b/Samples/iOS-Swift/iOS-Swift/AppStartup.m deleted file mode 100644 index f3d7488e68..0000000000 --- a/Samples/iOS-Swift/iOS-Swift/AppStartup.m +++ /dev/null @@ -1,26 +0,0 @@ -#import "AppStartup.h" - -@implementation AppStartup - -// we must do this in objective c, because it's not permitted to be overridden in Swift -+ (void)load -{ - if ([NSProcessInfo.processInfo.arguments containsObject:@"--io.sentry.wipe-data"]) { - NSLog(@"[iOS-Swift] removing app data"); - NSString *appSupport = [NSSearchPathForDirectoriesInDomains( - NSApplicationSupportDirectory, NSUserDomainMask, true) firstObject]; - NSString *cache = [NSSearchPathForDirectoriesInDomains( - NSCachesDirectory, NSUserDomainMask, true) firstObject]; - NSFileManager *fm = NSFileManager.defaultManager; - for (NSString *dir in @[ appSupport, cache ]) { - for (NSString *file in [fm enumeratorAtPath:dir]) { - NSError *error; - if (![fm removeItemAtPath:[dir stringByAppendingPathComponent:file] error:&error]) { - NSLog(@"[iOS-Swift] failed to remove data at app startup."); - } - } - } - } -} - -@end diff --git a/Samples/iOS-Swift/iOS-Swift/Profiling/ProfilingViewController.swift b/Samples/iOS-Swift/iOS-Swift/Profiling/ProfilingViewController.swift index ece70bdbbd..5634db0680 100644 --- a/Samples/iOS-Swift/iOS-Swift/Profiling/ProfilingViewController.swift +++ b/Samples/iOS-Swift/iOS-Swift/Profiling/ProfilingViewController.swift @@ -46,7 +46,7 @@ class ProfilingViewController: UIViewController, UITextFieldDelegate { let value = SentryBenchmarking.stopBenchmark()! valueTextField.isHidden = false valueTextField.text = value - print("[iOS-Swift] [ProfilingViewController] benchmarking results:\n\(value)") + print("[iOS-Swift] [debug] [ProfilingViewController] benchmarking results:\n\(value)") } @IBAction func startCPUWork(_ sender: UIButton) { @@ -130,7 +130,7 @@ extension ProfilingViewController { let url = file as! URL if url.absoluteString.contains(fileName) { block(url) - print("[iOS-Swift] [ProfilingViewController] removing file at \(url)") + print("[iOS-Swift] [debug] [ProfilingViewController] removing file at \(url)") try! FileManager.default.removeItem(at: url) return } @@ -150,7 +150,7 @@ extension ProfilingViewController { return } let contents = data.base64EncodedString() - print("[iOS-Swift] [ProfilingViewController] contents of file at \(file): \(String(describing: String(data: data, encoding: .utf8)))") + print("[iOS-Swift] [debug] [ProfilingViewController] contents of file at \(file): \(String(describing: String(data: data, encoding: .utf8)))") profilingUITestDataMarshalingTextField.text = contents profilingUITestDataMarshalingStatus.text = "✅" } diff --git a/Samples/iOS-Swift/iOS-SwiftUITests/ProfilingUITests.swift b/Samples/iOS-Swift/iOS-SwiftUITests/ProfilingUITests.swift index 8e0297de1d..a675c6c7c0 100644 --- a/Samples/iOS-Swift/iOS-SwiftUITests/ProfilingUITests.swift +++ b/Samples/iOS-Swift/iOS-SwiftUITests/ProfilingUITests.swift @@ -16,7 +16,7 @@ class ProfilingUITests: BaseUITest { app.launchArguments.append("--io.sentry.wipe-data") launchApp() - // First launch enables in-app profiling by setting traces/profiles sample rates to 1 (which is the default configuration in the sample app), but not launch profiling; assert that we did not write a config to allow the next launch to be profiled + // First launch enables in-app profiling by setting traces/profiles sample rates to 1 (which is the default configuration in the sample app), but not launch profiling; assert that we did not write a config to allow the next launch to be profiled. try performAssertions(shouldProfileThisLaunch: false, shouldProfileNextLaunch: false) // no profiling should be done on this launch; set the option to allow launch profiling for the next launch, keeping the default numerical sampling rates of 1 for traces and profiles @@ -28,20 +28,17 @@ class ProfilingUITests: BaseUITest { // this launch should not run the profiler; configure sampler functions returning 1 and numerical rates set to 0, which should result in a profile being taken as samplers override numerical rates try relaunchAndConfigureSubsequentLaunches(shouldProfileThisLaunch: false, shouldEnableLaunchProfilingOptionForNextLaunch: true, profilesSampleRate: 0, tracesSampleRate: 0, profilesSamplerValue: 1, tracesSamplerValue: 1) - // this launch has the configuration to run the profiler, but because swizzling is disabled, it will not be saved due to the ui.load transaction not being allowed to be created. it will also configure it to not run a profile for the next launch due to disabling swizzling, which would override the option to enable launch profiling. this specific scenario, where a previous launch configures a profile, but then something prevents the associated tx from running, is not automatically avoidable. in the future we will create a dummy transaction to attach the profile to. - try relaunchAndConfigureSubsequentLaunches(shouldProfileThisLaunch: false, shouldEnableLaunchProfilingOptionForNextLaunch: true, shouldDisableSwizzling: true) + // this launch should run the profiler; configure sampler functions returning 0 and numerical rates set to 0, which should result in no profile being taken next launch + try relaunchAndConfigureSubsequentLaunches(shouldProfileThisLaunch: true, shouldEnableLaunchProfilingOptionForNextLaunch: true, profilesSamplerValue: 0, tracesSamplerValue: 0) - // this launch should not run the profiler and configure it not to run the next launch due to disabling automatic performance tracking, which would override the option to enable launch profiling - try relaunchAndConfigureSubsequentLaunches(shouldProfileThisLaunch: false, shouldEnableLaunchProfilingOptionForNextLaunch: true, shouldDisableAutoPerformanceTracking: true) - - // this launch should not run the profiler and configure it not to run the next launch launch due to disabling UIViewController tracing, which would override the option to enable launch profiling - try relaunchAndConfigureSubsequentLaunches(shouldProfileThisLaunch: false, shouldEnableLaunchProfilingOptionForNextLaunch: true, shouldDisableUIViewControllerTracing: true) + // this launch should not run the profiler, but configure it to run the next launch + try relaunchAndConfigureSubsequentLaunches(shouldProfileThisLaunch: false, shouldEnableLaunchProfilingOptionForNextLaunch: true) - // this launch should not run the profiler and configure it not to run the next launch launch due to disabling tracing, which would override the option to enable launch profiling - try relaunchAndConfigureSubsequentLaunches(shouldProfileThisLaunch: false, shouldEnableLaunchProfilingOptionForNextLaunch: true, shouldDisableTracing: true) + // this launch should run the profiler, and configure it not to run the next launch due to disabling tracing, which would override the option to enable launch profiling + try relaunchAndConfigureSubsequentLaunches(shouldProfileThisLaunch: true, shouldEnableLaunchProfilingOptionForNextLaunch: true, shouldDisableTracing: true) - // make sure the profiler respects the last configuration not to run - try relaunchAndConfigureSubsequentLaunches(shouldProfileThisLaunch: false, shouldEnableLaunchProfilingOptionForNextLaunch: true) + // make sure the profiler respects the last configuration not to run; don't let another config file get written + try relaunchAndConfigureSubsequentLaunches(shouldProfileThisLaunch: false, shouldEnableLaunchProfilingOptionForNextLaunch: false) } /** diff --git a/Sources/Sentry/Profiling/SentryLaunchProfiling.m b/Sources/Sentry/Profiling/SentryLaunchProfiling.m index 6fd50c1bb7..b89fc60bd4 100644 --- a/Sources/Sentry/Profiling/SentryLaunchProfiling.m +++ b/Sources/Sentry/Profiling/SentryLaunchProfiling.m @@ -15,15 +15,17 @@ # import "SentrySamplerDecision.h" # import "SentrySampling.h" # import "SentrySamplingContext.h" +# import "SentryTraceOrigins.h" +# import "SentryTracer+Private.h" # import "SentryTracerConfiguration.h" -# import "SentryTransactionContext.h" +# import "SentryTransactionContext+Private.h" + +NS_ASSUME_NONNULL_BEGIN BOOL isTracingAppLaunch; -SentryId *_Nullable appLaunchTraceId; -NSObject *appLaunchTraceLock; -uint64_t appLaunchSystemTime; NSString *const kSentryLaunchProfileConfigKeyTracesSampleRate = @"traces"; NSString *const kSentryLaunchProfileConfigKeyProfilesSampleRate = @"profiles"; +static SentryTracer *_Nullable launchTracer; # pragma mark - Private @@ -36,29 +38,11 @@ SentryLaunchProfileConfig shouldProfileNextLaunch(SentryOptions *options) { - BOOL shouldProfileNextLaunch = options.enableAppLaunchProfiling -# if SENTRY_UIKIT_AVAILABLE - && options.enableUIViewControllerTracing && options.enableSwizzling -# endif // SENTRY_UIKIT_AVAILABLE - && options.enableAutoPerformanceTracing && options.enableTracing; + BOOL shouldProfileNextLaunch = options.enableAppLaunchProfiling && options.enableTracing; if (!shouldProfileNextLaunch) { -# if SENTRY_UIKIT_AVAILABLE - SENTRY_LOG_DEBUG( - @"Won't profile next launch due to specified options configuration: " - @"options.enableAppLaunchProfiling: %d; options.enableAutoPerformanceTracing: %d; " - @"options.enableUIViewControllerTracing: %d; options.enableSwizzling: %d; " - @"options.enableTracing: %d", - options.enableAppLaunchProfiling, options.enableAutoPerformanceTracing, - options.enableUIViewControllerTracing, options.enableSwizzling, options.enableTracing); -# else - SENTRY_LOG_DEBUG( - @"Won't profile next launch due to specified options configuration: " - @"options.enableAppLaunchProfiling: %d; options.enableAutoPerformanceTracing: %d; " - @"options.enableSwizzling: %d; options.enableTracing: %d", - options.enableAppLaunchProfiling, options.enableAutoPerformanceTracing, - options.enableSwizzling, options.enableTracing); -# endif // SENTRY_UIKIT_AVAILABLE - + SENTRY_LOG_DEBUG(@"Won't profile next launch due to specified options configuration: " + @"options.enableAppLaunchProfiling: %d; options.enableTracing: %d", + options.enableAppLaunchProfiling, options.enableTracing); return (SentryLaunchProfileConfig) { NO, nil, nil }; } @@ -92,24 +76,44 @@ { [SentryDependencyContainer.sharedInstance.dispatchQueueWrapper dispatchAsyncWithBlock:^{ SentryLaunchProfileConfig config = shouldProfileNextLaunch(options); - if (config.shouldProfile) { - NSMutableDictionary *configDict = - [NSMutableDictionary dictionary]; - configDict[kSentryLaunchProfileConfigKeyTracesSampleRate] - = config.tracesDecision.sampleRate; - configDict[kSentryLaunchProfileConfigKeyProfilesSampleRate] - = config.profilesDecision.sampleRate; - writeAppLaunchProfilingConfigFile(configDict); - } else { - if (isTracingAppLaunch) { - backupAppLaunchProfilingConfigFile(); - } else { - removeAppLaunchProfilingConfigFile(); - } + if (!config.shouldProfile) { + removeAppLaunchProfilingConfigFile(); + return; } + + NSMutableDictionary *configDict = + [NSMutableDictionary dictionary]; + configDict[kSentryLaunchProfileConfigKeyTracesSampleRate] + = config.tracesDecision.sampleRate; + configDict[kSentryLaunchProfileConfigKeyProfilesSampleRate] + = config.profilesDecision.sampleRate; + writeAppLaunchProfilingConfigFile(configDict); }]; } +SentryTransactionContext * +context(NSNumber *tracesRate) +{ + SentryTransactionContext *context = + [[SentryTransactionContext alloc] initWithName:@"launch" + nameSource:kSentryTransactionNameSourceCustom + operation:@"app.lifecycle" + origin:SentryTraceOriginAutoAppStartProfile + sampled:kSentrySampleDecisionYes]; + context.sampleRate = tracesRate; + return context; +} + +SentryTracerConfiguration * +config(NSNumber *profilesRate) +{ + SentryTracerConfiguration *config = [SentryTracerConfiguration defaultConfiguration]; + config.profilesSamplerDecision = + [[SentrySamplerDecision alloc] initWithDecision:kSentrySampleDecisionYes + forSampleRate:profilesRate]; + return config; +} + void startLaunchProfile(void) { @@ -118,7 +122,8 @@ // directly to customers, and we'll want to ensure it only runs once. dispatch_once is an // efficient operation so it's fine to leave this in the launch path in any case. dispatch_once(&onceToken, ^{ - if (!appLaunchProfileConfigFileExists()) { + isTracingAppLaunch = appLaunchProfileConfigFileExists(); + if (!isTracingAppLaunch) { return; } @@ -129,34 +134,36 @@ [SentryLog configure:YES diagnosticLevel:kSentryLevelDebug]; # endif // defined(DEBUG) - appLaunchSystemTime = SentryDependencyContainer.sharedInstance.dateProvider.systemTime; - appLaunchTraceLock = [[NSObject alloc] init]; - appLaunchTraceId = [[SentryId alloc] init]; - - SENTRY_LOG_INFO(@"Starting app launch profile at %llu", appLaunchSystemTime); + NSDictionary *rates = appLaunchProfileConfiguration(); + NSNumber *profilesRate = rates[kSentryLaunchProfileConfigKeyProfilesSampleRate]; + NSNumber *tracesRate = rates[kSentryLaunchProfileConfigKeyTracesSampleRate]; + if (profilesRate == nil || tracesRate == nil) { + SENTRY_LOG_DEBUG( + @"Received a nil configured launch sample rate, will not trace or profile."); + return; + } - // don't worry about synchronizing the write here, as there should be no other tracing - // activity going on this early in the process. this codepath is also behind a dispatch_once - isTracingAppLaunch = [SentryProfiler startWithTracer:appLaunchTraceId]; + SENTRY_LOG_INFO(@"Starting app launch profile."); + launchTracer = [[SentryTracer alloc] initWithTransactionContext:context(tracesRate) + hub:nil + configuration:config(profilesRate)]; }); } -BOOL -injectLaunchSamplerDecisions( - SentryTransactionContext *transactionContext, SentryTracerConfiguration *configuration) +void +stopLaunchProfile(SentryHub *hub) { - NSDictionary *rates = appLaunchProfileConfiguration(); - removeAppLaunchProfilingConfigBackupFile(); - NSNumber *profilesRate = rates[kSentryLaunchProfileConfigKeyProfilesSampleRate]; - NSNumber *tracesRate = rates[kSentryLaunchProfileConfigKeyTracesSampleRate]; - if (profilesRate == nil || tracesRate == nil) { - return NO; + if (launchTracer == nil) { + SENTRY_LOG_DEBUG(@"No launch tracer present to stop."); + return; } - configuration.profilesSamplerDecision = - [[SentrySamplerDecision alloc] initWithDecision:kSentrySampleDecisionYes - forSampleRate:profilesRate]; - transactionContext.sampleRate = rates[kSentryLaunchProfileConfigKeyTracesSampleRate]; - return YES; + + SENTRY_LOG_DEBUG(@"Finishing launch tracer."); + + launchTracer.hub = hub; + [launchTracer finish]; } +NS_ASSUME_NONNULL_END + #endif // SENTRY_TARGET_PROFILING_SUPPORTED diff --git a/Sources/Sentry/Profiling/SentryProfilerState.mm b/Sources/Sentry/Profiling/SentryProfilerState.mm index 59032cc9cd..77c14bb74b 100644 --- a/Sources/Sentry/Profiling/SentryProfilerState.mm +++ b/Sources/Sentry/Profiling/SentryProfilerState.mm @@ -115,6 +115,10 @@ - (void)appendBacktrace:(const Backtrace &)backtrace # endif const auto stack = [NSMutableArray array]; +# if defined(DEBUG) + printf("[Sentry] [debug] [SentryProfilerState:%d] stack frames: %lu; stack thread: %s\n", + __LINE__, backtrace.addresses.size(), threadID.UTF8String); +# endif // defined(DEBUG) for (std::vector::size_type backtraceAddressIdx = 0; backtraceAddressIdx < backtrace.addresses.size(); backtraceAddressIdx++) { const auto instructionAddress @@ -125,8 +129,12 @@ - (void)appendBacktrace:(const Backtrace &)backtrace const auto frame = [NSMutableDictionary dictionary]; frame[@"instruction_addr"] = instructionAddress; # if defined(DEBUG) - frame[@"function"] + const auto functionName = parseBacktraceSymbolsFunctionName(symbols[backtraceAddressIdx]); + printf("[Sentry] [debug] [SentryProfilerState:%d] function name: %s; instruction " + "address: %s\n", + __LINE__, functionName.UTF8String, instructionAddress.UTF8String); + frame[@"function"] = functionName; # endif const auto newFrameIndex = @(state.frames.count); [stack addObject:newFrameIndex]; @@ -137,6 +145,7 @@ - (void)appendBacktrace:(const Backtrace &)backtrace } } # if defined(DEBUG) + printf("-----finished backtrace-----\n"); free(symbols); # endif diff --git a/Sources/Sentry/SentryFileManager.m b/Sources/Sentry/SentryFileManager.m index ba382eb2cb..b89b98a071 100644 --- a/Sources/Sentry/SentryFileManager.m +++ b/Sources/Sentry/SentryFileManager.m @@ -778,15 +778,14 @@ - (void)createPathsWithOptions:(SentryOptions *)options NSDictionary *_Nullable appLaunchProfileConfiguration(void) { - NSURL *url = launchProfileConfigBackupFileURL(); + NSURL *url = launchProfileConfigFileURL(); if (![[NSFileManager defaultManager] fileExistsAtPath:url.path]) { return nil; } NSError *error; - NSDictionary *config = [NSDictionary - dictionaryWithContentsOfURL:launchProfileConfigBackupFileURL() - error:&error]; + NSDictionary *config = + [NSDictionary dictionaryWithContentsOfURL:url error:&error]; SENTRY_CASSERT( error == nil, @"Encountered error trying to retrieve app launch profile config: %@", error); return config; @@ -811,31 +810,6 @@ - (void)createPathsWithOptions:(SentryOptions *)options { _non_thread_safe_removeFileAtPath(launchProfileConfigFileURL().path); } - -void -removeAppLaunchProfilingConfigBackupFile(void) -{ - _non_thread_safe_removeFileAtPath(launchProfileConfigBackupFileURL().path); -} - -void -backupAppLaunchProfilingConfigFile(void) -{ - NSFileManager *fm = [NSFileManager defaultManager]; - NSString *fromPath = launchProfileConfigFileURL().path; - if (!SENTRY_CASSERT_RETURN([fm fileExistsAtPath:fromPath], - @"Expect to have a current launch profile config to use for subsequent transaction.")) { - return; - } - - NSString *toPath = launchProfileConfigBackupFileURL().path; - _non_thread_safe_removeFileAtPath(toPath); - - NSError *error; - SENTRY_CASSERT([fm moveItemAtPath:fromPath toPath:toPath error:&error], - @"Failed to backup launch profile config file for use to set up associated transaction: %@", - error); -} #endif // SENTRY_TARGET_PROFILING_SUPPORTED @end diff --git a/Sources/Sentry/SentryHub.m b/Sources/Sentry/SentryHub.m index 315589e867..ef1727fb72 100644 --- a/Sources/Sentry/SentryHub.m +++ b/Sources/Sentry/SentryHub.m @@ -31,10 +31,6 @@ # import "SentryUIViewControllerPerformanceTracker.h" #endif // SENTRY_HAS_UIKIT -#if SENTRY_TARGET_PROFILING_SUPPORTED -# import "SentryLaunchProfiling.h" -#endif // SENTRY_TARGET_PROFILING_SUPPORTED" - NS_ASSUME_NONNULL_BEGIN @interface @@ -373,36 +369,22 @@ - (SentryTracer *)startTransactionWithContext:(SentryTransactionContext *)transa customSamplingContext:(NSDictionary *)customSamplingContext configuration:(SentryTracerConfiguration *)configuration { + SentrySamplingContext *samplingContext = + [[SentrySamplingContext alloc] initWithTransactionContext:transactionContext + customSamplingContext:customSamplingContext]; - BOOL applySampling = YES; + SentrySamplerDecision *tracesSamplerDecision + = sampleTrace(samplingContext, self.client.options); + transactionContext = [self transactionContext:transactionContext + withSampled:tracesSamplerDecision.decision]; + transactionContext.sampleRate = tracesSamplerDecision.sampleRate; #if SENTRY_TARGET_PROFILING_SUPPORTED - // in order to sample launch profiles, we compute the trace/profile sampling decisions in - // SentrySDK.startWithOptions and if the decisions evaluate to yes, we write a config file with - // the actual sampling rates used, and the presence of this file indicates to the profiler on - // the next launch that it should take the profile. then, we need a transaction to attach it to, - // so we also don't want to reevaluate the sampling logic and possibly sample out the - // transaction it would be attached to, thus rendering the profile as wasted overhead for the - // launch. here we can assume a sample decision of yes, and we retrieve the sample rates to - // inject into the context/configuration - applySampling - = !isTracingAppLaunch || injectLaunchSamplerDecisions(transactionContext, configuration); -#endif // SENTRY_TARGET_PROFILING_SUPPORTED - - if (applySampling) { - SentrySamplingContext *samplingContext = - [[SentrySamplingContext alloc] initWithTransactionContext:transactionContext - customSamplingContext:customSamplingContext]; - SentrySamplerDecision *tracesSamplerDecision - = sampleTrace(samplingContext, _client.options); - transactionContext = [self transactionContext:transactionContext - withSampled:tracesSamplerDecision.decision]; - transactionContext.sampleRate = tracesSamplerDecision.sampleRate; -#if SENTRY_TARGET_PROFILING_SUPPORTED - configuration.profilesSamplerDecision - = sampleProfile(samplingContext, tracesSamplerDecision, _client.options); -#endif // SENTRY_TARGET_PROFILING_SUPPORTED - } + SentrySamplerDecision *profilesSamplerDecision + = sampleProfile(samplingContext, tracesSamplerDecision, self.client.options); + + configuration.profilesSamplerDecision = profilesSamplerDecision; +#endif // SENTRY_TARGET_PROFILING_SUPPORTED" SentryTracer *tracer = [[SentryTracer alloc] initWithTransactionContext:transactionContext hub:self diff --git a/Sources/Sentry/SentryOptions.m b/Sources/Sentry/SentryOptions.m index 1a5fff4db2..03d90c2b65 100644 --- a/Sources/Sentry/SentryOptions.m +++ b/Sources/Sentry/SentryOptions.m @@ -34,7 +34,6 @@ #if SENTRY_HAS_METRIC_KIT # import "SentryMetricKitIntegration.h" #endif // SENTRY_HAS_METRIC_KIT - NSString *const kSentryDefaultEnvironment = @"production"; @implementation SentryOptions { diff --git a/Sources/Sentry/SentryProfileTimeseries.mm b/Sources/Sentry/SentryProfileTimeseries.mm index 0a9ec459e1..4e9344d423 100644 --- a/Sources/Sentry/SentryProfileTimeseries.mm +++ b/Sources/Sentry/SentryProfileTimeseries.mm @@ -46,6 +46,8 @@ return nil; } + SENTRY_LOG_DEBUG(@"Finding relevant samples from %lu total.", (unsigned long)samples.count); + const auto firstIndex = [samples indexOfObjectWithOptions:NSEnumerationConcurrent passingTest:^BOOL(SentrySample *_Nonnull sample, NSUInteger idx, diff --git a/Sources/Sentry/SentryProfiler.mm b/Sources/Sentry/SentryProfiler.mm index a743697ab2..b98753280e 100644 --- a/Sources/Sentry/SentryProfiler.mm +++ b/Sources/Sentry/SentryProfiler.mm @@ -50,10 +50,17 @@ # import # endif // SENTRY_HAS_UIKIT -# if defined(TEST) || defined(TESTCI) +# if defined(TEST) || defined(TESTCI) || defined(DEBUG) # import "SentryFileManager.h" +# import "SentryInternalDefines.h" # import "SentryLaunchProfiling.h" +/** + * A category that overrides its `+[load]` method to deliberately take a long time to run, so we can + * see it show up in profile stack traces. Categories' `+[load]` methods are guaranteed to be called + * after all of a module's normal class' overrides, so we can be confident the ordering will always + * have started the launch profiler by the time this runs. + */ @interface SentryProfiler (SlowLoad) @end @@ -62,17 +69,19 @@ SentryProfiler (SlowLoad) + (void)load { + SENTRY_LOG_DEBUG(@"Starting slow load method"); if ([NSProcessInfo.processInfo.arguments containsObject:@"--io.sentry.slow-load-method"]) { NSMutableString *a = [NSMutableString string]; // 1,000,000 iterations takes about 225 milliseconds in the iPhone 15 simulator on an // M2 macbook pro; we might have to adapt this for CI - for (NSUInteger i = 0; i < 1000000; i++) { + for (NSUInteger i = 0; i < 4000000; i++) { [a appendFormat:@"%d", arc4random() % 12345]; } } + SENTRY_LOG_DEBUG(@"Finishing slow load method"); } @end -# endif // defined(TEST) || defined(TESTCI) +# endif // defined(TEST) || defined(TESTCI) || defined(DEBUG) const int kSentryProfilerFrequencyHz = 101; NSTimeInterval kSentryProfilerTimeoutInterval = 30; @@ -400,11 +409,13 @@ + (void)recordMetrics + (nullable SentryEnvelopeItem *)createProfilingEnvelopeItemForTransaction: (SentryTransaction *)transaction { + SENTRY_LOG_DEBUG(@"Creating profiling envelope item"); const auto payload = [self collectProfileBetween:transaction.startSystemTime and:transaction.endSystemTime forTrace:transaction.trace.internalID onHub:transaction.trace.hub]; if (payload == nil) { + SENTRY_LOG_DEBUG(@"Payload was empty, will not create a profiling envelope item."); return nil; } @@ -426,7 +437,7 @@ + (nullable SentryEnvelopeItem *)createEnvelopeItemForProfilePayload: return [[SentryEnvelopeItem alloc] initWithHeader:header data:JSONData]; } -# if defined(TEST) || defined(TESTCI) +# if defined(TEST) || defined(TESTCI) || defined(DEBUG) void writeProfileFile(NSDictionary *payload) { @@ -450,20 +461,25 @@ + (nullable SentryEnvelopeItem *)createEnvelopeItemForProfilePayload: NSString *pathToWrite; if (isTracingAppLaunch) { + SENTRY_LOG_DEBUG(@"Writing app launch profile."); pathToWrite = [appSupportDirPath stringByAppendingPathComponent:@"launchProfile"]; - if ([fm fileExistsAtPath:pathToWrite]) { - SENTRY_LOG_DEBUG(@"Already a launch profile file present."); - return; - } } else { + SENTRY_LOG_DEBUG(@"Overwriting last non-launch profile."); pathToWrite = [appSupportDirPath stringByAppendingPathComponent:@"profile"]; } - SENTRY_LOG_DEBUG(@"Writing app launch profile to file."); + if ([fm fileExistsAtPath:pathToWrite]) { + SENTRY_LOG_DEBUG(@"Already a%@ profile file present; make sure to remove them right after " + @"using them, and that tests clean state in between so there isn't " + @"leftover config producing one when it isn't expected.", + isTracingAppLaunch ? @" launch" : @""); + } + + SENTRY_LOG_DEBUG(@"Writing%@ profile to file.", isTracingAppLaunch ? @" launch" : @""); SENTRY_CASSERT( [data writeToFile:pathToWrite atomically:YES], @"Failed to write profile to test file"); } -# endif // defined(TEST) || defined(TESTCI) +# endif // defined(TEST) || defined(TESTCI) || defined(DEBUG) + (nullable NSMutableDictionary *)collectProfileBetween:(uint64_t)startSystemTime and:(uint64_t)endSystemTime @@ -477,9 +493,9 @@ + (nullable SentryEnvelopeItem *)createEnvelopeItemForProfilePayload: const auto payload = [profiler serializeBetween:startSystemTime and:endSystemTime onHub:hub]; -# if defined(TEST) || defined(TESTCI) +# if defined(TEST) || defined(TESTCI) || defined(DEBUG) writeProfileFile(payload); -# endif // defined(TEST) || defined(TESTCI) +# endif // defined(TEST) || defined(TESTCI) || defined(DEBUG) return payload; } diff --git a/Sources/Sentry/SentrySDK.m b/Sources/Sentry/SentrySDK.m index 57bc6487b4..4493999fc0 100644 --- a/Sources/Sentry/SentrySDK.m +++ b/Sources/Sentry/SentrySDK.m @@ -186,7 +186,8 @@ + (void)startWithOptions:(SentryOptions *)options = options.initialScope([[SentryScope alloc] initWithMaxBreadcrumbs:options.maxBreadcrumbs]); // The Hub needs to be initialized with a client so that closing a session // can happen. - [SentrySDK setCurrentHub:[[SentryHub alloc] initWithClient:newClient andScope:scope]]; + SentryHub *hub = [[SentryHub alloc] initWithClient:newClient andScope:scope]; + [SentrySDK setCurrentHub:hub]; SENTRY_LOG_DEBUG(@"SDK initialized! Version: %@", SentryMeta.versionString); SENTRY_LOG_DEBUG(@"Dispatching init work required to run on main thread."); @@ -200,11 +201,14 @@ + (void)startWithOptions:(SentryOptions *)options #if TARGET_OS_IOS && SENTRY_HAS_UIKIT [SentryDependencyContainer.sharedInstance.uiDeviceWrapper start]; #endif // TARGET_OS_IOS && SENTRY_HAS_UIKIT - }]; #if SENTRY_TARGET_PROFILING_SUPPORTED - configureLaunchProfiling(options); + [SentryDependencyContainer.sharedInstance.dispatchQueueWrapper dispatchAsyncWithBlock:^{ + stopLaunchProfile(hub); + configureLaunchProfiling(options); + }]; #endif // SENTRY_TARGET_PROFILING_SUPPORTED + }]; } + (void)startWithConfigureOptions:(void (^)(SentryOptions *options))configureOptions diff --git a/Sources/Sentry/SentryTracer.m b/Sources/Sentry/SentryTracer.m index f5a000f67e..883efc3490 100644 --- a/Sources/Sentry/SentryTracer.m +++ b/Sources/Sentry/SentryTracer.m @@ -190,30 +190,17 @@ - (instancetype)initWithTransactionContext:(SentryTransactionContext *)transacti #endif // SENTRY_HAS_UIKIT #if SENTRY_TARGET_PROFILING_SUPPORTED - BOOL isTracingAppLaunchValue; - @synchronized(appLaunchTraceLock) { - isTracingAppLaunchValue = isTracingAppLaunch; - } - if (isTracingAppLaunchValue) { - SENTRY_ASSERT(appLaunchTraceId != nil, @"Expected an app launch trace ID."); - _internalID = appLaunchTraceId; - SENTRY_LOG_DEBUG( - @"App launch profile in progress, will attach to trace %@", _internalID.sentryIdString); - appLaunchTraceId = nil; - _isProfiling = isTracingAppLaunchValue; - _startSystemTime = appLaunchSystemTime; - } else { - if (_configuration.profilesSamplerDecision.decision == kSentrySampleDecisionYes) { - _internalID = [[SentryId alloc] init]; - if ((_isProfiling = [SentryProfiler startWithTracer:_internalID])) { - SENTRY_LOG_DEBUG(@"Started profiler for trace %@", _internalID.sentryIdString); - } - _startSystemTime = SentryDependencyContainer.sharedInstance.dateProvider.systemTime; + if (_configuration.profilesSamplerDecision.decision == kSentrySampleDecisionYes) { + _internalID = [[SentryId alloc] init]; + if ((_isProfiling = [SentryProfiler startWithTracer:_internalID])) { + SENTRY_LOG_DEBUG(@"Started profiler for trace %@ with internal id %@", + transactionContext.traceId.sentryIdString, _internalID.sentryIdString); } + _startSystemTime = SentryDependencyContainer.sharedInstance.dateProvider.systemTime; } #endif // SENTRY_TARGET_PROFILING_SUPPORTED - SENTRY_LOG_DEBUG(@"Started tracer with id: %@", transactionContext.traceId); + SENTRY_LOG_DEBUG(@"Started tracer with id: %@", transactionContext.traceId.sentryIdString); return self; } @@ -526,10 +513,13 @@ - (void)finishInternal { [self cancelDeadlineTimer]; if (self.isFinished) { + SENTRY_LOG_DEBUG(@"Tracer %@ was already finished.", _traceContext.traceId.sentryIdString); return; } @synchronized(self) { if (self.isFinished) { + SENTRY_LOG_DEBUG(@"Tracer %@ was already finished after synchronizing.", + _traceContext.traceId.sentryIdString); return; } // Keep existing status of auto generated transactions if set by the user. @@ -574,7 +564,15 @@ - (void)finishInternal return; } - if (_hub == nil) { + BOOL shouldBailWithNilHub = _hub == nil; +#if SENTRY_TARGET_PROFILING_SUPPORTED + if (isTracingAppLaunch) { + shouldBailWithNilHub = NO; + } +#endif // SENTRY_TARGET_PROFILING_SUPPORTED + if (shouldBailWithNilHub) { + SENTRY_LOG_DEBUG( + @"Hub was nil for tracer %@, nothing to do.", _traceContext.traceId.sentryIdString); return; } @@ -611,15 +609,6 @@ - (void)finishInternal #if SENTRY_TARGET_PROFILING_SUPPORTED if (self.isProfiling) { [self captureTransactionWithProfile:transaction]; - - // as long as this isn't used for any conditional branching logic, and is just being set to - // NO, we don't need to synchronize the read here - if (isTracingAppLaunch) { - @synchronized(appLaunchTraceLock) { - isTracingAppLaunch = NO; - } - } - return; } #endif // SENTRY_TARGET_PROFILING_SUPPORTED diff --git a/Sources/Sentry/include/SentryFileManager.h b/Sources/Sentry/include/SentryFileManager.h index 8d3322bf01..cb774591c7 100644 --- a/Sources/Sentry/include/SentryFileManager.h +++ b/Sources/Sentry/include/SentryFileManager.h @@ -124,20 +124,6 @@ SENTRY_EXTERN void writeAppLaunchProfilingConfigFile( */ SENTRY_EXTERN void removeAppLaunchProfilingConfigFile(void); -/** - * Save a current launch profile config file for use when the associated transaction needs to be - * started. This is when checking @c SentryOptions for whether to write a config for the next - * launch; if there's no current app launch profile going, then we'd simply remove whatever config - * file may exist, but if there is a launch profile going, we need to save the config that it used. - */ -SENTRY_EXTERN void backupAppLaunchProfilingConfigFile(void); - -/** - * Clean up the backup file saved off in the case that an app launch is in-flight when the SDK - * configures subsequent launches not to profile, requiring the current config to be saved for the - * associated transaction. This can be cleaned up when that transaction is started. - */ -SENTRY_EXTERN void removeAppLaunchProfilingConfigBackupFile(void); #endif // SENTRY_TARGET_PROFILING_SUPPORTED @end diff --git a/Sources/Sentry/include/SentryLaunchProfiling.h b/Sources/Sentry/include/SentryLaunchProfiling.h index a67ea4d497..d7d9863439 100644 --- a/Sources/Sentry/include/SentryLaunchProfiling.h +++ b/Sources/Sentry/include/SentryLaunchProfiling.h @@ -4,6 +4,7 @@ #if SENTRY_TARGET_PROFILING_SUPPORTED +@class SentryHub; @class SentryId; @class SentryOptions; @class SentryTracerConfiguration; @@ -12,11 +13,12 @@ NS_ASSUME_NONNULL_BEGIN SENTRY_EXTERN BOOL isTracingAppLaunch; -SENTRY_EXTERN SentryId *_Nullable appLaunchTraceId; -SENTRY_EXTERN uint64_t appLaunchSystemTime; -SENTRY_EXTERN NSObject *appLaunchTraceLock; -void startLaunchProfile(void); +/** Try to start a profiled trace for this app launch, if the configuration allows. */ +SENTRY_EXTERN void startLaunchProfile(void); + +/** Stop any profiled trace that may be in flight from the start of the app launch. */ +void stopLaunchProfile(SentryHub *hub); /** * Write a file to disk containing sample rates for profiles and traces. The presence of this file @@ -26,14 +28,6 @@ void startLaunchProfile(void); */ void configureLaunchProfiling(SentryOptions *options); -/** - * If there were previously persisted sampling rates sed when decidign the launch profile/trace, - * inject them into the context and configuration. - * @return @c YES if persisted rates were found and injected, @c NO otherwise. - */ -BOOL injectLaunchSamplerDecisions( - SentryTransactionContext *transactionContext, SentryTracerConfiguration *configuration); - NS_ASSUME_NONNULL_END #endif // SENTRY_TARGET_PROFILING_SUPPORTED diff --git a/Sources/Sentry/include/SentryTraceOrigins.h b/Sources/Sentry/include/SentryTraceOrigins.h index 9be87b76a7..7922cdb9a7 100644 --- a/Sources/Sentry/include/SentryTraceOrigins.h +++ b/Sources/Sentry/include/SentryTraceOrigins.h @@ -4,6 +4,7 @@ static NSString *const SentryTraceOriginManual = @"manual"; static NSString *const SentryTraceOriginUIEventTracker = @"auto.ui.event_tracker"; static NSString *const SentryTraceOriginAutoAppStart = @"auto.app.start"; +static NSString *const SentryTraceOriginAutoAppStartProfile = @"auto.app.start.profile"; static NSString *const SentryTraceOriginAutoNSData = @"auto.file.ns_data"; static NSString *const SentryTraceOriginAutoDBCoreData = @"auto.db.core_data"; static NSString *const SentryTraceOriginAutoHttpNSURLSession = @"auto.http.ns_url_session"; diff --git a/Tests/SentryProfilerTests/SentryAppLaunchProfilingTests.m b/Tests/SentryProfilerTests/SentryAppLaunchProfilingTests.m index f622f9a0c9..45270a23cb 100644 --- a/Tests/SentryProfilerTests/SentryAppLaunchProfilingTests.m +++ b/Tests/SentryProfilerTests/SentryAppLaunchProfilingTests.m @@ -3,6 +3,8 @@ #import "SentryOptions+Private.h" #import "SentryProfilingConditionals.h" #import "SentrySDK+Tests.h" +#import "SentryTraceOrigins.h" +#import "SentryTransactionContext.h" #import #if SENTRY_TARGET_PROFILING_SUPPORTED @@ -12,6 +14,14 @@ @interface SentryAppLaunchProfilingTests : XCTestCase @implementation SentryAppLaunchProfilingTests +- (void)testLaunchProfileTransactionContext +{ + SentryTransactionContext *actualContext = context(@1); + XCTAssertEqual(actualContext.nameSource, kSentryTransactionNameSourceCustom); + XCTAssert([actualContext.origin isEqualToString:SentryTraceOriginAutoAppStartProfile]); + XCTAssert(actualContext.sampled); +} + # define SENTRY_OPTION(name, value) \ NSStringFromSelector(@selector(name)) \ : value @@ -107,44 +117,7 @@ - (void)testSettingProfilesSampleRateTo0DisablesAppLaunchProfiling @"but profiles sample rate of 0 should not enable launch profiling"); } -- (void)testDisablingAutoPerformanceTracingOptionDisablesAppLaunchProfiling -{ - XCTAssertFalse( - shouldProfileNextLaunch( - [self defaultLaunchProfilingOptionsWithOverrides:@{ SENTRY_OPTION( - enableAutoPerformanceTracing, - @NO) }]) - .shouldProfile, - @"Default options with app launch profiling and tracing enabled, traces and profiles " - @"sample rates of 1, but automatic performance tracing disabled should not enable launch " - @"profiling"); -} - -# if SENTRY_HAS_UIKIT -- (void)testDisablingSwizzlingOptionDisablesAppLaunchProfiling -{ - XCTAssertFalse( - shouldProfileNextLaunch( - [self defaultLaunchProfilingOptionsWithOverrides:@{ SENTRY_OPTION( - enableSwizzling, @NO) }]) - .shouldProfile, - @"Default options with app launch profiling and tracing enabled, traces and profiles " - @"sample rates of 1, but swizzling disabled should not enable launch profiling"); -} - -- (void)testDisablingUIViewControllerTracingOptionDisablesAppLaunchProfiling -{ - XCTAssertFalse( - shouldProfileNextLaunch( - [self defaultLaunchProfilingOptionsWithOverrides:@{ SENTRY_OPTION( - enableUIViewControllerTracing, - @NO) }]) - .shouldProfile, - @"Default options with app launch profiling and tracing enabled, traces and profiles " - @"sample rates of 1, but UIViewController tracing disabled should not enable launch " - @"profiling"); -} -# endif // SENTRY_HAS_UIKIT +# pragma mark - Private - (SentryOptions *)defaultLaunchProfilingOptionsWithOverrides: (NSDictionary *)overrides diff --git a/Tests/SentryTests/Helper/SentryFileManager+Test.h b/Tests/SentryTests/Helper/SentryFileManager+Test.h index 1285179f52..aa102040fb 100644 --- a/Tests/SentryTests/Helper/SentryFileManager+Test.h +++ b/Tests/SentryTests/Helper/SentryFileManager+Test.h @@ -3,7 +3,6 @@ NS_ASSUME_NONNULL_BEGIN SENTRY_EXTERN NSURL *launchProfileConfigFileURL(void); -SENTRY_EXTERN NSURL *launchProfileConfigBackupFileURL(void); @interface SentryFileManager () diff --git a/Tests/SentryTests/Helper/SentryFileManagerTests.swift b/Tests/SentryTests/Helper/SentryFileManagerTests.swift index 4ead32d4e5..809329bd30 100644 --- a/Tests/SentryTests/Helper/SentryFileManagerTests.swift +++ b/Tests/SentryTests/Helper/SentryFileManagerTests.swift @@ -730,7 +730,7 @@ extension SentryFileManagerTests { func testAppLaunchProfileConfiguration() throws { let expectedTracesSampleRate = 0.12 let expectedProfilesSampleRate = 0.34 - try ensureAppLaunchProfileConfig(tracesSampleRate: expectedTracesSampleRate, profilesSampleRate: expectedProfilesSampleRate, backup: true) + try ensureAppLaunchProfileConfig(tracesSampleRate: expectedTracesSampleRate, profilesSampleRate: expectedProfilesSampleRate) let config = appLaunchProfileConfiguration() let actualTracesSampleRate = try XCTUnwrap(config?[kSentryLaunchProfileConfigKeyTracesSampleRate]).doubleValue let actualProfilesSampleRate = try XCTUnwrap(config?[kSentryLaunchProfileConfigKeyProfilesSampleRate]).doubleValue @@ -740,7 +740,7 @@ extension SentryFileManagerTests { // if a file isn't present when we expect it to be, like if there was an issue when we went to write it to disk func testAppLaunchProfileConfiguration_noConfigurationExists() throws { - try ensureAppLaunchProfileConfig(exists: false, backup: true) + try ensureAppLaunchProfileConfig(exists: false) expect(appLaunchProfileConfiguration()) == nil } @@ -795,53 +795,12 @@ extension SentryFileManagerTests { removeAppLaunchProfilingConfigFile() expect(NSDictionary(contentsOf: launchProfileConfigFileURL())) == nil } - - func testBackupAppLaunchProfilingConfigFile() throws { - try ensureAppLaunchProfileConfig(exists: true) - try ensureAppLaunchProfileConfig(exists: false, backup: true) - expect(NSDictionary(contentsOf: launchProfileConfigFileURL())) != nil - expect(NSDictionary(contentsOf: launchProfileConfigBackupFileURL())) == nil - backupAppLaunchProfilingConfigFile() - expect(NSDictionary(contentsOf: launchProfileConfigFileURL())) == nil - expect(NSDictionary(contentsOf: launchProfileConfigBackupFileURL())) != nil - } - - // if a file is still present in the backup location, like if a crash occurred before it could be removed, or an error occurred when trying to remove it, make sure we overwrite it - func testBackupAppLaunchProfilingConfigFile_anotherBackupFilePresent() throws { - try ensureAppLaunchProfileConfig(exists: true, tracesSampleRate: 0.1, profilesSampleRate: 0.2) - try ensureAppLaunchProfileConfig(exists: true, tracesSampleRate: 0.3, profilesSampleRate: 0.4, backup: true) - expect(NSDictionary(contentsOf: launchProfileConfigFileURL())) != nil - expect(NSDictionary(contentsOf: launchProfileConfigBackupFileURL())) != nil - backupAppLaunchProfilingConfigFile() - expect(NSDictionary(contentsOf: launchProfileConfigFileURL())) == nil - expect(NSDictionary(contentsOf: launchProfileConfigBackupFileURL())) != nil - } - - func testRemoveAppLaunchProfilingConfigBackupFile() throws { - try ensureAppLaunchProfileConfig(exists: true, backup: true) - expect(NSDictionary(contentsOf: launchProfileConfigBackupFileURL())) != nil - removeAppLaunchProfilingConfigBackupFile() - expect(NSDictionary(contentsOf: launchProfileConfigBackupFileURL())) == nil - } - - // if there's not a file when we expect one, just make sure we don't crash - func testRemoveAppLaunchProfilingConfigBackupFile_noFileExists() throws { - try ensureAppLaunchProfileConfig(exists: false, backup: true) - expect(NSDictionary(contentsOf: launchProfileConfigBackupFileURL())) == nil - removeAppLaunchProfilingConfigBackupFile() - expect(NSDictionary(contentsOf: launchProfileConfigBackupFileURL())) == nil - } } // MARK: Private profiling tests private extension SentryFileManagerTests { - func ensureAppLaunchProfileConfig(exists: Bool = true, tracesSampleRate: Double = 1, profilesSampleRate: Double = 1, backup: Bool = false) throws { - let url: URL - if backup { - url = launchProfileConfigBackupFileURL() - } else { - url = launchProfileConfigFileURL() - } + func ensureAppLaunchProfileConfig(exists: Bool = true, tracesSampleRate: Double = 1, profilesSampleRate: Double = 1) throws { + let url = launchProfileConfigFileURL() if exists { let dict = [kSentryLaunchProfileConfigKeyTracesSampleRate: tracesSampleRate, kSentryLaunchProfileConfigKeyProfilesSampleRate: profilesSampleRate] diff --git a/Tests/SentryTests/SentryLaunchProfiling+Tests.h b/Tests/SentryTests/SentryLaunchProfiling+Tests.h index 92cbff552e..c4f1ba31d6 100644 --- a/Tests/SentryTests/SentryLaunchProfiling+Tests.h +++ b/Tests/SentryTests/SentryLaunchProfiling+Tests.h @@ -1,6 +1,8 @@ #import "SentryDefines.h" #import "SentryLaunchProfiling.h" +#if SENTRY_TARGET_PROFILING_SUPPORTED + @class SentrySamplerDecision; @class SentryOptions; @@ -17,4 +19,9 @@ SENTRY_EXTERN SentryLaunchProfileConfig shouldProfileNextLaunch(SentryOptions *o SENTRY_EXTERN NSString *const kSentryLaunchProfileConfigKeyTracesSampleRate; SENTRY_EXTERN NSString *const kSentryLaunchProfileConfigKeyProfilesSampleRate; +SENTRY_EXTERN SentryTransactionContext *context(NSNumber *tracesRate); +SENTRY_EXTERN SentryTracerConfiguration *config(NSNumber *profilesRate); + NS_ASSUME_NONNULL_END + +#endif // SENTRY_TARGET_PROFILING_SUPPORTED diff --git a/develop-docs/README.md b/develop-docs/README.md index b0548c4241..43967e649f 100644 --- a/develop-docs/README.md +++ b/develop-docs/README.md @@ -111,3 +111,19 @@ This feature is experimental and is currently not compatible with SPM. ## Logging We have a set of macros for debugging at various levels defined in SentryLog.h. These are not async-safe; to log from special places like crash handlers, see SentryCrashLogger.h; see the headerdocs in that header for how to work with those logging macros. There are also separate macros in SentryProfilingLogging.hpp specifically for the profiler; these are completely compiled out of release builds due to https://github.com/getsentry/sentry-cocoa/issues/3336. + +## Profiling + +The profiler runs on a dedicated thread, and on a predefined interval will enumerate all other threads and gather the backtrace on each non-idle thread. + +The information is stored in deduplicated frame and stack indexed lookups for memory and transmission efficiency. These are maintained in `SentryProfilerState`. + +If enabled and sampled in (controlled by `SentryOptions.profilesSampleRate` or `SentryOptions.profilesSampler`), the profiler will start along with a trace, and the profile information is sliced to the start and end of each transaction and sent with them an envelope attachments. + +The profiler will automatically time out if it is not stopped within 30 seconds, and also stops automatically if the app is sent to the background. + +There's only ever one profiler instance running at a time, but instances that have timed out will be kept in memory until all traces that ran concurrently with it have finished and serialized to envelopes. The associations between profiler instances and traces are maintained in `SentryProfiledTracerConcurrency`. + +App launches can be automatically profiled if configured with `SentryOptions.enableAppLaunchProfiling`. If enabled, when `SentrySDK.startWithOptions` is called, `SentryLaunchProfiling.configureLaunchProfiling` will get a sample rate for traces and profiles with their respective options, and store those rates in a file to be read on the next launch. On each launch, `SentryLaunchProfiling.startLaunchProfile` checks for the presence of that file is used to decide whether to start an app launch profiled trace, and afterwards retrieves those rates to initialize a `SentryTransactionContext` and `SentryTracerConfiguration`, and provides them to a new `SentryTracer` instance, which is what actually starts the profiler. There is no hub at this time; also in the call to `SentrySDK.startWithOptions`, any current profiled launch trace is attempted to be finished, and the hub that exists by that time is provided to the `SentryTracer` instance via `SentryLaunchProfiling.stopLaunchProfile` so that when it needs to transmit the transaction envelope, the infrastructure is in place to do so. + +In testing and debug environments, when a profile payload is serialized for transmission, the dictionary will also be written to a file in application support that can be retrieved by a sample app. This helps with UI tests that want to verify the contents of a profile after some app interaction. See `iOS-Swift.ProfilingViewController.viewLastProfile` and `iOS-SwiftUITests.ProfilingUITests`.