-
Notifications
You must be signed in to change notification settings - Fork 3.1k
Refactor FXIOS-13450 [Swift 6] Fix strict concurrency warnings in Tab Manager #29257
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
base: epic-branch/swift-migration
Are you sure you want to change the base?
Refactor FXIOS-13450 [Swift 6] Fix strict concurrency warnings in Tab Manager #29257
Conversation
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.
First round of review! I want to pull the PR locally as well tomorrow morning
firefox-ios/Client/Frontend/Browser/Tabs/Middleware/TabManagerMiddleware.swift
Outdated
Show resolved
Hide resolved
private nonisolated let tabDataStore: TabDataStore | ||
private let tabSessionStore: TabSessionStore | ||
private let imageStore: DiskImageStore? | ||
private nonisolated let imageStore: DiskImageStore? |
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.
Same question with tabDataStore
and imageStore
, feels like we'll need to work on those to improve?
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 are both actors so they are sendable and should not need to be isolated to the main actor
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.
Can we mark these 2 protocols as requiring an Actor then to be safe? 👀
protocol Foo : Actor { }
I also noticed that if we don't do that, we don't get this error, which actually sounds like something we should care about?
EXCEPT I noticed that fetchWindowDataUUIDs
is marked nonisolated
in the implementation, so it should also be in the protocol, and then that warning goes away.
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.
hmmm I don't see this warning at all. I agree the protocol method should be marked as nonisolated. I dont know if I totally see the benefit of enforcing the protocol to be an actor. It shouldn't have to be it just needs to be Thread safe in one way or another.
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.
When I tried marking the protocol as Actor isolated, I noticed the compiler picked up places we called the actor (via the let tabDataStore: TabDataStore
property) and suggested that isolated methods needed an await
. If you add the Actor to TabDataStore
protocol you'll see what I mean. Does this mean we could potentially be calling actor-isolated methods without an await
, if the protocol doesn't specify that an actor implements it?? 😬 That's my fear!
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 think that this actually works because of the structure of the protocol... and we we actually don't need to specify the nonisolated because of the way the protocol is structured. All isolated methods are async and all non isolated methods are not marked as async. If the protocol has a function that breaks the actor isolation we will get a compiler error:

@@ -138,7 +139,8 @@ enum SearchTelemetryValues { | |||
} | |||
} | |||
|
|||
class SearchTelemetry { | |||
// TODO: FXIOS Make SearchTelemetry actually sendable |
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.
Needs ticket number 👀
tabManager.undoCloseInactiveTabs() | ||
let inactiveTabs = self.refreshInactiveTabs(uuid: uuid) | ||
let refreshAction = TabPanelMiddlewareAction(inactiveTabModels: inactiveTabs, | ||
windowUUID: uuid, | ||
actionType: TabPanelMiddlewareActionType.refreshInactiveTabs) | ||
store.dispatchLegacy(refreshAction) |
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.
@cyndichin I removed this task since I was hoping that now that this is all isolated to the main actor we might not need it anymore? Would you be willing to help me test?
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.
First pass, I'll also test more tomorrow!
@@ -185,30 +186,32 @@ class SearchViewModel: FeatureFlaggable, LoaderListener { | |||
let tempSearchQuery = searchQuery | |||
suggestClient?.query(searchQuery, |
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 hoping later we can make the callback @MainActor
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.
ya dude.
private nonisolated let tabDataStore: TabDataStore | ||
private let tabSessionStore: TabSessionStore | ||
private let imageStore: DiskImageStore? | ||
private nonisolated let imageStore: DiskImageStore? |
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.
Can we mark these 2 protocols as requiring an Actor then to be safe? 👀
protocol Foo : Actor { }
I also noticed that if we don't do that, we don't get this error, which actually sounds like something we should care about?
EXCEPT I noticed that fetchWindowDataUUIDs
is marked nonisolated
in the implementation, so it should also be in the protocol, and then that warning goes away.
@@ -554,20 +537,17 @@ class TabManagerImplementation: NSObject, | |||
|
|||
private func restoreTabs() { | |||
tabs = [Tab]() | |||
Task { | |||
Task { @MainActor in |
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.
🙏
} | ||
buildTabRestore(window: windowData) | ||
TabErrorTelemetryHelper.shared.validateTabCountAfterRestoringTabs(windowUUID) | ||
// Log on main thread, where computed `tab` properties can be accessed without risk of races |
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.
Guess we don't need this comment anymore probably 😌
@@ -53,6 +53,7 @@ class TabManagerTests: XCTestCase { | |||
super.tearDown() | |||
} | |||
|
|||
@MainActor |
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 might be a good file to just annotate the entire test class since all the tab manager tests need @MainActor
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.
My personal preference was that each test be marked as main actor so tests aren't automatically put on the main thread if they don't mean to be? but I can make this update if you prefer.
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 don't have a strong preference, but it just seems easier if the entire test suite is for an object that is main actor isolated to mark the whole test class instead of a lot of functions. But I agree if it's just a few test cases it's better to mark just the tests. I don't have strong feelings either way!
@@ -187,7 +187,9 @@ class BrowserViewControllerWebViewDelegateTests: XCTestCase { | |||
subject.webView(tab.webView!, | |||
decidePolicyFor: MockNavigationAction(url: url, | |||
type: .linkActivated)) { _ in | |||
XCTAssertNotNil(subject.pendingRequests[url.absoluteString]) | |||
ensureMainThread { | |||
XCTAssertNotNil(subject.pendingRequests[url.absoluteString]) |
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 test seems odd, without an expectation I don't imagine this ever executes anyway 😆
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.
lol ya this test does nothing.
I think your latest warning fixes look good! ✅ I think my comment here should be addressed though. This is a bit of an interesting case... if an actor implements a protocol it seems that the compiler doesn't know that via the protocol definition so it might not know to |
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.
@Cramsden just some question and curiosities around the implementation. Overall looks good to me, but maybe extra eye are needed.
@@ -82,9 +82,15 @@ class AccountSyncHandler: TabEventHandler { | |||
/// To prevent multiple tab actions to have a separate syncs, we sync after 5s of no tab activity | |||
private func performClientsAndTabsSync() { | |||
guard profile.hasSyncableAccount() else { return } | |||
debouncer.call { [weak self] in self?.storeTabs() } | |||
debouncer.call { [weak self] in | |||
guard let self = self else {return } |
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.
Question, do we need to retain self here ?
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 also don't think this solves the compiler warning from before @Cramsden, I'm still seeing the Sendable self issue
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.
ya it doesn't... I will take it out
@@ -24,6 +24,7 @@ final class TabManagerMiddleware: FeatureFlaggable { | |||
private let summarizerServiceFactory: SummarizerServiceFactory | |||
private let tabsPanelTelemetry: TabsPanelTelemetry | |||
|
|||
@MainActor |
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.
Just a curiosity, why in these cases we need MainActor on all methods marking the class isn't enough right ?
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.
It is preferred for middlewares to isolate the whole class to the main actor but at this point I don't want the provider to be main actor isolated because we will want the protocol for middlewares to be isolated to the main actor and that will effect them all. This is just an interim step!
3a72799
to
0dd835e
Compare
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 going to approve because I want these changes on my branch and any other concerns I had can be resolved later! 👌 Nice work!! This was an annoying one to fix I bet haha.
Client.app: Coverage: 36.96
libStorage.a: Coverage: 56.74
Generated by 🚫 Danger Swift against cacb5db |
Looks like I got some additional commits in here that shouldn't be. Cleaning up the commit history now. |
Turns out this is not a new issue, fun! LGTM with the PR changes, since Isabella said there will be follow up changes as well |
f00aeb8
to
cacb5db
Compare
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 haven't run through this branch exhaustively yet, but nothing major jumped out. Just a few minor nits that we can address later.
guard Thread.isMainThread else { | ||
self.logger.log( | ||
"MessageCardMiddleware is not being called from the main thread!", | ||
level: .fatal, | ||
category: .tabs | ||
) |
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.
Very minor nit but this is probably worth an assert
to make it more obvious and catch it during dev if it happens. NAB though
uuid: ReservedWindowUUID(uuid: windowUUID, isNew: false), | ||
windowManager: windowManager) | ||
|
||
var tabManager: TabManager! |
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.
Nit: I know it's annoying to point this out but ideally we should probably avoid this IUO even when we know it's safe, if for no other reason than consistent hygiene/etiquette. Definitely NAB 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.
Even in tests? I guess that is fair that crashing is not as clear as failing?
if Thread.isMainThread { | ||
MainActor.assumeIsolated { | ||
tabManager = injectedTabManager ?? TabManagerImplementation( | ||
profile: profile, | ||
uuid: ReservedWindowUUID(uuid: windowUUID, isNew: false), | ||
windowManager: windowManager | ||
) | ||
AppContainer.shared.register(service: MockThemeManager() as ThemeManager) | ||
} | ||
} else { | ||
DispatchQueue.main.sync { | ||
tabManager = injectedTabManager ?? TabManagerImplementation( | ||
profile: profile, | ||
uuid: ReservedWindowUUID(uuid: windowUUID, isNew: false), | ||
windowManager: windowManager | ||
) | ||
AppContainer.shared.register(service: MockThemeManager() as ThemeManager) | ||
} | ||
} |
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.
Is the reason we can't use ensureMainThread
here because of the sync
(instead of async
)?
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.
Yes -- I originally put this in when doing the ThemeManager stuff. We have an open ticket for hopefully coming up with a better solution, I know you and I talked a bit about this too 😅 FXIOS-13151
I hypothesized that we'd want to ensure the setup is complete before running a test (hence the sync
), especially tests and setUp() methods isolated to the main actor. (I didn't actually test to see if this causes flakiness 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.
It is my hope that this can be isolated to the main actor but I think that is going to be a pretty big change.
) { [weak self] _ in | ||
self?.longPressGestureRecognizers.forEach { gesture in | ||
gesture.minimumPressDuration = longPressDuration | ||
guard let self = self else { return } |
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.
unimportant nit but you can shorten this to guard let self else { return }
userInfo: nil, | ||
repeats: false) | ||
impressionTelemetryTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: false, block: { [weak self] _ in | ||
guard let self = self else { return } |
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.
unimportant nit but you can shorten this to guard let self else { return }
📜 Tickets
Jira ticket
💡 Description
This PR cleans up a lot of the functionality in the TabManager that is marked as async but does not need to be in order to simplify the general logic.
This PR also isolates the TabManager protocol to the MainActor
This PR fixes any other warnings that were in TabManager with strict concurrency on
🎥 Demos
Demo
📝 Checklist
@Mergifyio backport release/v150.0
)