diff --git a/Samples/iOS-Swift/iOS-Swift.xcodeproj/project.pbxproj b/Samples/iOS-Swift/iOS-Swift.xcodeproj/project.pbxproj index 3ed5c9a5fb4..b722db68955 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 72e340ada09..dc3a836c25c 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 1b7bb41899d..00000000000 --- 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 f3d7488e68d..00000000000 --- 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 ece70bdbbd5..5634db06804 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/Sources/Sentry/Profiling/SentryLaunchProfiling.m b/Sources/Sentry/Profiling/SentryLaunchProfiling.m index ae805b120ea..b89fc60bd42 100644 --- a/Sources/Sentry/Profiling/SentryLaunchProfiling.m +++ b/Sources/Sentry/Profiling/SentryLaunchProfiling.m @@ -15,17 +15,17 @@ # import "SentrySamplerDecision.h" # import "SentrySampling.h" # import "SentrySamplingContext.h" -# import "SentryTracer.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"; -SentryTracer *_Nullable launchTracer; +static SentryTracer *_Nullable launchTracer; # pragma mark - Private @@ -91,6 +91,29 @@ }]; } +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) { @@ -111,39 +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); - - SentryTransactionContext *context = - [[SentryTransactionContext alloc] initWithName:@"launch" operation:@"app.lifecycle"]; - SentryTracerConfiguration *config = [SentryTracerConfiguration defaultConfiguration]; NSDictionary *rates = appLaunchProfileConfiguration(); NSNumber *profilesRate = rates[kSentryLaunchProfileConfigKeyProfilesSampleRate]; NSNumber *tracesRate = rates[kSentryLaunchProfileConfigKeyTracesSampleRate]; - if (profilesRate != nil && tracesRate != nil) { - config.profilesSamplerDecision = - [[SentrySamplerDecision alloc] initWithDecision:kSentrySampleDecisionYes - forSampleRate:profilesRate]; - context.sampleRate = tracesRate; + if (profilesRate == nil || tracesRate == nil) { + SENTRY_LOG_DEBUG( + @"Received a nil configured launch sample rate, will not trace or profile."); + return; } - launchTracer = [[SentryTracer alloc] initWithTransactionContext:context + + SENTRY_LOG_INFO(@"Starting app launch profile."); + launchTracer = [[SentryTracer alloc] initWithTransactionContext:context(tracesRate) hub:nil - configuration:config]; + configuration:config(profilesRate)]; }); } void -stopLaunchProfile(void) +stopLaunchProfile(SentryHub *hub) { if (launchTracer == nil) { SENTRY_LOG_DEBUG(@"No launch tracer present to stop."); + return; } 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/SentryProfiler.mm b/Sources/Sentry/SentryProfiler.mm index 2eb5edcc365..b98753280ec 100644 --- a/Sources/Sentry/SentryProfiler.mm +++ b/Sources/Sentry/SentryProfiler.mm @@ -52,6 +52,7 @@ # if defined(TEST) || defined(TESTCI) || defined(DEBUG) # import "SentryFileManager.h" +# import "SentryInternalDefines.h" # import "SentryLaunchProfiling.h" /** @@ -436,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) { @@ -460,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 @@ -487,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 34e083a9822..4493999fc08 100644 --- a/Sources/Sentry/SentrySDK.m +++ b/Sources/Sentry/SentrySDK.m @@ -164,10 +164,6 @@ + (void)setStartTimestamp:(NSDate *)value + (void)startWithOptions:(SentryOptions *)options { -#if SENTRY_TARGET_PROFILING_SUPPORTED - stopLaunchProfile(); -#endif // SENTRY_TARGET_PROFILING_SUPPORTED - [SentryLog configure:options.debug diagnosticLevel:options.diagnosticLevel]; // We accept the tradeoff that the SDK might not be fully initialized directly after @@ -190,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."); @@ -204,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 a0188c06a82..883efc34905 100644 --- a/Sources/Sentry/SentryTracer.m +++ b/Sources/Sentry/SentryTracer.m @@ -609,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 8d3322bf01f..cb774591c79 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 cebed488b98..d7d98634395 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,15 +13,12 @@ NS_ASSUME_NONNULL_BEGIN SENTRY_EXTERN BOOL isTracingAppLaunch; -SENTRY_EXTERN SentryId *_Nullable appLaunchTraceId; -SENTRY_EXTERN uint64_t appLaunchSystemTime; -SENTRY_EXTERN NSObject *appLaunchTraceLock; /** Try to start a profiled trace for this app launch, if the configuration allows. */ -void startLaunchProfile(void); +SENTRY_EXTERN void startLaunchProfile(void); /** Stop any profiled trace that may be in flight from the start of the app launch. */ -void stopLaunchProfile(void); +void stopLaunchProfile(SentryHub *hub); /** * Write a file to disk containing sample rates for profiles and traces. The presence of this file diff --git a/Sources/Sentry/include/SentryTraceOrigins.h b/Sources/Sentry/include/SentryTraceOrigins.h index 9be87b76a76..7922cdb9a7c 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 6cd2b025636..45270a23cb9 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,6 +117,8 @@ - (void)testSettingProfilesSampleRateTo0DisablesAppLaunchProfiling @"but profiles sample rate of 0 should not enable launch profiling"); } +# pragma mark - Private + - (SentryOptions *)defaultLaunchProfilingOptionsWithOverrides: (NSDictionary *)overrides { diff --git a/Tests/SentryTests/SentryLaunchProfiling+Tests.h b/Tests/SentryTests/SentryLaunchProfiling+Tests.h index 92cbff552e7..c4f1ba31d6c 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 b0548c42412..43967e649fd 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`.