-
Notifications
You must be signed in to change notification settings - Fork 338
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
Conversation
tagging @MarkVillacampa here too since he's worked on these checks quite a bit |
Generated by 🚫 Danger |
@@ -99,23 +99,26 @@ class CustomerInfoManager { | |||
func fetchAndCacheCustomerInfoIfStale(appUserID: String, | |||
isAppBackgrounded: Bool, | |||
completion: CustomerInfoCompletion?) { |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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
// 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 | ||
} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
Note: this was only an issue in production, because of this check which would early-return true in sandbox |
There was a problem hiding this 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.
// 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 | ||
} |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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.
@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 |
closing in favor of #3875 |
Alternative approach to #3875, also meant to address #3871 We're currently looking at a field called Environment in the local receipt to check whether the app is running in sandbox on macOS. However, since it's an undocumented field, we can't trust that it will always be there, so if the value is production then we check the bundle's signature, which can be slow to run. This PR changes that behavior so that we only perform that check if the value for that variable isn't available, which would cause a receipt parsing failure since it's marked as non-optional. Still need to write tests for this, but it should be significantly easier than for the other PR.
Alternative approach to #3875, also meant to address #3871 We're currently looking at a field called Environment in the local receipt to check whether the app is running in sandbox on macOS. However, since it's an undocumented field, we can't trust that it will always be there, so if the value is production then we check the bundle's signature, which can be slow to run. This PR changes that behavior so that we only perform that check if the value for that variable isn't available, which would cause a receipt parsing failure since it's marked as non-optional. Still need to write tests for this, but it should be significantly easier than for the other PR.
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:
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.