Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix mac os sandbox check slowness #3875

Open
wants to merge 5 commits into
base: main
Choose a base branch
from

Conversation

aboedo
Copy link
Member

@aboedo aboedo commented May 7, 2024

Addresses #3871

Solves the issue by ensuring that the calls to isSandbox are performed on a worker thread, and that we're pre-loading the values.

I'm not a huge fan of this solution since it isn't technically perfectly safe, meaning that it's possible (albeit very unlikely) for us to still check sandbox values in the main thread.

Additionally I don't love how hard it is to actually write tests for it.

The ideal solutions would be:

  • change the check so that it loads instantly, which would fix the issue without further changes. But... I don't know of a faster way to check on macOS, sadly.
  • update isSandbox to make it asynchronous... which would be a BIG refactor, affecting a large chunk of the codebase. It doesn't feel worth the trouble.

@aboedo aboedo requested a review from a team May 7, 2024 19:02
@aboedo aboedo self-assigned this May 7, 2024
@aboedo aboedo requested a review from MarkVillacampa May 7, 2024 19:02
@aboedo
Copy link
Member Author

aboedo commented May 7, 2024

tagging @MarkVillacampa here too since he's worked on these checks quite a bit

@RevenueCat-Danger-Bot
Copy link

RevenueCat-Danger-Bot commented May 7, 2024

1 Message
📖 Size increase: 1.67 KB

Generated by 🚫 Danger

@@ -99,23 +99,26 @@ class CustomerInfoManager {
func fetchAndCacheCustomerInfoIfStale(appUserID: String,
isAppBackgrounded: Bool,
completion: CustomerInfoCompletion?) {
Copy link
Member Author

Choose a reason for hiding this comment

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

Note: I'd recommend setting the diff to hide whitespace, since it makes it much clearer that all this change does is to add a single line to run the code in a worker thread

@@ -32,6 +32,8 @@ final class DefaultMacAppStoreDetector: MacAppStoreDetector {
/// it checked the common name but was changed to an extension check to make it more
/// future-proof.
///
/// - warning: this method may take a long time to run on macOS. It should not be called on the main thread.
Copy link
Member Author

Choose a reason for hiding this comment

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

these warnings should ideally be fixed by the solutions described in the PR description, but I couldn't find a great way to make them work, sadly

Comment on lines +147 to +151
// eager-load the isSandbox value from a worker thread so that's immediately available
// this is useful on macOS where it may take long for the value to compute
self.operationDispatcher.dispatchOnWorkerThread {
_ = self._isSandbox
}
Copy link
Member Author

Choose a reason for hiding this comment

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

this pre-loading makes the changes quite safe. After all, for the most part we'll only be loading once, as soon as SystemInfo is created, and from a worker thread.
So other accesses will likely not hit this from a different thread. Ideally we'd modify the property to be asynchronous though so that we're completely covered.

Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm I imagine we could potentially be calculating the isSandbox property multiple times at the same time right? One here on initialization in a worker thread, then another in the main thread, if that property gets accessed from the main thread (Or even more if accessed from multiple threads). I think this might be ok, but I wanted to mention it.

I also see that we are accessing the sandboxEnvironmentDetector.isSandbox in multiple places, like the DeviceCache or the StoreKit1Wrapper. Do we need to handle those as well? (That can happen in different PRs though)

Copy link
Member Author

Choose a reason for hiding this comment

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

we could, but it shouldn't be a problem other than it being inefficient the first time. But we were already doing that before this PR, the changes here just ensure that it happens on a worker thread instead

@@ -1743,21 +1743,23 @@ private extension Purchases {
// Note: it's important that we observe "will enter foreground" instead of
// "did become active" so that we don't trigger cache updates in the middle
// of purchases due to pop-ups stealing focus from the app.
self.updateAllCachesIfNeeded(isAppBackgrounded: false)
self.dispatchSyncSubscriberAttributes()
self.operationDispatcher.dispatchOnWorkerThread {
Copy link
Member Author

Choose a reason for hiding this comment

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

I really dislike how indirect this is, but it's not a bad idea to dispatch this work on the worker thread in any case

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm slightly concerned about any unknown side-effects... but nothing comes to mind right now. I think this should be safe yeah.

Copy link
Member Author

Choose a reason for hiding this comment

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

I believe it should be okay, all this code is already meant to be working on its own without user interaction (other than the user opening the SDK, that is)

}

expect(invokedCompletion).toEventually(beTrue())
expect(self.mockOperationDispatcher.invokedDispatchOnWorkerThread) == true
Copy link
Member Author

Choose a reason for hiding this comment

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

Not a fan of this since it's not a very accurate check, other things could be invoking this method.
I don't know of a much better way to test when using this implementation, though.

Copy link
Contributor

Choose a reason for hiding this comment

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

throwing something crazy in here, and not sure it will work, but maybe modifying dispatchOnWorkerThread to this:

func dispatchOnWorkerThread(delay: Delay = .none, caller: String = #function, file: String = #file, line: Int = #line, block: @escaping @Sendable () -> Void) {

would let you check what has called dispatchOnWorkerThread in the mock?

Copy link
Member Author

Choose a reason for hiding this comment

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

definitely an interesting approach, I'll take a look

@aboedo aboedo added the fix A bug fix label May 7, 2024
@aboedo
Copy link
Member Author

aboedo commented May 7, 2024

Note: this was only an issue in production, because of this check which would early-return true in sandbox

Copy link
Contributor

@tonidero tonidero left a comment

Choose a reason for hiding this comment

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

I'm worrying that this solution might be a bit brittle, as you said... but I believe it should be safe to merge this.

Comment on lines +147 to +151
// eager-load the isSandbox value from a worker thread so that's immediately available
// this is useful on macOS where it may take long for the value to compute
self.operationDispatcher.dispatchOnWorkerThread {
_ = self._isSandbox
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm I imagine we could potentially be calculating the isSandbox property multiple times at the same time right? One here on initialization in a worker thread, then another in the main thread, if that property gets accessed from the main thread (Or even more if accessed from multiple threads). I think this might be ok, but I wanted to mention it.

I also see that we are accessing the sandboxEnvironmentDetector.isSandbox in multiple places, like the DeviceCache or the StoreKit1Wrapper. Do we need to handle those as well? (That can happen in different PRs though)

@@ -1743,21 +1743,23 @@ private extension Purchases {
// Note: it's important that we observe "will enter foreground" instead of
// "did become active" so that we don't trigger cache updates in the middle
// of purchases due to pop-ups stealing focus from the app.
self.updateAllCachesIfNeeded(isAppBackgrounded: false)
self.dispatchSyncSubscriberAttributes()
self.operationDispatcher.dispatchOnWorkerThread {
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm slightly concerned about any unknown side-effects... but nothing comes to mind right now. I think this should be safe yeah.

@aboedo
Copy link
Member Author

aboedo commented May 8, 2024

@tonidero yeah I definitely still don't love this solution. Maybe it makes sense to limit its effects to macOS for the sake of minimizing impact... Or to give the asynchronous isSandbox approach another try, even though that one feels a lot more impactful in terms of the changes

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

Successfully merging this pull request may close these issues.

None yet

5 participants