Skip to content

Conversation

noahsmartin
Copy link
Contributor

@noahsmartin noahsmartin commented Aug 12, 2025

SentryApplication was doing 2 things. It was wrapping UI/NSApplication methods for unit testing purposes, and it was attempting to provide background thread safe access to isActive. I say attempting because it used Notification Center to listen for foreground/background and change a state var, but didn't have any kind of locking. So it was prone to a data race where the main thread would write to it through the notification at the same time a background thread read from it.

The other issue was the AppKit version direct called in to NSApplication and made no attempt at being thread safe, so this object had different amounts of safety depending on the platform.

This PR separates these two behaviors, so SentryApplication is just the system API wrapping (with some extra post-processing on system APIs) and ThreadsafeApplication is a UIKit only thread safe API. This ThreadsafeApplication now uses locks to ensure there isn't a data race. Luckily our code was not relying on the macOS version being thread safe, and now that is just more clear at compile time.

#skip-changelog

@noahsmartin noahsmartin force-pushed the updateApplication branch 2 times, most recently from 721aa8c to fa65d1b Compare August 12, 2025 23:04
Copy link

codecov bot commented Aug 12, 2025

❌ 1 Tests Failed:

Tests completed Failed Passed Skipped
6389 1 6388 33
View the top 1 failed test(s) by shortest run time
iOS_SwiftUI_UITests.LaunchUITests::testTransactionSpan
Stack Traces | 0s run time
.../iOS-SwiftUI/iOS-SwiftUI-UITests/LaunchUITests.swift:18 - Failed to get matching snapshots: Timed out while evaluating UI query.

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

@noahsmartin noahsmartin force-pushed the updateApplication branch 3 times, most recently from a115144 to 00a642f Compare August 13, 2025 03:18
Copy link
Contributor

github-actions bot commented Aug 13, 2025

Performance metrics 🚀

  Plain With Sentry Diff
Startup time 1198.58 ms 1231.93 ms 33.35 ms
Size 23.75 KiB 928.30 KiB 904.55 KiB

Baseline results on branch: main

Startup times

Revision Plain With Sentry Diff
ebc72be 1221.24 ms 1249.66 ms 28.42 ms
fc0757d 1231.83 ms 1248.98 ms 17.15 ms
f97a070 1218.88 ms 1253.12 ms 34.24 ms
f5d202b 1237.90 ms 1259.49 ms 21.59 ms
1a34ddc 1218.94 ms 1251.86 ms 32.92 ms
ea5a59b 1222.87 ms 1253.47 ms 30.60 ms
83bb978 1238.33 ms 1260.04 ms 21.71 ms
fdea6f5 1216.08 ms 1241.82 ms 25.73 ms
ccf1278 1226.84 ms 1248.51 ms 21.67 ms
9e0030e 1222.78 ms 1242.23 ms 19.45 ms

App size

Revision Plain With Sentry Diff
ebc72be 23.75 KiB 908.22 KiB 884.47 KiB
fc0757d 23.75 KiB 850.73 KiB 826.98 KiB
f97a070 23.75 KiB 858.68 KiB 834.93 KiB
f5d202b 23.75 KiB 904.53 KiB 880.78 KiB
1a34ddc 23.75 KiB 919.88 KiB 896.13 KiB
ea5a59b 23.75 KiB 874.46 KiB 850.71 KiB
83bb978 23.75 KiB 920.64 KiB 896.89 KiB
fdea6f5 23.75 KiB 867.15 KiB 843.40 KiB
ccf1278 23.75 KiB 877.15 KiB 853.40 KiB
9e0030e 23.75 KiB 893.72 KiB 869.97 KiB

Previous results on branch: updateApplication

Startup times

Revision Plain With Sentry Diff
ee17551 1217.88 ms 1245.02 ms 27.14 ms
621c433 1219.33 ms 1246.92 ms 27.59 ms
8ce26a2 1225.54 ms 1242.65 ms 17.10 ms
289850d 1216.71 ms 1239.00 ms 22.29 ms

App size

Revision Plain With Sentry Diff
ee17551 23.75 KiB 928.53 KiB 904.78 KiB
621c433 23.75 KiB 928.56 KiB 904.81 KiB
8ce26a2 23.75 KiB 928.00 KiB 904.25 KiB
289850d 23.75 KiB 928.00 KiB 904.25 KiB

@noahsmartin noahsmartin force-pushed the updateApplication branch 2 times, most recently from 0a54d3f to fc48fe4 Compare August 13, 2025 12:47
@noahsmartin noahsmartin marked this pull request as ready for review August 13, 2025 12:51
cursor[bot]

This comment was marked as outdated.

@ohwhen
Copy link

ohwhen commented Aug 13, 2025

🔥

Copy link
Member

@philprime philprime left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good progress, let some comments

@@ -185,7 +185,8 @@ - (void)enrichScope:(SentryScope *)scope
// The UIWindowScene is unavailable on visionOS
#if SENTRY_TARGET_REPLAY_SUPPORTED

NSArray<UIWindow *> *appWindows = SentryDependencyContainer.sharedInstance.application.windows;
NSArray<UIWindow *> *appWindows =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

m: Is there a good reason why we shouldn't use the thread-safe version here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getWindows is only defined on the non-thread safe version. AFAIK it's never safe to have a UIWindow instance off the main thread

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or in Swift terms, UIWindow is not Sendable

Comment on lines +10 to +11
// This can only be accessed on the main thread
var mainThread_isActive: Bool { get }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

m: Is there a good reason for us to not fully synchronize the application onto the main thread? The app life cycle will always live of main

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure what you mean, this is conformed to by NSApplication and NSApplication can only be used on the main thread. We could write a helper/extension that is a function designed to be called from any context that then dispatches sync to the main thread and check "isActive" but there is no use case in the codebase that requires that right now so I would be hesitant to add extra code, particularly code that uses "dispatch_sync" since that's easy to cause deadlock with and I try to avoid it

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I see what you mean, like by doing something similar to what we do with UIApplication. The reason is that before this PR we were not doing anything to try to make this accessible to a background thread, and it was only called from the main thread. Still only called from the main thread and I wanted to avoid any extra changes here other than cleaning up the code to make it more clear what each piece does and fixing the one race condition bug

#if !os(macOS) && !os(watchOS) && !SENTRY_NO_UIKIT
import UIKit

@objc @_spi(Private) public final class SentryThreadsafeApplication: NSObject {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

m: We should replace the application in the dependency container with this wrapper, so we can not use the wrong one by accident.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The dependency container has both, that's the idea behind this PR because previously some of SentryApplication was designed to be called form anywhere and some was not. The one that can only be used form the main thread is still useful to have in the dependency container IMO since it allows us to mock those calls in tests


#endif /* SentryUIApplication_Private_h */
#endif
//
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

l: If we don't need it, remove it please.

Copy link
Contributor

🚨 Detected changes in high risk code 🚨

High-risk code can easily blow up and is hard to test. We had severe bugs in the past. Be extra careful when changing these files, and have an extra careful look at these:

  • Sources/Sentry/SentryUIViewControllerSwizzling.m

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants