From f715546c7354443cf0511e0aecb538c30ff58c3a Mon Sep 17 00:00:00 2001 From: Andrew McKnight Date: Thu, 1 Feb 2024 16:38:11 -0900 Subject: [PATCH 1/4] test: manage files in sample app from integration test --- .../iOS-Swift.xcodeproj/project.pbxproj | 6 +++++ Samples/iOS-Swift/iOS-Swift/AppStartup.h | 17 ++++++++++++ Samples/iOS-Swift/iOS-Swift/AppStartup.m | 26 +++++++++++++++++++ .../Profiling/ProfilingViewController.swift | 1 + 4 files changed, 50 insertions(+) create mode 100644 Samples/iOS-Swift/iOS-Swift/AppStartup.h create mode 100644 Samples/iOS-Swift/iOS-Swift/AppStartup.m diff --git a/Samples/iOS-Swift/iOS-Swift.xcodeproj/project.pbxproj b/Samples/iOS-Swift/iOS-Swift.xcodeproj/project.pbxproj index 4f50f3ca2a3..5afbfb896fc 100644 --- a/Samples/iOS-Swift/iOS-Swift.xcodeproj/project.pbxproj +++ b/Samples/iOS-Swift/iOS-Swift.xcodeproj/project.pbxproj @@ -38,6 +38,7 @@ 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 */; }; @@ -314,6 +315,8 @@ 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 = ""; }; @@ -475,6 +478,8 @@ D8DBDA73274D4DF900007380 /* ViewControllers */, 63F93AA9245AC91600A500DB /* iOS-Swift.entitlements */, 637AFDA9243B02760034958B /* AppDelegate.swift */, + 84DEE88C2B6A4D1200A7BC17 /* AppStartup.h */, + 84DEE88D2B6A4D1200A7BC17 /* AppStartup.m */, 637AFDAD243B02760034958B /* TransactionsViewController.swift */, 84AB90782A50031B0054C99A /* Profiling */, D80D021229EE93630084393D /* ErrorsViewController.swift */, @@ -953,6 +958,7 @@ 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/AppStartup.h b/Samples/iOS-Swift/iOS-Swift/AppStartup.h new file mode 100644 index 00000000000..1b7bb41899d --- /dev/null +++ b/Samples/iOS-Swift/iOS-Swift/AppStartup.h @@ -0,0 +1,17 @@ +// +// 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 new file mode 100644 index 00000000000..f3d7488e68d --- /dev/null +++ b/Samples/iOS-Swift/iOS-Swift/AppStartup.m @@ -0,0 +1,26 @@ +#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 7909a0ca0d8..9d042d59378 100644 --- a/Samples/iOS-Swift/iOS-Swift/Profiling/ProfilingViewController.swift +++ b/Samples/iOS-Swift/iOS-Swift/Profiling/ProfilingViewController.swift @@ -130,6 +130,7 @@ extension ProfilingViewController { let url = file as! URL if url.absoluteString.contains(fileName) { block(url) + try! FileManager.default.removeItem(at: url) return } } From 3af711baf9e8f712379729e055227bf11af611d4 Mon Sep 17 00:00:00 2001 From: Andrew McKnight Date: Tue, 6 Feb 2024 17:03:03 -0900 Subject: [PATCH 2/4] fix test on macos (isnt applicable) --- .../SentryAppLaunchProfilingTests.m | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Tests/SentryProfilerTests/SentryAppLaunchProfilingTests.m b/Tests/SentryProfilerTests/SentryAppLaunchProfilingTests.m index 2cd47f9d7b9..f622f9a0c93 100644 --- a/Tests/SentryProfilerTests/SentryAppLaunchProfilingTests.m +++ b/Tests/SentryProfilerTests/SentryAppLaunchProfilingTests.m @@ -107,31 +107,31 @@ - (void)testSettingProfilesSampleRateTo0DisablesAppLaunchProfiling @"but profiles sample rate of 0 should not enable launch profiling"); } -- (void)testDisablingSwizzlingOptionDisablesAppLaunchProfiling +- (void)testDisablingAutoPerformanceTracingOptionDisablesAppLaunchProfiling { XCTAssertFalse( shouldProfileNextLaunch( [self defaultLaunchProfilingOptionsWithOverrides:@{ SENTRY_OPTION( - enableSwizzling, @NO) }]) + enableAutoPerformanceTracing, + @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"); + @"sample rates of 1, but automatic performance tracing disabled should not enable launch " + @"profiling"); } -- (void)testDisablingAutoPerformanceTracingOptionDisablesAppLaunchProfiling +# if SENTRY_HAS_UIKIT +- (void)testDisablingSwizzlingOptionDisablesAppLaunchProfiling { XCTAssertFalse( shouldProfileNextLaunch( [self defaultLaunchProfilingOptionsWithOverrides:@{ SENTRY_OPTION( - enableAutoPerformanceTracing, - @NO) }]) + enableSwizzling, @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"); + @"sample rates of 1, but swizzling disabled should not enable launch profiling"); } -# if SENTRY_HAS_UIKIT - (void)testDisablingUIViewControllerTracingOptionDisablesAppLaunchProfiling { XCTAssertFalse( From b799c57418174382aefa9f2e841036c06ca2f64d Mon Sep 17 00:00:00 2001 From: Andrew McKnight Date: Wed, 7 Feb 2024 11:42:54 -0900 Subject: [PATCH 3/4] add availability declaration to fix some ui tests that are failing to build due to the async setUp override --- Samples/iOS-Swift/iOS-SwiftUITests/ProfilingUITests.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Samples/iOS-Swift/iOS-SwiftUITests/ProfilingUITests.swift b/Samples/iOS-Swift/iOS-SwiftUITests/ProfilingUITests.swift index e526f56c0a7..e754ea4c36f 100644 --- a/Samples/iOS-Swift/iOS-SwiftUITests/ProfilingUITests.swift +++ b/Samples/iOS-Swift/iOS-SwiftUITests/ProfilingUITests.swift @@ -2,7 +2,8 @@ import XCTest //swiftlint:disable function_body_length todo -class ProfilingUITests: BaseUITest { +@available(iOS 16, *) +class ProfilingUITests: BaseUITest { override var automaticallyLaunchAndTerminateApp: Bool { false } // this will run before the non-async BaseUITest.setUp, so we can bail out before running any of the logic in there @@ -72,6 +73,7 @@ class ProfilingUITests: BaseUITest { } } +@available(iOS 16, *) extension ProfilingUITests { // We don't need to test these on multiple OSes right now, and older versions seem to have issues; older devices or VM images running simulators might just be slower. Latest OS is enough coverage for our needs for now. func checkOSVersionForProfilingTest() throws { From f3c04cd24014673c7e795d2dabd7be40b0859f81 Mon Sep 17 00:00:00 2001 From: Andrew McKnight Date: Thu, 8 Feb 2024 13:30:19 -0900 Subject: [PATCH 4/4] test(app launch profiling): integration test fixes and additions (#3605) --- .../Profiling/ProfilingViewController.swift | 5 +- .../iOS-SwiftUITests/ProfilingUITests.swift | 34 +- Sentry.xcodeproj/project.pbxproj | 2 - .../Sentry/Profiling/SentryLaunchProfiling.m | 11 +- Sources/Sentry/SentryFileManager.m | 81 +++- Sources/Sentry/include/SentryFileManager.h | 15 + .../Sentry/include/SentryLaunchProfiling.h | 2 +- .../Helper/SentryFileManager+Test.h | 3 + .../Helper/SentryFileManagerTests.swift | 359 ++++++++++++------ .../SentryTests/SentryLaunchProfiling+Tests.h | 5 +- .../SentryTests/SentryTests-Bridging-Header.h | 1 + 11 files changed, 377 insertions(+), 141 deletions(-) diff --git a/Samples/iOS-Swift/iOS-Swift/Profiling/ProfilingViewController.swift b/Samples/iOS-Swift/iOS-Swift/Profiling/ProfilingViewController.swift index 9d042d59378..ece70bdbbd5 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] [Profiling] benchmarking results:\n\(value)") + print("[iOS-Swift] [ProfilingViewController] benchmarking results:\n\(value)") } @IBAction func startCPUWork(_ sender: UIButton) { @@ -130,6 +130,7 @@ extension ProfilingViewController { let url = file as! URL if url.absoluteString.contains(fileName) { block(url) + print("[iOS-Swift] [ProfilingViewController] removing file at \(url)") try! FileManager.default.removeItem(at: url) return } @@ -149,7 +150,7 @@ extension ProfilingViewController { return } let contents = data.base64EncodedString() - print("[iOS-Swift] [Profiling] contents of file at \(file): \(String(data: data, encoding: .utf8))") + print("[iOS-Swift] [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 e754ea4c36f..8e0297de1db 100644 --- a/Samples/iOS-Swift/iOS-SwiftUITests/ProfilingUITests.swift +++ b/Samples/iOS-Swift/iOS-SwiftUITests/ProfilingUITests.swift @@ -13,6 +13,7 @@ class ProfilingUITests: BaseUITest { } func testProfiledAppLaunches() throws { + 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 @@ -22,12 +23,12 @@ class ProfilingUITests: BaseUITest { try relaunchAndConfigureSubsequentLaunches(shouldProfileThisLaunch: false, shouldEnableLaunchProfilingOptionForNextLaunch: true) // this launch should run the profiler, then set the option to allow launch profiling to true, but set the numerical sample rates to 0 so that the next launch should not profile - try relaunchAndConfigureSubsequentLaunches(shouldProfileThisLaunch: false, shouldEnableLaunchProfilingOptionForNextLaunch: true, profilesSampleRate: 0, tracesSampleRate: 0) + try relaunchAndConfigureSubsequentLaunches(shouldProfileThisLaunch: true, shouldEnableLaunchProfilingOptionForNextLaunch: true, profilesSampleRate: 0, tracesSampleRate: 0) // 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 run due to the ui.load transaction not being allowed to be created but configure it not to run the next launch due to disabling swizzling, which would override the option to enable launch profiling + // 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 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 @@ -132,31 +133,46 @@ extension ProfilingUITests { func assertLaunchProfile() throws { retrieveLaunchProfileData() - var lastProfile = try marshalJSONDictionaryFromApp() + let lastProfile = try marshalJSONDictionaryFromApp() let sampledProfile = try XCTUnwrap(lastProfile["profile"] as? [String: Any]) let stacks = try XCTUnwrap(sampledProfile["stacks"] as? [[Int]]) let frames = try XCTUnwrap(sampledProfile["frames"] as? [[String: Any]]) - let functions = stacks.map({ stack in + let stackFunctions = stacks.map({ stack in stack.map { stackFrame in frames[stackFrame]["function"] } }) - let stack = try XCTUnwrap(functions.first { nextStack in - let result = nextStack.contains { frame in + + // grab the first stack that contained frames from the fixture code that simulates a slow +[load] method + var stackID: Int? + let stack = try XCTUnwrap(stackFunctions.enumerated().first { nextStack in + let result = nextStack.element.contains { frame in let result = (frame as! String).contains("+[SentryProfiler(SlowLoad) load]") + if result { + stackID = nextStack.offset + } return result } return result - }).map { any in + }).element.map { any in try XCTUnwrap(any as? String) } + guard stackID != nil else { + XCTFail("Didn't find the ID of the stack containing the target function") + return + } + // ensure that the stack doesn't contain any calls to main functions; this ensures we actually captured pre-main stacks XCTAssertFalse(stack.contains("main")) XCTAssertFalse(stack.contains("UIApplicationMain")) XCTAssertFalse(stack.contains("-[UIApplication _run]")) - // ???: can we assert that this stack was on the main thread? - // TODO: yes! need to correlate the samples.[].stack_id with samples.[].thread_id + // ensure that the stack happened on the main thread; this is a cross-check to make sure we didn't accidentally grab a stack from a different thread that wouldn't have had a call to main() anyways, thereby possibly missing the real stack that may have contained main() calls (but shouldn't for this test) + let samples = try XCTUnwrap(sampledProfile["samples"] as? [[String: Any]]) + let sample = try XCTUnwrap(samples.first { nextSample in + try XCTUnwrap(nextSample["stack_id"] as? NSNumber).intValue == stackID + }) + XCTAssert(try XCTUnwrap(sample["thread_id"] as? String) == "259") // the main thread is always ID 259 } /** diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index 64db5e26c65..0365bc7dbd8 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -626,7 +626,6 @@ 8431EFE229B27BAD00D8DC56 /* SentryNSTimerFactoryTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849472842971C41A002603DE /* SentryNSTimerFactoryTest.swift */; }; 8431EFE529B27BAD00D8DC56 /* SentryNSProcessInfoWrapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849472822971C2CD002603DE /* SentryNSProcessInfoWrapperTests.swift */; }; 8431EFE829B27BAD00D8DC56 /* SentrySystemWrapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849472802971C107002603DE /* SentrySystemWrapperTests.swift */; }; - 8431F00529B2849A00D8DC56 /* (null) in Sources */ = {isa = PBXBuildFile; }; 8431F01529B2851500D8DC56 /* TestSentryNSTimerFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 844EDCE72947DCD700C86F34 /* TestSentryNSTimerFactory.swift */; }; 8431F01629B2851500D8DC56 /* TestSentryNSProcessInfoWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 844EDC712941442200C86F34 /* TestSentryNSProcessInfoWrapper.swift */; }; 8431F01729B2851500D8DC56 /* TestSentrySystemWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 844EDC7829415AB300C86F34 /* TestSentrySystemWrapper.swift */; }; @@ -4519,7 +4518,6 @@ 0A2D7BBA29152CBF008727AF /* SentryWatchdogTerminationsScopeObserverTests.swift in Sources */, 7BD4BD4B27EB2DC20071F4FF /* SentryDiscardedEventTests.swift in Sources */, 63FE721A20DA66EC00CDBAE8 /* SentryCrashSysCtl_Tests.m in Sources */, - 8431F00529B2849A00D8DC56 /* (null) in Sources */, 7B88F30424BC8E6500ADF90A /* SentrySerializationTests.swift in Sources */, 7B34721728086A9D0041F047 /* SentrySwizzleWrapperTests.swift in Sources */, 8EC4CF5025C3A0070093DEE9 /* SentrySpanContextTests.swift in Sources */, diff --git a/Sources/Sentry/Profiling/SentryLaunchProfiling.m b/Sources/Sentry/Profiling/SentryLaunchProfiling.m index 23aa75a99df..6fd50c1bb72 100644 --- a/Sources/Sentry/Profiling/SentryLaunchProfiling.m +++ b/Sources/Sentry/Profiling/SentryLaunchProfiling.m @@ -22,8 +22,8 @@ SentryId *_Nullable appLaunchTraceId; NSObject *appLaunchTraceLock; uint64_t appLaunchSystemTime; -static NSString *const kSentryLaunchProfileConfigKeyTracesSampleRate = @"traces"; -static NSString *const kSentryLaunchProfileConfigKeyProfilesSampleRate = @"profiles"; +NSString *const kSentryLaunchProfileConfigKeyTracesSampleRate = @"traces"; +NSString *const kSentryLaunchProfileConfigKeyProfilesSampleRate = @"profiles"; # pragma mark - Private @@ -101,7 +101,11 @@ = config.profilesDecision.sampleRate; writeAppLaunchProfilingConfigFile(configDict); } else { - removeAppLaunchProfilingConfigFile(); + if (isTracingAppLaunch) { + backupAppLaunchProfilingConfigFile(); + } else { + removeAppLaunchProfilingConfigFile(); + } } }]; } @@ -142,6 +146,7 @@ SentryTransactionContext *transactionContext, SentryTracerConfiguration *configuration) { NSDictionary *rates = appLaunchProfileConfiguration(); + removeAppLaunchProfilingConfigBackupFile(); NSNumber *profilesRate = rates[kSentryLaunchProfileConfigKeyProfilesSampleRate]; NSNumber *tracesRate = rates[kSentryLaunchProfileConfigKeyTracesSampleRate]; if (profilesRate == nil || tracesRate == nil) { diff --git a/Sources/Sentry/SentryFileManager.m b/Sources/Sentry/SentryFileManager.m index f9909dea338..ba382eb2cbf 100644 --- a/Sources/Sentry/SentryFileManager.m +++ b/Sources/Sentry/SentryFileManager.m @@ -36,6 +36,28 @@ return YES; } +/** + * @warning This is called from a `@synchronized` context in instance methods, but doesn't require + * that when calling from other static functions. Make sure you pay attention to where this is used + * from. + */ +void +_non_thread_safe_removeFileAtPath(NSString *path) +{ + NSError *error = nil; + NSFileManager *fileManager = [NSFileManager defaultManager]; + if (![fileManager removeItemAtPath:path error:&error]) { + if (error.code == NSFileNoSuchFileError) { + SENTRY_LOG_DEBUG(@"No file to delete at %@", path); + } else { + SENTRY_LOG_ERROR( + @"Error occurred while deleting file at %@ because of %@", path, error); + } + } else { + SENTRY_LOG_DEBUG(@"Successfully deleted file at %@", path); + } +} + @interface SentryFileManager () @@ -284,19 +306,8 @@ - (void)deleteAllEnvelopes - (void)removeFileAtPath:(NSString *)path { - NSFileManager *fileManager = [NSFileManager defaultManager]; - NSError *error = nil; @synchronized(self) { - if (![fileManager removeItemAtPath:path error:&error]) { - if (error.code == NSFileNoSuchFileError) { - SENTRY_LOG_DEBUG(@"No file to delete at %@", path); - } else { - SENTRY_LOG_ERROR( - @"Error occurred while deleting file at %@ because of %@", path, error); - } - } else { - SENTRY_LOG_DEBUG(@"Successfully deleted file at %@", path); - } + _non_thread_safe_removeFileAtPath(path); } } @@ -751,11 +762,30 @@ - (void)createPathsWithOptions:(SentryOptions *)options return sentryLaunchConfigFileURL; } +NSURL * +launchProfileConfigBackupFileURL(void) +{ + static NSURL *sentryLaunchConfigFileURL; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + sentryLaunchConfigFileURL = + [[NSURL fileURLWithPath:[sentryApplicationSupportPath() + stringByAppendingPathComponent:@"profileLaunch"]] + URLByAppendingPathExtension:@"bak"]; + }); + return sentryLaunchConfigFileURL; +} + NSDictionary *_Nullable appLaunchProfileConfiguration(void) { + NSURL *url = launchProfileConfigBackupFileURL(); + if (![[NSFileManager defaultManager] fileExistsAtPath:url.path]) { + return nil; + } + NSError *error; NSDictionary *config = [NSDictionary - dictionaryWithContentsOfURL:launchProfileConfigFileURL() + dictionaryWithContentsOfURL:launchProfileConfigBackupFileURL() error:&error]; SENTRY_CASSERT( error == nil, @"Encountered error trying to retrieve app launch profile config: %@", error); @@ -778,16 +808,33 @@ - (void)createPathsWithOptions:(SentryOptions *)options void removeAppLaunchProfilingConfigFile(void) +{ + _non_thread_safe_removeFileAtPath(launchProfileConfigFileURL().path); +} + +void +removeAppLaunchProfilingConfigBackupFile(void) +{ + _non_thread_safe_removeFileAtPath(launchProfileConfigBackupFileURL().path); +} + +void +backupAppLaunchProfilingConfigFile(void) { NSFileManager *fm = [NSFileManager defaultManager]; - NSString *path = launchProfileConfigFileURL().path; - if (![fm fileExistsAtPath:path]) { + 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 removeItemAtPath:path error:&error], - @"Failed to remove launch profile marker file: %@", 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 diff --git a/Sources/Sentry/include/SentryFileManager.h b/Sources/Sentry/include/SentryFileManager.h index 76946982789..8d3322bf01f 100644 --- a/Sources/Sentry/include/SentryFileManager.h +++ b/Sources/Sentry/include/SentryFileManager.h @@ -123,6 +123,21 @@ SENTRY_EXTERN void writeAppLaunchProfilingConfigFile( * start the profiler. */ 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 1146a3482d7..a67ea4d4974 100644 --- a/Sources/Sentry/include/SentryLaunchProfiling.h +++ b/Sources/Sentry/include/SentryLaunchProfiling.h @@ -18,7 +18,7 @@ SENTRY_EXTERN NSObject *appLaunchTraceLock; void startLaunchProfile(void); -/* +/** * Write a file to disk containing sample rates for profiles and traces. The presence of this file * will let the profiler know to start on the app launch, and the sample rates contained will help * thread sampling decisions through to SentryHub later when it needs to start a transaction for the diff --git a/Tests/SentryTests/Helper/SentryFileManager+Test.h b/Tests/SentryTests/Helper/SentryFileManager+Test.h index e85d818f134..1285179f521 100644 --- a/Tests/SentryTests/Helper/SentryFileManager+Test.h +++ b/Tests/SentryTests/Helper/SentryFileManager+Test.h @@ -2,6 +2,9 @@ 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 39fb13664a3..4ead32d4e53 100644 --- a/Tests/SentryTests/Helper/SentryFileManagerTests.swift +++ b/Tests/SentryTests/Helper/SentryFileManagerTests.swift @@ -14,13 +14,13 @@ class SentryFileManagerTests: XCTestCase { let dispatchQueueWrapper: TestSentryDispatchQueueWrapper! let options: Options - + let session = SentrySession(releaseName: "1.0.0", distinctId: "some-id") let sessionEnvelope: SentryEnvelope - + let sessionUpdate: SentrySession let sessionUpdateEnvelope: SentryEnvelope - + let expectedSessionUpdate: SentrySession // swiftlint:disable weak_delegate @@ -32,23 +32,23 @@ class SentryFileManagerTests: XCTestCase { init() { currentDateProvider = TestCurrentDateProvider() dispatchQueueWrapper = TestSentryDispatchQueueWrapper() - + eventIds = (0...(maxCacheItems + 10)).map { _ in SentryId() } options = Options() options.dsn = TestConstants.dsnAsString(username: "SentryFileManagerTests") sessionEnvelope = SentryEnvelope(session: session) - + let sessionCopy = session.copy() as! SentrySession sessionCopy.incrementErrors() // We need to serialize in order to set the timestamp and the duration sessionUpdate = SentrySession(jsonObject: sessionCopy.serialize())! - + let event = Event() let items = [SentryEnvelopeItem(session: sessionUpdate), SentryEnvelopeItem(event: event)] sessionUpdateEnvelope = SentryEnvelope(id: event.eventId, items: items) - + let sessionUpdateCopy = sessionUpdate.copy() as! SentrySession // We need to serialize in order to set the timestamp and the duration expectedSessionUpdate = SentrySession(jsonObject: sessionUpdateCopy.serialize())! @@ -70,19 +70,19 @@ class SentryFileManagerTests: XCTestCase { sut.setDelegate(delegate) return sut } - + } - + private var fixture: Fixture! private var sut: SentryFileManager! - + override func setUp() { super.setUp() fixture = Fixture() SentryDependencyContainer.sharedInstance().dateProvider = fixture.currentDateProvider - + sut = fixture.getSut() - + sut.deleteAllEnvelopes() sut.deleteTimestampLastInForeground() } @@ -102,10 +102,10 @@ class SentryFileManagerTests: XCTestCase { sut.store(TestConstants.envelope) sut.storeCurrentSession(SentrySession(releaseName: "1.0.0", distinctId: "some-id")) sut.storeTimestampLast(inForeground: Date()) - + _ = try! SentryFileManager(options: fixture.options, dispatchQueueWrapper: TestSentryDispatchQueueWrapper()) let fileManager = try! SentryFileManager(options: fixture.options, dispatchQueueWrapper: TestSentryDispatchQueueWrapper()) - + XCTAssertEqual(1, fileManager.getAllEnvelopes().count) XCTAssertNotNil(fileManager.readCurrentSession()) XCTAssertNotNil(fileManager.readTimestampLastInForeground()) @@ -122,7 +122,7 @@ class SentryFileManagerTests: XCTestCase { func testInitDoesntCreateEventsFolder() { assertEventFolderDoesntExist() } - + func testStoreEnvelope() throws { let envelope = TestConstants.envelope sut.store(envelope) @@ -135,13 +135,13 @@ class SentryFileManagerTests: XCTestCase { let actualData = envelopes[0].contents XCTAssertEqual(expectedData, actualData as Data) } - + func testDeleteOldEnvelopes() throws { try givenOldEnvelopes() - + sut = fixture.getSut() sut.deleteOldEnvelopeItems() - + XCTAssertEqual(sut.getAllEnvelopes().count, 0) } @@ -214,39 +214,39 @@ class SentryFileManagerTests: XCTestCase { sut.deleteOldEnvelopeItems() try givenOldEnvelopes() - + sut.deleteOldEnvelopeItems() - + XCTAssertEqual(sut.getAllEnvelopes().count, 0) } - + func testDontDeleteYoungEnvelopes() throws { let envelope = TestConstants.envelope let path = sut.store(envelope) - + let timeIntervalSince1970 = fixture.currentDateProvider.date().timeIntervalSince1970 - (90 * 24 * 60 * 60) let date = Date(timeIntervalSince1970: timeIntervalSince1970) try FileManager.default.setAttributes([FileAttributeKey.creationDate: date], ofItemAtPath: path) - + XCTAssertEqual(sut.getAllEnvelopes().count, 1) - + sut = fixture.getSut() - + XCTAssertEqual(sut.getAllEnvelopes().count, 1) } - + func testDontDeleteYoungEnvelopesFromOldEnvelopesFolder() throws { let envelope = TestConstants.envelope sut.store(envelope) - + let timeIntervalSince1970 = fixture.currentDateProvider.date().timeIntervalSince1970 - (90 * 24 * 60 * 60) let date = Date(timeIntervalSince1970: timeIntervalSince1970) try FileManager.default.setAttributes([FileAttributeKey.creationDate: date], ofItemAtPath: sut.envelopesPath) - + XCTAssertEqual(sut.getAllEnvelopes().count, 1) - + sut = fixture.getSut() - + XCTAssertEqual(sut.getAllEnvelopes().count, 1) } @@ -254,7 +254,7 @@ class SentryFileManagerTests: XCTestCase { try givenOldEnvelopes() fixture.dispatchQueueWrapper.dispatchAsyncExecutesBlock = false - + // Initialize sut in extra function so ARC deallocates it func getSut() { _ = fixture.getSut() @@ -265,7 +265,7 @@ class SentryFileManagerTests: XCTestCase { XCTAssertEqual(sut.getAllEnvelopes().count, 1) } - + func testCreateDirDoesNotThrow() throws { try SentryFileManager.createDirectory(atPath: "a") } @@ -276,12 +276,12 @@ class SentryFileManagerTests: XCTestCase { sut.removeFile(atPath: "x") XCTAssertFalse(logOutput.loggedMessages.contains(where: { $0.contains("[error]") })) } - + func testDefaultMaxEnvelopes() { for _ in 0...(fixture.maxCacheItems + 1) { sut.store(TestConstants.envelope) } - + let events = sut.getAllEnvelopes() XCTAssertEqual(fixture.maxCacheItems, events.count) } @@ -302,7 +302,7 @@ class SentryFileManagerTests: XCTestCase { let expected: [SentryDataCategory] = [.error, .attachment, .session, .error] XCTAssertEqual(expected, fixture.delegate.envelopeItemsDeleted.invocations) } - + func testDefaultMaxEnvelopesConcurrent() { let maxCacheItems = 1 let sut = fixture.getSut(maxCacheItems: UInt(maxCacheItems)) @@ -323,7 +323,7 @@ class SentryFileManagerTests: XCTestCase { queue.activate() wait(for: [envelopeStoredExpectation], timeout: 10) - + let events = sut.getAllEnvelopes() XCTAssertEqual(maxCacheItems, events.count) } @@ -337,7 +337,7 @@ class SentryFileManagerTests: XCTestCase { let events = sut.getAllEnvelopes() XCTAssertEqual(maxCacheItems, UInt(events.count)) } - + func testMigrateSessionInit_SessionUpdateIsLast() { sut.store(fixture.sessionEnvelope) // just some other session @@ -346,22 +346,22 @@ class SentryFileManagerTests: XCTestCase { sut.store(TestConstants.envelope) } sut.store(fixture.sessionUpdateEnvelope) - + assertSessionInitMoved(sut.getAllEnvelopes().last!) assertSessionEnvelopesStored(count: 2) } - + func testMigrateSessionInit_SessionUpdateIsSecond() { sut.store(fixture.sessionEnvelope) sut.store(fixture.sessionUpdateEnvelope) for _ in 0...(fixture.maxCacheItems - 2) { sut.store(TestConstants.envelope) } - + assertSessionInitMoved(sut.getAllEnvelopes().first!) assertSessionEnvelopesStored(count: 1) } - + func testMigrateSessionInit_IsInMiddle() { sut.store(fixture.sessionEnvelope) for _ in 0...10 { @@ -371,7 +371,7 @@ class SentryFileManagerTests: XCTestCase { for _ in 0...18 { sut.store(TestConstants.envelope) } - + assertSessionInitMoved(sut.getAllEnvelopes()[10]) assertSessionEnvelopesStored(count: 1) } @@ -387,20 +387,20 @@ class SentryFileManagerTests: XCTestCase { for _ in 0...16 { sut.store(TestConstants.envelope) } - + assertSessionInitMoved(sut.getAllEnvelopes()[10]) assertSessionInitNotMoved(sut.getAllEnvelopes()[11]) assertSessionInitNotMoved(sut.getAllEnvelopes()[12]) assertSessionEnvelopesStored(count: 3) } - + func testMigrateSessionInit_NoOtherSessionUpdate() { sut.store(fixture.sessionEnvelope) sut.store(fixture.sessionUpdateEnvelope) for _ in 0...(fixture.maxCacheItems - 1) { sut.store(TestConstants.envelope) } - + assertSessionEnvelopesStored(count: 0) } @@ -418,7 +418,7 @@ class SentryFileManagerTests: XCTestCase { try! fileManager.createDirectory(atPath: envelopePath, withIntermediateDirectories: false, attributes: nil) sut.store(fixture.sessionUpdateEnvelope) - + assertSessionInitMoved(sut.getAllEnvelopes().last!) } @@ -428,18 +428,18 @@ class SentryFileManagerTests: XCTestCase { for _ in 0...(fixture.maxCacheItems - 2) { sut.store(TestConstants.envelope) } - + XCTAssertEqual(0, fixture.delegate.envelopeItemsDeleted.count) } - + func testGetAllEnvelopesAreSortedByDateAscending() { givenMaximumEnvelopes() - + let envelopes = sut.getAllEnvelopes() - + // Envelopes are sorted ascending by date and only the latest amount of maxCacheItems are kept let expectedEventIds = Array(fixture.eventIds[11.. [String: Any] { return ["app": self] } diff --git a/Tests/SentryTests/SentryLaunchProfiling+Tests.h b/Tests/SentryTests/SentryLaunchProfiling+Tests.h index 91997dc7bac..92cbff552e7 100644 --- a/Tests/SentryTests/SentryLaunchProfiling+Tests.h +++ b/Tests/SentryTests/SentryLaunchProfiling+Tests.h @@ -1,4 +1,4 @@ -#import "SentryInternalDefines.h" +#import "SentryDefines.h" #import "SentryLaunchProfiling.h" @class SentrySamplerDecision; @@ -14,4 +14,7 @@ typedef struct { SENTRY_EXTERN SentryLaunchProfileConfig shouldProfileNextLaunch(SentryOptions *options); +SENTRY_EXTERN NSString *const kSentryLaunchProfileConfigKeyTracesSampleRate; +SENTRY_EXTERN NSString *const kSentryLaunchProfileConfigKeyProfilesSampleRate; + NS_ASSUME_NONNULL_END diff --git a/Tests/SentryTests/SentryTests-Bridging-Header.h b/Tests/SentryTests/SentryTests-Bridging-Header.h index c2308050e3b..26bdb95b5cd 100644 --- a/Tests/SentryTests/SentryTests-Bridging-Header.h +++ b/Tests/SentryTests/SentryTests-Bridging-Header.h @@ -125,6 +125,7 @@ #import "SentryInstallation+Test.h" #import "SentryInstallation.h" #import "SentryInternalNotificationNames.h" +#import "SentryLaunchProfiling+Tests.h" #import "SentryLevelMapper.h" #import "SentryLog+TestInit.h" #import "SentryLog.h"