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

Bugfix FXIOS-9998 [inactive tabs] Fix crash when deleting and the undoing the inactive tabs multiple times in a row (related improvements FXIOS-9954, FXIOS-10010, FXIOS-9999) #22075

Merged
merged 25 commits into from
Sep 24, 2024

Conversation

ih-codes
Copy link
Collaborator

@ih-codes ih-codes commented Sep 18, 2024

📜 Tickets

Jira ticket FXIOS-9998
Github issue 21953

This will also fix (or improve at least) some other lingering inactive tab bugs:
Jira ticket FXIOS-9954 -- Improvement but not 100% fix
Github issue 21863

Jira ticket FXIOS-9954
Github issue 21971

Jira ticket FXIOS-9954
Github issue 21954

💡 Description

I couldn't reproduce the crash in FXIOS-9998, but after inspecting the crash log it was clear there was something odd going on with wiping and restoring multiple inactive tabs.

So I cleaned up a lot of this logic and reduced dependencies to some of the inner methods (e.g. we don't need to calculate the "viable" deletion index prior to removing a tab, hold onto that index, and then pass it back into updateSelectedTabAfterRemovalOf later. We can just do some math in findRightOrLeftTab.)

Regrettably, the logic for updateSelectedTabAfterRemovalOf is more complicated now, but that's unavoidable as inactive tabs just weren't fully accounted for prior. I tried to document the complexity as best I could.

I also added a huge swath of unit tests because of this complex logic. If anyone has suggestions on making these prettier while still retaining their usefulness, my ears are open. The tests are a bit long so the bot might shame me for PR length. 😅

Actual code changes to review are much smaller!!

📝 Checklist

You have to check all boxes before merging

  • Filled in the above information (tickets numbers and description of your work)
  • Updated the PR name to follow our PR naming guidelines
  • Wrote unit tests and/or ensured the tests suite is passing
  • When working on UI, I checked and implemented accessibility (minimum Dynamic Text and VoiceOver)
  • If needed, I updated documentation / comments for complex code and public methods
  • If needed, added a backport comment (example @Mergifyio backport release/v120)

@ih-codes ih-codes requested a review from a team as a code owner September 18, 2024 22:34
self.updateIndexAfterRemovalOf(tab, deletedIndex: index, viableTabsIndex: viableTabsIndex)
DispatchQueue.main.async { [weak self] in
self?.removeTab(tab, flushToDisk: true)
self?.updateSelectedTabAfterRemovalOf(tab, deletedIndex: index)
Copy link
Collaborator Author

@ih-codes ih-codes Sep 18, 2024

Choose a reason for hiding this comment

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

I was able to simplify things a bit by calculating this "viableTabsIndex" inside findRightOrLeftTab instead of holding onto it and passing it in later.

isSelected: selectedTab?.tabUUID == tab.tabUUID)
}
self.removeTab(tab, flushToDisk: true)
self.updateIndexAfterRemovalOf(tab, deletedIndex: index, viableTabsIndex: viableTabsIndex)
self.updateSelectedTabAfterRemovalOf(tab, deletedIndex: index) // Crash path
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Same simplification here for "viableTabsIndex"

@@ -819,56 +824,96 @@ class LegacyTabManager: NSObject, FeatureFlaggable, TabManager, TabEventHandler
privateConfiguration.preferences.javaScriptCanOpenWindowsAutomatically = allowPopups
}

// returns all activate tabs (private or normal)
private func viableTabs(isPrivate: Bool = false) -> [Tab] {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This method didn't really seem useful anymore. There's a separate task to clean up the inactiveTabs refactor.

@@ -905,24 +950,6 @@ class LegacyTabManager: NSObject, FeatureFlaggable, TabManager, TabEventHandler
storeChanges()
}

// Select the most recently visited tab, IFF it is also the parent tab of the closed tab.
private func selectParentTab(afterRemoving tab: Tab) -> Bool {
Copy link
Collaborator Author

@ih-codes ih-codes Sep 18, 2024

Choose a reason for hiding this comment

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

This was only used in one place, so this logic is essentially moved to updateSelectedTabAfterRemovalOf (formerly called updateIndexAfterRemovalOf) instead.

var normalActiveTabs: [Tab] { get }
var inactiveTabs: [Tab] { get }
var normalInactiveTabs: [Tab] { get }
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Renamed for clarity since private tabs can't be inactive technically, or at least not as we are using "inactive". :)

Copy link
Contributor

@dataports dataports Sep 19, 2024

Choose a reason for hiding this comment

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

Kind of a nit, but I vote keeping this to inactiveTabs because there's a lot of references to "inactiveTabs" without the normal modifier (in other variables, enums, etc) in the codebase that it might actually just be more mental overhead to change the name at this point. Maybe a comment here stating that private tabs can't be inactive would be better?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I can revert this pretty easily for now if we want to have a larger discussion on the value of this change!

I agree, other naming should be updated as well in this case. It could be a separate task if we want to go forward with a change like this.

For me, it was the lack of symmetry between normalActiveTabs and inactiveTabs that was a bit confusing. And that an "active tab" is assumed normal and not private, while an "inactive tab" is assumed also normal and not private. But a "private tab" can't be "inactive tab"—except actually, it can be, we just don't differentiate in the UI or have special private inactive tab logic haha. It starts to get a little confusing to know exactly what each subarray contains as there are now 2 flags at play. 😂

While I was writing the tests I had to be very explicit with this, which was why I thought the naming could maybe be improved across the three tab types we really care about: normal active tabs, normal inactive tabs, and private tabs (technically either active or inactive).

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah maybe revert for now and lets have a larger discussion - I agree that something is not quite right with the naming for the tabs. Maybe we can discuss in the iOS devs channel and make a ticket to change the naming from that? You could basically just put what you wrote here.
If we're gonna fix it, we might as well fix it everywhere in the project 😆

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Sounds good to me! I've started a discussion in slack and made a proposal task for us to discuss! 👍 Will revert this change in the meantime.

@@ -189,16 +189,16 @@ class TabManagerImplementation: LegacyTabManager, Notifiable, WindowSimpleTabsPr
level: .debug,
category: .tabs)

selectTab(tabToSelect)
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

There was an edge case where tabToSelect might be nil so I thought we should handle that explicitly.

let windowData = WindowData(id: windowUUID,
activeTabId: activeTabID,
activeTabId: self.selectedTabUUID ?? UUID(),
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@OrlaM This is the spot where we see selectedTab as nil each time. Just turn on inactive tabs and close a bunch at once, it's 100% reproducible. I've filed a separate ticket as I do not think this is related to the crash I was investigating.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@@ -132,6 +131,15 @@ class LegacyTabManager: NSObject, FeatureFlaggable, TabManager, TabEventHandler
return tabs[_selectedIndex]
}

var selectedTabUUID: UUID? {
guard let selectedTab = self.selectedTab,
let uuid = UUID(uuidString: selectedTab.tabUUID) else {
Copy link
Contributor

Choose a reason for hiding this comment

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

minor nit, but maybe else { return nil } on a new line here? Not married to it though.

Probably something we should talk about in the iOS weekly meeting one day and add stuff to the style guide for guard syntax since I don't see anything there atm

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yeah, let's have a larger discussion. I know Roux has voiced preference to this stylistic choice. I personally find it harder to read when the return paths are not all left aligned. 😅

// Viable tabs for selection are private tabs and normal active tabs. We never want to select a normal inactive tab.
let viableTabsToSelect = removedTab.isPrivate
? privateTabs
: normalActiveTabs
Copy link
Contributor

Choose a reason for hiding this comment

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

I really like this spacing for ternary operator syntax btw, much easier to parse for me

…closing last inactive tabs. Add additional unit tests.
@ih-codes
Copy link
Collaborator Author

Working on some bugfixes on this branch... don't bother reviewing until I work through them tomorrow! 🙏 Will close the PR just so no one wastes time on this until it's ready.

@ih-codes ih-codes closed this Sep 19, 2024
@ih-codes ih-codes reopened this Sep 20, 2024
@ih-codes
Copy link
Collaborator Author

Cleaned this up a bit, fixed the edge case @dataports spotted, and added more tests to catch that situation. I think this is ready for review again. 🙏

@mobiletest-ci-bot
Copy link

mobiletest-ci-bot commented Sep 21, 2024

Warnings
⚠️ Variable 'appendUpLeft' in Large is out of alphabetical order.
⚠️ Variable 'appMenu' in Large is out of alphabetical order.
⚠️ Variable 'arrowClockwise' in Large is out of alphabetical order.
⚠️ Variable 'avatarCircle' in Large is out of alphabetical order.
⚠️ Variable 'back' in Large is out of alphabetical order.
⚠️ Variable 'bookmark' in Large is out of alphabetical order.
⚠️ Variable 'bookmarkFill' in Large is out of alphabetical order.
⚠️ Variable 'bookmarkSlash' in Large is out of alphabetical order.
⚠️ Variable 'bookmarkTrayFill' in Large is out of alphabetical order.
⚠️ Variable 'checkmark' in Large is out of alphabetical order.
⚠️ Variable 'chevronDown' in Large is out of alphabetical order.
⚠️ Variable 'chevronLeft' in Large is out of alphabetical order.
⚠️ Variable 'chevronRight' in Large is out of alphabetical order.
⚠️ Variable 'chevronUp' in Large is out of alphabetical order.
⚠️ Variable 'clipboard' in Large is out of alphabetical order.
⚠️ Variable 'cloud' in Large is out of alphabetical order.
⚠️ Variable 'competitiveness' in Large is out of alphabetical order.
⚠️ Variable 'cookies' in Large is out of alphabetical order.
⚠️ Variable 'creditCard' in Large is out of alphabetical order.
⚠️ Variable 'criticalFill' in Large is out of alphabetical order.
⚠️ Variable 'cross' in Large is out of alphabetical order.
⚠️ Variable 'crossCircleFill' in Large is out of alphabetical order.
⚠️ Variable 'dataClearance' in Large is out of alphabetical order.
⚠️ Variable 'delete' in Large is out of alphabetical order.
⚠️ Variable 'deviceDesktop' in Large is out of alphabetical order.
⚠️ Variable 'deviceDesktopSend' in Large is out of alphabetical order.
⚠️ Variable 'deviceMobile' in Large is out of alphabetical order.
⚠️ Variable 'deviceTablet' in Large is out of alphabetical order.
⚠️ Variable 'download' in Large is out of alphabetical order.
⚠️ Variable 'edit' in Large is out of alphabetical order.
⚠️ Variable 'fingerprinter' in Large is out of alphabetical order.
⚠️ Variable 'folder' in Large is out of alphabetical order.
⚠️ Variable 'forward' in Large is out of alphabetical order.
⚠️ Variable 'globe' in Large is out of alphabetical order.
⚠️ Variable 'helpCircle' in Large is out of alphabetical order.
⚠️ Variable 'history' in Large is out of alphabetical order.
⚠️ Variable 'home' in Large is out of alphabetical order.
⚠️ Variable 'image' in Large is out of alphabetical order.
⚠️ Variable 'lightbulb' in Large is out of alphabetical order.
⚠️ Variable 'lightning' in Large is out of alphabetical order.
⚠️ Variable 'lightningFill' in Large is out of alphabetical order.
⚠️ Variable 'link' in Large is out of alphabetical order.
⚠️ Variable 'location' in Large is out of alphabetical order.
⚠️ Variable 'lock' in Large is out of alphabetical order.
⚠️ Variable 'lockFill' in Large is out of alphabetical order.
⚠️ Variable 'lockSlash' in Large is out of alphabetical order.
⚠️ Variable 'lockSlashFill' in Large is out of alphabetical order.
⚠️ Variable 'login' in Large is out of alphabetical order.
⚠️ Variable 'logoFirefox' in Large is out of alphabetical order.
⚠️ Variable 'nightMode' in Large is out of alphabetical order.
⚠️ Variable 'notificationDot' in Large is out of alphabetical order.
⚠️ Variable 'notificationDotFill' in Large is out of alphabetical order.
⚠️ Variable 'packaging' in Large is out of alphabetical order.
⚠️ Variable 'pageZoom' in Large is out of alphabetical order.
⚠️ Variable 'pin' in Large is out of alphabetical order.
⚠️ Variable 'pinSlash' in Large is out of alphabetical order.
⚠️ Variable 'plus' in Large is out of alphabetical order.
⚠️ Variable 'price' in Large is out of alphabetical order.
⚠️ Variable 'privateMode' in Large is out of alphabetical order.
⚠️ Variable 'privateModeCircleFill' in Large is out of alphabetical order.
⚠️ Variable 'qrCode' in Large is out of alphabetical order.
⚠️ Variable 'quality' in Large is out of alphabetical order.
⚠️ Variable 'readerView' in Large is out of alphabetical order.
⚠️ Variable 'readerViewFill' in Large is out of alphabetical order.
⚠️ Variable 'readingList' in Large is out of alphabetical order.
⚠️ Variable 'readingListAdd' in Large is out of alphabetical order.
⚠️ Variable 'search' in Large is out of alphabetical order.
⚠️ Variable 'settings' in Large is out of alphabetical order.
⚠️ Variable 'share' in Large is out of alphabetical order.
⚠️ Variable 'shield' in Large is out of alphabetical order.
⚠️ Variable 'shieldSlash' in Large is out of alphabetical order.
⚠️ Variable 'shipping' in Large is out of alphabetical order.
⚠️ Variable 'shopping' in Large is out of alphabetical order.
⚠️ Variable 'socialTracker' in Large is out of alphabetical order.
⚠️ Variable 'subtract' in Large is out of alphabetical order.
⚠️ Variable 'sync' in Large is out of alphabetical order.
⚠️ Variable 'syncTabs' in Large is out of alphabetical order.
⚠️ Variable 'tab' in Large is out of alphabetical order.
⚠️ Variable 'tabTray' in Large is out of alphabetical order.
⚠️ Variable 'warning' in Large is out of alphabetical order.
⚠️ Variable 'warningFill' in Large is out of alphabetical order.
⚠️ Variable 'whatsNew' in Large is out of alphabetical order.
⚠️ Variable 'tools' in Large is out of alphabetical order.
⚠️ Variable 'save' in Large is out of alphabetical order.
⚠️ Variable 'print' in Large is out of alphabetical order.
⚠️ Variable 'addToHomeScreen' in Large is out of alphabetical order.
⚠️ Variable 'gridAdd' in Large is out of alphabetical order.
⚠️ Pull Request size seems relatively large. If this Pull Request contains multiple changes, please split each into separate PR will helps faster, easier review. Consider using epic branches for work that would affect main.
Messages
📖 Project coverage: 31.79%
📖 Edited 12 files
📖 Created 0 files

Client.app: Coverage: 30.36

File Coverage
TabManagerMiddleware.swift 7.7% ⚠️
LegacyInactiveTabViewModel.swift 81.05%
Tab.swift 44.4% ⚠️
TabManager.swift 70.0%
TabManagerImplementation.swift 49.83% ⚠️
LegacyTabManager.swift 38.43% ⚠️

Generated by 🚫 Danger Swift against 1cc5604

@ih-codes ih-codes requested a review from a team as a code owner September 23, 2024 21:09
@ih-codes ih-codes merged commit 5e949c4 into main Sep 24, 2024
13 of 14 checks passed
@ih-codes ih-codes deleted the ih/FXIOS-9998-fix-close-inactive-tabs-crash branch September 24, 2024 19:59
@ih-codes ih-codes changed the title Bugfix FXIOS-9998 Fix crash when deleting and the undoing the inactive tabs multiple times in a row (related improvements FXIOS-9954, FXIOS-10010, FXIOS-9999) Bugfix FXIOS-9998 [inactive tabs] Fix crash when deleting and the undoing the inactive tabs multiple times in a row (related improvements FXIOS-9954, FXIOS-10010, FXIOS-9999) Sep 26, 2024
@dsmithpadilla
Copy link
Contributor

@mergify backport release/v131

Copy link
Contributor

mergify bot commented Sep 26, 2024

backport release/v131

✅ Backports have been created

mergify bot pushed a commit that referenced this pull request Sep 26, 2024
…e tabs multiple times in a row (related improvements FXIOS-9954, FXIOS-10010, FXIOS-9999) (#22075)

* Add TabManager / Tab helper getters and improve documentation. Minor improvements. Refactored naming for clarity.

* Fix to refresh the active tab area when all inactive tabs are closed, in case an active tab has been generated.

* Add logic for findRightOrLeftTab to find a neighbouring tab to a removed tab which does not require saving a subarray index prior to removing a the tab.

* Reworked logic for calculating the "next" index to select when removing a tab, which properly takes into account inactive tabs.

* Cleaned up some old inactive tabs nimbus experiment code.

* Add a ton of removeTab unit tests which check edge cases and common situations for normal active tabs, normal inactive tabs, and private tabs.

* Make sure TabManagerTests do not use debug timeout for inactive tabs.

* Bitrise test workaround for main thread issues related to FXIOS-10110.

* Rewrite unrelated failing test not to rely on the global notification center.

* Bump timeout for Danger.

* Bypass failing UI test to expedite PR, FIXME ticket FXIOS-10122

(cherry picked from commit 5e949c4)

# Conflicts:
#	firefox-ios/Client/TabManagement/Legacy/LegacyTabManager.swift
#	firefox-ios/Client/TabManagement/TabManagerImplementation.swift
#	firefox-ios/firefox-ios-tests/Tests/ClientTests/TabTests.swift
ih-codes added a commit that referenced this pull request Sep 26, 2024
…oing the inactive tabs multiple times in a row (related improvements FXIOS-9954, FXIOS-10010, FXIOS-9999) (backport #22075) (#22217)

* Bugfix FXIOS-9998 Fix crash when deleting and the undoing the inactive tabs multiple times in a row (related improvements FXIOS-9954, FXIOS-10010, FXIOS-9999) (#22075)

* Add TabManager / Tab helper getters and improve documentation. Minor improvements. Refactored naming for clarity.

* Fix to refresh the active tab area when all inactive tabs are closed, in case an active tab has been generated.

* Add logic for findRightOrLeftTab to find a neighbouring tab to a removed tab which does not require saving a subarray index prior to removing a the tab.

* Reworked logic for calculating the "next" index to select when removing a tab, which properly takes into account inactive tabs.

* Cleaned up some old inactive tabs nimbus experiment code.

* Add a ton of removeTab unit tests which check edge cases and common situations for normal active tabs, normal inactive tabs, and private tabs.

* Make sure TabManagerTests do not use debug timeout for inactive tabs.

* Bitrise test workaround for main thread issues related to FXIOS-10110.

* Rewrite unrelated failing test not to rely on the global notification center.

* Bump timeout for Danger.

* Bypass failing UI test to expedite PR, FIXME ticket FXIOS-10122

(cherry picked from commit 5e949c4)

# Conflicts:
#	firefox-ios/Client/TabManagement/Legacy/LegacyTabManager.swift
#	firefox-ios/Client/TabManagement/TabManagerImplementation.swift
#	firefox-ios/firefox-ios-tests/Tests/ClientTests/TabTests.swift

* Resolve merge conflicts.

---------

Co-authored-by: Isabella <[email protected]>
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.

5 participants