diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9f48937c3ce..a58a106e85c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -248,13 +248,13 @@ jobs: # We don't run all unit tests with Thread Sanitizer enabled because # that adds a significant overhead. thread-sanitizer: - # Disabled for now, see https://github.com/getsentry/sentry-cocoa/issues/3200 - if: false name: Unit iOS - Thread Sanitizer runs-on: macos-13 - # When there are threading issues the tests sometimes keep hanging timeout-minutes: 20 needs: [build-test-server] + + # There are several ways this test can flake. Sometimes threaded tests will just hang and the job will time out, other times waiting on expectations will take much longer than in a non-TSAN run and the test case will fail. We're making this nonfailable and will grep the logs to extract any actual thread sanitizer warnings to push to the PR, and ignore everything else. + continue-on-error: true steps: - uses: actions/checkout@v4 diff --git a/Samples/iOS-Swift/iOS-SwiftUITests/ProfilingUITests.swift b/Samples/iOS-Swift/iOS-SwiftUITests/ProfilingUITests.swift index f18edf5a6ca..885b289f760 100644 --- a/Samples/iOS-Swift/iOS-SwiftUITests/ProfilingUITests.swift +++ b/Samples/iOS-Swift/iOS-SwiftUITests/ProfilingUITests.swift @@ -24,7 +24,7 @@ final class ProfilingUITests: XCTestCase { XCUIApplication().tabBars["Tab Bar"].buttons["Transactions"].tap() app.buttons["Start transaction (main thread)"].afterWaitingForExistence("Couldn't find button to start transaction").tap() XCUIApplication().tabBars["Tab Bar"].buttons["Extra"].tap() - app.buttons["ANR filling run loop"].afterWaitingForExistence("Couldn't find button to ANR").tap() + app.buttons["Cause frozen frames"].afterWaitingForExistence("Couldn't find button to cause frozen frames").tap() XCUIApplication().tabBars["Tab Bar"].buttons["Transactions"].tap() app.buttons["Stop transaction"].afterWaitingForExistence("Couldn't find button to end transaction").tap() diff --git a/Sources/Sentry/SentryANRTracker.m b/Sources/Sentry/SentryANRTracker.m index 29dba69d03e..01bc4a15bfe 100644 --- a/Sources/Sentry/SentryANRTracker.m +++ b/Sources/Sentry/SentryANRTracker.m @@ -71,6 +71,8 @@ - (void)detectANRs NSInteger reportThreshold = 5; NSTimeInterval sleepInterval = self.timeoutInterval / reportThreshold; + SentryCurrentDateProvider *dateProvider = SentryDependencyContainer.sharedInstance.dateProvider; + // Canceling the thread can take up to sleepInterval. while (YES) { @synchronized(threadLock) { @@ -79,8 +81,7 @@ - (void)detectANRs } } - NSDate *blockDeadline = [[SentryDependencyContainer.sharedInstance.dateProvider date] - dateByAddingTimeInterval:self.timeoutInterval]; + NSDate *blockDeadline = [[dateProvider date] dateByAddingTimeInterval:self.timeoutInterval]; atomic_fetch_add_explicit(&ticksSinceUiUpdate, 1, memory_order_relaxed); @@ -107,8 +108,7 @@ - (void)detectANRs // an ANR. If the app gets suspended this thread could sleep and wake up again. To avoid // false positives, we don't report ANRs if the delta is too big. NSTimeInterval deltaFromNowToBlockDeadline = - [[SentryDependencyContainer.sharedInstance.dateProvider date] - timeIntervalSinceDate:blockDeadline]; + [[dateProvider date] timeIntervalSinceDate:blockDeadline]; if (deltaFromNowToBlockDeadline >= self.timeoutInterval) { SENTRY_LOG_DEBUG( @@ -165,8 +165,8 @@ - (void)addListener:(id)listener @synchronized(self.listeners) { [self.listeners addObject:listener]; - if (self.listeners.count > 0 && state == kSentryANRTrackerNotRunning) { - @synchronized(threadLock) { + @synchronized(threadLock) { + if (self.listeners.count > 0 && state == kSentryANRTrackerNotRunning) { if (state == kSentryANRTrackerNotRunning) { state = kSentryANRTrackerStarting; [NSThread detachNewThreadSelector:@selector(detectANRs) diff --git a/Sources/Sentry/SentryFileManager.m b/Sources/Sentry/SentryFileManager.m index ea4a2c2b391..716ee02d35d 100644 --- a/Sources/Sentry/SentryFileManager.m +++ b/Sources/Sentry/SentryFileManager.m @@ -253,10 +253,10 @@ - (void)removeFileAtPath:(NSString *)path - (NSString *)storeEnvelope:(SentryEnvelope *)envelope { NSData *envelopeData = [SentrySerialization dataWithEnvelope:envelope error:nil]; - NSString *path = - [self.envelopesPath stringByAppendingPathComponent:[self uniqueAscendingJsonName]]; @synchronized(self) { + NSString *path = + [self.envelopesPath stringByAppendingPathComponent:[self uniqueAscendingJsonName]]; SENTRY_LOG_DEBUG(@"Writing envelope to path: %@", path); if (![self writeData:envelopeData toPath:path]) { diff --git a/Sources/Sentry/SentryFramesTracker.m b/Sources/Sentry/SentryFramesTracker.m index ae2c4480974..69fb2f81398 100644 --- a/Sources/Sentry/SentryFramesTracker.m +++ b/Sources/Sentry/SentryFramesTracker.m @@ -58,6 +58,7 @@ - (instancetype)initWithDisplayLinkWrapper:(SentryDisplayLinkWrapper *)displayLi _displayLinkWrapper = displayLinkWrapper; _listeners = [NSHashTable weakObjectsHashTable]; [self resetFrames]; + SENTRY_LOG_DEBUG(@"Initialized frame tracker %@", self); } return self; } @@ -146,7 +147,8 @@ - (void)displayLinkCallback && frameDuration <= SentryFrozenFrameThreshold) { _slowFrames++; # if SENTRY_TARGET_PROFILING_SUPPORTED - SENTRY_LOG_DEBUG(@"Capturing slow frame starting at %llu.", thisFrameSystemTimestamp); + SENTRY_LOG_DEBUG(@"Capturing slow frame starting at %llu (frame tracker: %@).", + thisFrameSystemTimestamp, self); [self recordTimestamp:thisFrameSystemTimestamp value:@(thisFrameSystemTimestamp - self.previousFrameSystemTimestamp) array:self.slowFrameTimestamps]; diff --git a/Sources/Sentry/SentryLog.m b/Sources/Sentry/SentryLog.m index 9cf3541a6d7..7b58124463f 100644 --- a/Sources/Sentry/SentryLog.m +++ b/Sources/Sentry/SentryLog.m @@ -12,11 +12,19 @@ @implementation SentryLog static BOOL isDebug = YES; static SentryLevel diagnosticLevel = kSentryLevelError; static SentryLogOutput *logOutput; +static NSObject *logConfigureLock; + ++ (void)initialize +{ + logConfigureLock = [[NSObject alloc] init]; +} + (void)configure:(BOOL)debug diagnosticLevel:(SentryLevel)level { - isDebug = debug; - diagnosticLevel = level; + @synchronized(logConfigureLock) { + isDebug = debug; + diagnosticLevel = level; + } } + (void)logWithMessage:(NSString *)message andLevel:(SentryLevel)level @@ -33,7 +41,9 @@ + (void)logWithMessage:(NSString *)message andLevel:(SentryLevel)level + (BOOL)willLogAtLevel:(SentryLevel)level { - return isDebug && level != kSentryLevelNone && level >= diagnosticLevel; + @synchronized(logConfigureLock) { + return isDebug && level != kSentryLevelNone && level >= diagnosticLevel; + } } // Internal and only needed for testing. diff --git a/Sources/Sentry/SentryNetworkTracker.m b/Sources/Sentry/SentryNetworkTracker.m index 361d6959be3..be23f18b8bb 100644 --- a/Sources/Sentry/SentryNetworkTracker.m +++ b/Sources/Sentry/SentryNetworkTracker.m @@ -334,10 +334,12 @@ - (void)urlSessionTask:(NSURLSessionTask *)sessionTask setState:(NSURLSessionTas - (void)captureFailedRequests:(NSURLSessionTask *)sessionTask { - if (!self.isCaptureFailedRequestsEnabled) { - SENTRY_LOG_DEBUG( - @"captureFailedRequestsEnabled is disabled, not capturing HTTP Client errors."); - return; + @synchronized(self) { + if (!self.isCaptureFailedRequestsEnabled) { + SENTRY_LOG_DEBUG( + @"captureFailedRequestsEnabled is disabled, not capturing HTTP Client errors."); + return; + } } // if request or response are null, we can't raise the event diff --git a/Sources/Sentry/SentryProfiler.mm b/Sources/Sentry/SentryProfiler.mm index 8a65aaa8600..8d8862c4a5e 100644 --- a/Sources/Sentry/SentryProfiler.mm +++ b/Sources/Sentry/SentryProfiler.mm @@ -62,6 +62,20 @@ std::mutex _gProfilerLock; SentryProfiler *_Nullable _gCurrentProfiler; +BOOL +threadSanitizerIsPresent(void) +{ +# if defined(__has_feature) +# if __has_feature(thread_sanitizer) + return YES; +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wunreachable-code" +# endif // __has_feature(thread_sanitizer) +# endif // defined(__has_feature) + + return NO; +} + NSString * profilerTruncationReasonName(SentryProfilerTruncationReason reason) { @@ -498,17 +512,10 @@ - (void)startMetricProfiler - (void)start { -// Disable profiling when running with TSAN because it produces a TSAN false -// positive, similar to the situation described here: -// https://github.com/envoyproxy/envoy/issues/2561 -# if defined(__has_feature) -# if __has_feature(thread_sanitizer) - SENTRY_LOG_DEBUG(@"Disabling profiling when running with TSAN"); - return; -# pragma clang diagnostic push -# pragma clang diagnostic ignored "-Wunreachable-code" -# endif // __has_feature(thread_sanitizer) -# endif // defined(__has_feature) + if (threadSanitizerIsPresent()) { + SENTRY_LOG_DEBUG(@"Disabling profiling when running with TSAN"); + return; + } if (_profiler != nullptr) { // This theoretically shouldn't be possible as long as we're checking for nil and running diff --git a/Sources/Sentry/SentrySDK.m b/Sources/Sentry/SentrySDK.m index 57fb54c931a..20b57746422 100644 --- a/Sources/Sentry/SentrySDK.m +++ b/Sources/Sentry/SentrySDK.m @@ -137,24 +137,29 @@ + (void)setStartInvocations:(NSUInteger)value + (void)startWithOptions:(SentryOptions *)options { + [SentryLog configure:options.debug diagnosticLevel:options.diagnosticLevel]; + // We accept the tradeoff that the SDK might not be fully initialized directly after // initializing it on a background thread because scheduling the init synchronously on the main // thread could lead to deadlocks. - [SentryThreadWrapper onMainThread:^{ - startInvocations++; + SENTRY_LOG_DEBUG(@"Starting SDK..."); - [SentryLog configure:options.debug diagnosticLevel:options.diagnosticLevel]; + startInvocations++; - SentryClient *newClient = [[SentryClient alloc] initWithOptions:options]; - [newClient.fileManager moveAppStateToPreviousAppState]; - [newClient.fileManager moveBreadcrumbsToPreviousBreadcrumbs]; + SentryClient *newClient = [[SentryClient alloc] initWithOptions:options]; + [newClient.fileManager moveAppStateToPreviousAppState]; + [newClient.fileManager moveBreadcrumbsToPreviousBreadcrumbs]; - SentryScope *scope = 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]]; - SENTRY_LOG_DEBUG(@"SDK initialized! Version: %@", SentryMeta.versionString); + SentryScope *scope + = 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]]; + SENTRY_LOG_DEBUG(@"SDK initialized! Version: %@", SentryMeta.versionString); + + SENTRY_LOG_DEBUG(@"Dispatching init work required to run on main thread."); + [SentryThreadWrapper onMainThread:^{ + SENTRY_LOG_DEBUG(@"SDK main thread init started..."); [SentrySDK installIntegrations]; [SentryCrashWrapper.sharedInstance startBinaryImageCache]; diff --git a/Sources/Sentry/SentrySpan.m b/Sources/Sentry/SentrySpan.m index 7cc57f96225..95bbbcf5183 100644 --- a/Sources/Sentry/SentrySpan.m +++ b/Sources/Sentry/SentrySpan.m @@ -28,6 +28,7 @@ @implementation SentrySpan { NSMutableDictionary *_data; NSMutableDictionary *_tags; + NSObject *_stateLock; BOOL _isFinished; } @@ -48,6 +49,7 @@ - (instancetype)initWithContext:(SentrySpanContext *)context } _tags = [[NSMutableDictionary alloc] init]; + _stateLock = [[NSObject alloc] init]; _isFinished = NO; _status = kSentrySpanStatusUndefined; @@ -147,7 +149,9 @@ - (void)setMeasurement:(NSString *)name value:(NSNumber *)value unit:(SentryMeas - (BOOL)isFinished { - return _isFinished; + @synchronized(_stateLock) { + return _isFinished; + } } - (void)finish @@ -159,7 +163,9 @@ - (void)finish - (void)finishWithStatus:(SentrySpanStatus)status { self.status = status; - _isFinished = YES; + @synchronized(_stateLock) { + _isFinished = YES; + } if (self.timestamp == nil) { self.timestamp = [SentryDependencyContainer.sharedInstance.dateProvider date]; SENTRY_LOG_DEBUG(@"Setting span timestamp: %@ at system time %llu", self.timestamp, diff --git a/Sources/Sentry/SentryStacktraceBuilder.m b/Sources/Sentry/SentryStacktraceBuilder.m index 8748c3a4c30..7a841f7777e 100644 --- a/Sources/Sentry/SentryStacktraceBuilder.m +++ b/Sources/Sentry/SentryStacktraceBuilder.m @@ -6,6 +6,7 @@ #import "SentryCrashSymbolicator.h" #import "SentryFrame.h" #import "SentryFrameRemover.h" +#import "SentryLog.h" #import "SentryStacktrace.h" #import @@ -106,6 +107,7 @@ - (SentryStacktrace *)buildStacktraceForCurrentThread - (nullable SentryStacktrace *)buildStacktraceForCurrentThreadAsyncUnsafe { + SENTRY_LOG_DEBUG(@"Building async-unsafe stack trace..."); SentryCrashStackCursor stackCursor; sentrycrashsc_initSelfThread(&stackCursor, 0); stackCursor.symbolicate = sentrycrashsymbolicator_symbolicate_async_unsafe; diff --git a/Sources/Sentry/SentryThreadWrapper.m b/Sources/Sentry/SentryThreadWrapper.m index c11e6478dbd..58816a4c27d 100644 --- a/Sources/Sentry/SentryThreadWrapper.m +++ b/Sources/Sentry/SentryThreadWrapper.m @@ -1,4 +1,5 @@ #import "SentryThreadWrapper.h" +#import "SentryLog.h" NS_ASSUME_NONNULL_BEGIN @@ -22,8 +23,10 @@ - (void)threadFinished:(NSUUID *)threadID + (void)onMainThread:(void (^)(void))block { if ([NSThread isMainThread]) { + SENTRY_LOG_DEBUG(@"Already on main thread."); block(); } else { + SENTRY_LOG_DEBUG(@"Dispatching asynchronously to main queue."); dispatch_async(dispatch_get_main_queue(), block); } } diff --git a/Sources/Sentry/include/SentryProfiler.h b/Sources/Sentry/include/SentryProfiler.h index 59b672507eb..fa833d80469 100644 --- a/Sources/Sentry/include/SentryProfiler.h +++ b/Sources/Sentry/include/SentryProfiler.h @@ -28,6 +28,12 @@ SENTRY_EXTERN NSString *const kSentryProfilerSerializationKeyFrameRates; SENTRY_EXTERN_C_BEGIN +/** + * Disable profiling when running with TSAN because it produces a TSAN false positive, similar to + * the situation described here: https://github.com/envoyproxy/envoy/issues/2561 + */ +BOOL threadSanitizerIsPresent(void); + NSString *profilerTruncationReasonName(SentryProfilerTruncationReason reason); SENTRY_EXTERN_C_END diff --git a/Sources/SentryCrash/Recording/SentryCrashBinaryImageCache.c b/Sources/SentryCrash/Recording/SentryCrashBinaryImageCache.c index 5852b6aa3bc..9e412937151 100644 --- a/Sources/SentryCrash/Recording/SentryCrashBinaryImageCache.c +++ b/Sources/SentryCrash/Recording/SentryCrashBinaryImageCache.c @@ -75,9 +75,12 @@ static sentrycrashbic_cacheChangeCallback imageRemovedCallback = NULL; static void binaryImageAdded(const struct mach_header *header, intptr_t slide) { + pthread_mutex_lock(&binaryImagesMutex); if (tailNode == NULL) { + pthread_mutex_unlock(&binaryImagesMutex); return; } + pthread_mutex_unlock(&binaryImagesMutex); Dl_info info; if (!dladdr(header, &info) || info.dli_fname == NULL) { return; diff --git a/Sources/SentryCrash/Recording/Tools/SentryCrashStackCursor_SelfThread.m b/Sources/SentryCrash/Recording/Tools/SentryCrashStackCursor_SelfThread.m index 13c47e1e0c3..2b4d0f179a0 100644 --- a/Sources/SentryCrash/Recording/Tools/SentryCrashStackCursor_SelfThread.m +++ b/Sources/SentryCrash/Recording/Tools/SentryCrashStackCursor_SelfThread.m @@ -25,6 +25,7 @@ #include "SentryCrashStackCursor_SelfThread.h" #include "SentryCrashStackCursor_Backtrace.h" +#import "SentryLog.h" #include // #define SentryCrashLogger_LocalLevel TRACE @@ -57,17 +58,23 @@ int backtraceLength; if (@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)) { if (stitchSwiftAsync) { + SENTRY_LOG_DEBUG(@"Retrieving backtrace with async swift stitching..."); backtraceLength = (int)backtrace_async((void **)context->backtrace, MAX_BACKTRACE_LENGTH, NULL); } else { + SENTRY_LOG_DEBUG(@"Retrieving backtrace without async swift stitching..."); backtraceLength = backtrace((void **)context->backtrace, MAX_BACKTRACE_LENGTH); } } else { + SENTRY_LOG_DEBUG( + @"Retrieving backtrace without async swift stitching (old platform versions)..."); backtraceLength = backtrace((void **)context->backtrace, MAX_BACKTRACE_LENGTH); } #else + SENTRY_LOG_DEBUG(@"Retrieving backtrace without async swift stitching (old Xcode versions)..."); int backtraceLength = backtrace((void **)context->backtrace, MAX_BACKTRACE_LENGTH); #endif + SENTRY_LOG_DEBUG(@"Finished retrieving backtrace."); sentrycrashsc_initWithBacktrace(cursor, context->backtrace, backtraceLength, skipEntries + 1); } diff --git a/Tests/SentryTests/Integrations/Performance/FramesTracking/SentryFramesTrackerTests.swift b/Tests/SentryTests/Integrations/Performance/FramesTracking/SentryFramesTrackerTests.swift index f776683a24f..8d1916af3d8 100644 --- a/Tests/SentryTests/Integrations/Performance/FramesTracking/SentryFramesTrackerTests.swift +++ b/Tests/SentryTests/Integrations/Performance/FramesTracking/SentryFramesTrackerTests.swift @@ -174,6 +174,11 @@ private extension SentryFramesTrackerTests { #if os(iOS) || os(macOS) || targetEnvironment(macCatalyst) func assertProfilingData(slow: UInt? = nil, frozen: UInt? = nil, frameRates: UInt? = nil) throws { + if threadSanitizerIsPresent() { + // profiling data will not have been gathered with TSAN running + return + } + func assertFrameInfo(frame: [String: NSNumber]) throws { XCTAssertNotNil(frame["timestamp"], "Expected a timestamp for the frame.") XCTAssertNotNil(frame["value"], "Expected a duration value for the frame.") diff --git a/Tests/SentryTests/PrivateSentrySDKOnlyTests.swift b/Tests/SentryTests/PrivateSentrySDKOnlyTests.swift index 5a2574a3546..ae11a7fc313 100644 --- a/Tests/SentryTests/PrivateSentrySDKOnlyTests.swift +++ b/Tests/SentryTests/PrivateSentrySDKOnlyTests.swift @@ -125,6 +125,9 @@ class PrivateSentrySDKOnlyTests: XCTestCase { * Smoke Tests profiling via PrivateSentrySDKOnly. Actual profiling unit tests are done elsewhere. */ func testProfilingStartAndCollect() throws { + if threadSanitizerIsPresent() { + throw XCTSkip("Profiler does not run if thread sanitizer is attached.") + } let options = Options() options.dsn = TestConstants.dsnAsString(username: "SentryFramesTrackingIntegrationTests") let client = TestClient(options: options) diff --git a/Tests/SentryTests/SentryCrash/SentryCrashCPU_Tests.m b/Tests/SentryTests/SentryCrash/SentryCrashCPU_Tests.m index ac777d231fb..5e96e36ea4f 100644 --- a/Tests/SentryTests/SentryCrash/SentryCrashCPU_Tests.m +++ b/Tests/SentryTests/SentryCrash/SentryCrashCPU_Tests.m @@ -40,9 +40,26 @@ @implementation SentryCrashCPU_Tests - (void)testCPUState { + NSObject *notificationObject = [[NSObject alloc] init]; TestThread *thread = [[TestThread alloc] init]; + thread.notificationObject = notificationObject; + + XCTestExpectation *exp = [self expectationWithDescription:@"thread started"]; + [NSNotificationCenter.defaultCenter + addObserverForName:@"io.sentry.test.TestThread.main" + object:notificationObject + queue:nil + usingBlock:^(NSNotification *_Nonnull __unused notification) { + [NSNotificationCenter.defaultCenter + removeObserver:self + name:@"io.sentry.test.TestThread.main" + object:notificationObject]; + [exp fulfill]; + }]; + [thread start]; - [NSThread sleepForTimeInterval:0.1]; + [self waitForExpectationsWithTimeout:1 handler:nil]; + kern_return_t kr; kr = thread_suspend(thread.thread); XCTAssertTrue(kr == KERN_SUCCESS, @""); diff --git a/Tests/SentryTests/SentryCrash/SentryCrashCachedData_Tests.m b/Tests/SentryTests/SentryCrash/SentryCrashCachedData_Tests.m index 0999a63393f..929df7d57e4 100644 --- a/Tests/SentryTests/SentryCrash/SentryCrashCachedData_Tests.m +++ b/Tests/SentryTests/SentryCrash/SentryCrashCachedData_Tests.m @@ -38,10 +38,27 @@ - (void)testGetThreadName sentrycrashccd_close(); NSString *expectedName = @"This is a test thread"; - TestThread *thread = [TestThread new]; + NSObject *notificationObject = [[NSObject alloc] init]; + TestThread *thread = [[TestThread alloc] init]; + thread.notificationObject = notificationObject; thread.name = expectedName; + + XCTestExpectation *exp = [self expectationWithDescription:@"thread started"]; + [NSNotificationCenter.defaultCenter + addObserverForName:@"io.sentry.test.TestThread.main" + object:notificationObject + queue:nil + usingBlock:^(NSNotification *_Nonnull __unused notification) { + [NSNotificationCenter.defaultCenter + removeObserver:self + name:@"io.sentry.test.TestThread.main" + object:notificationObject]; + [exp fulfill]; + }]; + [thread start]; - [NSThread sleepForTimeInterval:0.1]; + [self waitForExpectationsWithTimeout:1 handler:nil]; + sentrycrashccd_init(10); [NSThread sleepForTimeInterval:0.1]; [thread cancel]; diff --git a/Tests/SentryTests/SentryCrash/SentryStacktraceBuilderTests.swift b/Tests/SentryTests/SentryCrash/SentryStacktraceBuilderTests.swift index 0d8a73f98bc..9f7c74b2788 100644 --- a/Tests/SentryTests/SentryCrash/SentryStacktraceBuilderTests.swift +++ b/Tests/SentryTests/SentryCrash/SentryStacktraceBuilderTests.swift @@ -75,48 +75,76 @@ class SentryStacktraceBuilderTests: XCTestCase { XCTAssertTrue(filteredFrames.count == 1, "The frames must be ordered from caller to callee, or oldest to youngest.") } - func testConcurrentStacktraces() { - guard #available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) else { return } + func testConcurrentStacktraces() throws { + guard #available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) else { + throw XCTSkip("Not available for earlier platform versions") + } SentrySDK.start { options in options.dsn = TestConstants.dsnAsString(username: "SentryStacktraceBuilderTests") options.swiftAsyncStacktraces = true + options.debug = true } let waitForAsyncToRun = expectation(description: "Wait async functions") Task { + print("\(Date()) [Sentry] [TEST] running async task...") let filteredFrames = await self.firstFrame() waitForAsyncToRun.fulfill() XCTAssertGreaterThanOrEqual(filteredFrames, 3, "The Stacktrace must include the async callers.") } - wait(for: [waitForAsyncToRun], timeout: 1) + + var timeout: TimeInterval = 1 + #if !os(watchOS) && !os(tvOS) + // observed the async task taking a long time to finish if TSAN is attached + if threadSanitizerIsPresent() { + timeout = 10 + } + #endif // !os(watchOS) || !os(tvOS) + wait(for: [waitForAsyncToRun], timeout: timeout) } - func testConcurrentStacktraces_noStitching() { - guard #available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) else { return } + func testConcurrentStacktraces_noStitching() throws { + guard #available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) else { + throw XCTSkip("Not available for earlier platform versions") + } SentrySDK.start { options in options.dsn = TestConstants.dsnAsString(username: "SentryStacktraceBuilderTests") options.swiftAsyncStacktraces = false + options.debug = true } let waitForAsyncToRun = expectation(description: "Wait async functions") Task { + print("\(Date()) [Sentry] [TEST] running async task...") let filteredFrames = await self.firstFrame() waitForAsyncToRun.fulfill() XCTAssertGreaterThanOrEqual(filteredFrames, 1, "The Stacktrace must have only one function.") } - wait(for: [waitForAsyncToRun], timeout: 1) + + var timeout: TimeInterval = 1 + #if !os(watchOS) && !os(tvOS) + // observed the async task taking a long time to finish if TSAN is attached + if threadSanitizerIsPresent() { + timeout = 10 + } + #endif // !os(watchOS) || !os(tvOS) + wait(for: [waitForAsyncToRun], timeout: timeout) } @available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) func firstFrame() async -> Int { + print("\(Date()) [Sentry] [TEST] first async frame about to await...") return await innerFrame1() } @available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) func innerFrame1() async -> Int { - await Task { @MainActor in }.value + print("\(Date()) [Sentry] [TEST] second async frame about to await on task...") + await Task { @MainActor in + print("\(Date()) [Sentry] [TEST] executing task inside second async frame...") + }.value return await innerFrame2() } @@ -127,37 +155,7 @@ class SentryStacktraceBuilderTests: XCTestCase { let filteredFrames = actual.frames .compactMap({ $0.function }) .filter { needed.contains(where: $0.contains) } + print("\(Date()) [Sentry] [TEST] returning filtered frames.") return filteredFrames.count - - } - - func asyncFrame1(expect: XCTestExpectation) { - fixture.queue.asyncAfter(deadline: DispatchTime.now()) { - self.asyncFrame2(expect: expect) - } - } - - func asyncFrame2(expect: XCTestExpectation) { - fixture.queue.async { - self.asyncAssertion(expect: expect) - } - } - - func asyncAssertion(expect: XCTestExpectation) { - let actual = fixture.sut.buildStacktraceForCurrentThread() - - let filteredFrames = actual.frames.filter { frame in - return frame.function?.contains("testAsyncStacktraces") ?? false || - frame.function?.contains("asyncFrame1") ?? false || - frame.function?.contains("asyncFrame2") ?? false - } - let startFrames = actual.frames.filter { frame in - return frame.stackStart?.boolValue ?? false - } - - XCTAssertTrue(filteredFrames.count >= 3, "The Stacktrace must include the async callers.") - XCTAssertTrue(startFrames.count >= 3, "The Stacktrace must have async continuation markers.") - - expect.fulfill() } } diff --git a/Tests/SentryTests/SentryCrash/SentryThreadInspectorTests.swift b/Tests/SentryTests/SentryCrash/SentryThreadInspectorTests.swift index 2014700a1d2..767316040e6 100644 --- a/Tests/SentryTests/SentryCrash/SentryThreadInspectorTests.swift +++ b/Tests/SentryTests/SentryCrash/SentryThreadInspectorTests.swift @@ -90,18 +90,27 @@ class SentryThreadInspectorTests: XCTestCase { let sut = self.fixture.getSut(testWithRealMachineContextWrapper: true) + let lock = NSLock() for _ in 0.. Bool { + print("[Sentry] [TEST] [\(#file):\(#line) starting install.") installedInTheMainThread = Thread.isMainThread MainThreadTestIntegration.expectation?.fulfill() MainThreadTestIntegration.expectation = nil diff --git a/Tests/SentryTests/Transaction/SentryTracerTests.swift b/Tests/SentryTests/Transaction/SentryTracerTests.swift index aed308c3297..e63ca895a87 100644 --- a/Tests/SentryTests/Transaction/SentryTracerTests.swift +++ b/Tests/SentryTests/Transaction/SentryTracerTests.swift @@ -193,7 +193,13 @@ class SentryTracerTests: XCTestCase { XCTAssertEqual(2, fixture.dispatchQueue.blockOnMainInvocations.count, "The NSTimer must be started and cancelled on the main thread.") } - func testCancelDeadlineTimer_TracerDeallocated() { + func testCancelDeadlineTimer_TracerDeallocated() throws { +#if !os(tvOS) && !os(watchOS) + if threadSanitizerIsPresent() { + throw XCTSkip("doesn't currently work with TSAN enabled. the tracer instance remains retained by something in the TSAN dylib, and we cannot debug the memory graph with TSAN attached to see what is retaining it. it's likely out of our control.") + } +#endif // !os(tvOS) && !os(watchOS) + var invocations = 0 fixture.dispatchQueue.blockBeforeMainBlock = { // The second invocation the block for invalidating the timer @@ -551,7 +557,13 @@ class SentryTracerTests: XCTestCase { XCTAssertEqual(expectedEndTimestamp, sut.timestamp) } - func testIdleTimeout_TracerDeallocated() { + func testIdleTimeout_TracerDeallocated() throws { +#if !os(tvOS) && !os(watchOS) + if threadSanitizerIsPresent() { + throw XCTSkip("doesn't currently work with TSAN enabled. the tracer instance remains retained by something in the TSAN dylib, and we cannot debug the memory graph with TSAN attached to see what is retaining it. it's likely out of our control.") + } +#endif // !os(tvOS) && !os(watchOS) + // Interact with sut in extra function so ARC deallocates it func getSut() { let sut = fixture.getSut(idleTimeout: fixture.idleTimeout, dispatchQueueWrapper: fixture.dispatchQueue) @@ -561,6 +573,7 @@ class SentryTracerTests: XCTestCase { getSut() + // dispatch the idle timeout block manually for dispatchAfterBlock in fixture.dispatchQueue.dispatchAfterInvocations.invocations { dispatchAfterBlock.block() } diff --git a/scripts/no-changes-in-high-risk-files.sh b/scripts/no-changes-in-high-risk-files.sh index d2f5f91696f..790f3f01be1 100755 --- a/scripts/no-changes-in-high-risk-files.sh +++ b/scripts/no-changes-in-high-risk-files.sh @@ -5,7 +5,7 @@ set -euo pipefail ACTUAL=$(shasum -a 256 ./Sources/Sentry/SentryNSURLSessionTaskSearch.m ./Sources/Sentry/SentryNetworkTracker.m ./Sources/Sentry/SentryUIViewControllerSwizzling.m ./Sources/Sentry/SentryNSDataSwizzling.m ./Sources/Sentry/SentrySubClassFinder.m ./Sources/Sentry/SentryCoreDataSwizzling.m ./Sources/Sentry/SentrySwizzleWrapper.m ./Sources/Sentry/include/SentrySwizzle.h ./Sources/Sentry/SentrySwizzle.m) EXPECTED="819d5ca5e3db2ac23c859b14c149b7f0754d3ae88bea1dba92c18f49a81da0e1 ./Sources/Sentry/SentryNSURLSessionTaskSearch.m -57056426d77fd1cc27e72bdcea4bbf617dd8c1e82eecf003df8f0e348aae90ab ./Sources/Sentry/SentryNetworkTracker.m +526c44c5f06a1d03fcb7ed813cee8aac520ffe539b911a44d5d8bd1c85e91a0e ./Sources/Sentry/SentryNetworkTracker.m 132a491c706bdb68b47c2e0a14aeaa5c611664f34156565dbfc874b360d6a742 ./Sources/Sentry/SentryUIViewControllerSwizzling.m e95e62ec7363984f20c78643bb7d992a41a740f97e1befb71525ac34caf88b37 ./Sources/Sentry/SentryNSDataSwizzling.m cc3849725bd1733515c71742872bed94ca47d2c115ef9d8c98383eae2e171925 ./Sources/Sentry/SentrySubClassFinder.m diff --git a/scripts/tests-with-thread-sanitizer.sh b/scripts/tests-with-thread-sanitizer.sh index 5e19bb5b6cb..47266b8b811 100755 --- a/scripts/tests-with-thread-sanitizer.sh +++ b/scripts/tests-with-thread-sanitizer.sh @@ -1,19 +1,19 @@ #!/bin/bash -set -euo pipefail +set -euox pipefail # When enableThreadSanitizer is enabled and ThreadSanitizer finds an issue, # the logs only show failing tests, but don't highlight the threading issues. -# Therefore we print a hint to find the threading issues. -env NSUnbufferedIO=YES xcodebuild -workspace Sentry.xcworkspace -scheme Sentry -configuration Test -enableThreadSanitizer YES \ - -destination "platform=iOS Simulator,OS=14.4,name=iPhone 11" \ +# Therefore we print a hint to find the threading issues. Profiler doesn't +# run when it detects TSAN is present, so we skip those tests. +env NSUnbufferedIO=YES CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO xcodebuild -workspace Sentry.xcworkspace -scheme Sentry -configuration Test -enableThreadSanitizer YES \ + -destination "platform=iOS Simulator,OS=latest,name=iPhone 14" \ + -skip-testing:"SentryProfilerTests" \ test | tee thread-sanitizer.log | xcpretty -t testStatus=$? if [ $testStatus -eq 0 ]; then echo "ThreadSanitizer didn't find problems." - exit 0 else echo "ThreadSanitizer found problems or one of the tests failed. Search for \"ThreadSanitizer\" in the thread-sanitizer.log artifact for more details." - exit 1 fi