Skip to content

Conversation

Cramsden
Copy link
Contributor

📜 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

Before After
Demo

📝 Checklist

  • I filled in the ticket numbers and a description of my work
  • I updated the PR name to follow our PR naming guidelines
  • I ensured unit tests pass and wrote tests for new code
  • If working on UI, I checked and implemented accessibility (Dynamic Text and VoiceOver)
  • If adding telemetry, I read the data stewardship requirements and will request a data review
  • If needed, I updated documentation and added comments to complex code
  • If needed, I added a backport comment (example @Mergifyio backport release/v150.0)

@Cramsden Cramsden requested a review from a team as a code owner September 10, 2025 20:05
@Cramsden Cramsden requested review from thatswinnie, dataports, lmarceau and ih-codes and removed request for a team September 10, 2025 20:05
Copy link
Contributor

@lmarceau lmarceau left a 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

private nonisolated let tabDataStore: TabDataStore
private let tabSessionStore: TabSessionStore
private let imageStore: DiskImageStore?
private nonisolated let imageStore: DiskImageStore?
Copy link
Contributor

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?

Copy link
Contributor Author

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

Copy link
Collaborator

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?
Screenshot 2025-09-10 at 3 13 53 PM

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.

Copy link
Contributor Author

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.

Copy link
Collaborator

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!

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 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:

Screenshot 2025-09-11 at 8 41 50 PM

@@ -138,7 +139,8 @@ enum SearchTelemetryValues {
}
}

class SearchTelemetry {
// TODO: FXIOS Make SearchTelemetry actually sendable
Copy link
Contributor

Choose a reason for hiding this comment

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

Needs ticket number 👀

Comment on lines +711 to +716
tabManager.undoCloseInactiveTabs()
let inactiveTabs = self.refreshInactiveTabs(uuid: uuid)
let refreshAction = TabPanelMiddlewareAction(inactiveTabModels: inactiveTabs,
windowUUID: uuid,
actionType: TabPanelMiddlewareActionType.refreshInactiveTabs)
store.dispatchLegacy(refreshAction)
Copy link
Contributor Author

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?

Copy link
Collaborator

@ih-codes ih-codes left a 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,
Copy link
Collaborator

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

Copy link
Contributor Author

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?
Copy link
Collaborator

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?
Screenshot 2025-09-10 at 3 13 53 PM

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
Copy link
Collaborator

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
Copy link
Collaborator

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
Copy link
Collaborator

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

Copy link
Contributor Author

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.

Copy link
Collaborator

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])
Copy link
Collaborator

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 😆

Copy link
Contributor Author

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.

@lmarceau lmarceau self-requested a review September 10, 2025 23:16
@ih-codes
Copy link
Collaborator

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 await calls. 👀 I'll add a topic to the agenda for this since it's the first time I'm seeing it!

Copy link
Collaborator

@FilippoZazzeroni FilippoZazzeroni left a 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 }
Copy link
Collaborator

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 ?

Copy link
Collaborator

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
Screenshot 2025-09-11 at 9 57 33 AM

Copy link
Contributor Author

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
Copy link
Collaborator

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 ?

Copy link
Contributor Author

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!

@ih-codes ih-codes force-pushed the epic-branch/swift-migration branch from 3a72799 to 0dd835e Compare September 11, 2025 16:03
@ih-codes ih-codes requested a review from issammani as a code owner September 11, 2025 16:03
Copy link
Collaborator

@ih-codes ih-codes 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 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.

@mobiletest-ci-bot
Copy link

mobiletest-ci-bot commented Sep 11, 2025

Messages
📖 Project coverage: 37.29%
📖 Edited 39 files
📖 Created 0 files

Client.app: Coverage: 36.96

File Coverage
BrowserViewController+TabToolbarDelegate.swift 1.85% ⚠️
TabManagerMiddleware.swift 27.5% ⚠️
TabErrorTelemetryHelper.swift 12.71% ⚠️
TopTabDisplayManager.swift 15.53% ⚠️
SearchTelemetry.swift 5.17% ⚠️
NoImageModeHelper.swift 12.12% ⚠️
NightModeHelper.swift 30.77% ⚠️
ReaderPanel.swift 29.33% ⚠️
SearchViewModel.swift 52.68%
AccountSyncHandler.swift 86.47%
MessageCardMiddleware.swift 98.63%
TabToolbarHelper.swift 47.81% ⚠️
WindowManager.swift 84.87%
SummarizeMiddleware.swift 84.62%
TabManager.swift 80.0%
WindowSimpleTabsCoordinator.swift 95.0%
ToolbarMiddleware.swift 93.01%
TabManagerImplementation.swift 53.45%

libStorage.a: Coverage: 56.74

File Coverage
DiskImageStore.swift 67.61%

Generated by 🚫 Danger Swift against cacb5db

@lmarceau
Copy link
Contributor

There seems to be one issue with the changes. If I have a lot of tabs open and then "Close all tabs" from the tab tray, the UI is frozen. You can open a bunch of tabs by using the option under the debug menu. I clicked it 3 times (and then you need to wait a bit so all the tabs are opened before you try to close all tabs).

Screenshot 2025-09-11 at 2 59 33 PM

Here's a video. After I click to close all tabs the UI is frozen for a good chunk of time.

Simulator.Screen.Recording.-.iPhone.16.-.2025-09-11.at.15.00.38.mp4

@Cramsden
Copy link
Contributor Author

Looks like I got some additional commits in here that shouldn't be. Cleaning up the commit history now.

@lmarceau
Copy link
Contributor

lmarceau commented Sep 11, 2025

There seems to be one issue with the changes. If I have a lot of tabs open and then "Close all tabs" from the tab tray, the UI is frozen. You can open a bunch of tabs by using the option under the debug menu. I clicked it 3 times (and then you need to wait a bit so all the tabs are opened before you try to close all tabs). [...]

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

@Cramsden Cramsden force-pushed the cr/FXIOS-13450_strict-concurrency-in-tab-manager branch from f00aeb8 to cacb5db Compare September 11, 2025 19:29
Copy link
Collaborator

@mattreaganmozilla mattreaganmozilla left a 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.

Comment on lines +51 to +56
guard Thread.isMainThread else {
self.logger.log(
"MessageCardMiddleware is not being called from the main thread!",
level: .fatal,
category: .tabs
)
Copy link
Collaborator

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!
Copy link
Collaborator

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

Copy link
Contributor Author

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?

Comment on lines 47 to 65
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)
}
}
Copy link
Collaborator

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)?

Copy link
Collaborator

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)

Copy link
Contributor Author

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 }
Copy link
Contributor

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 }
Copy link
Contributor

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 }

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.

7 participants