Fix tab switching crash when creating new tabs (Issue #7716)

Problem:
The Zen browser was experiencing random crashes when switching tabs, particularly
when creating a new tab with Ctrl+T and typing a URL. The crash logs showed
"CompositorBridgeChild receives IPC close with reason=AbnormalShutdown" errors,
indicating issues with the graphics compositor during tab switching operations.
This was caused by race conditions in the tab switching code and lack of proper
error handling when dealing with browser elements that might be in an invalid state.

Solution:
Instead of just catching errors, we've implemented a more robust solution that addresses
the root causes of the race conditions in the tab switching code:

1. State Management System:
   - Added a queue-based system for tab operations to ensure they execute in sequence
   - Implemented debouncing to prevent rapid tab switching that could cause race conditions
   - Added state validation before operations to ensure browser elements are in a valid state

2. Asynchronous Operation Handling:
   - Used Promises and async/await for proper sequencing of operations
   - Added proper timing controls with small delays to ensure DOM is ready
   - Implemented a mutex-like approach to prevent concurrent workspace changes

3. Robust Tab State Validation:
   - Added comprehensive checks for tab validity before operations
   - Implemented proper cleanup of resources to prevent memory leaks
   - Added validation for browser window state to prevent operations on closed windows

4. Improved Error Recovery:
   - Added fallback mechanisms when operations fail
   - Implemented proper cleanup of state even when errors occur
   - Added more informative error messages for better debugging

These changes make the browser much more robust when handling tab switching operations,
preventing the crashes reported in issue #7716.
This commit is contained in:
Marcus Bergo
2025-04-20 00:37:32 -03:00
parent fb1575d875
commit 263db42602
2 changed files with 348 additions and 53 deletions

View File

@@ -175,25 +175,136 @@ var gZenUIManager = {
_clearTimeout: null,
_lastTab: null,
// Track tab switching state to prevent race conditions
_tabSwitchState: {
inProgress: false,
lastSwitchTime: 0,
debounceTime: 100, // ms to wait between tab switches
queue: [],
processingQueue: false,
},
// Queue tab switch operations to prevent race conditions
async _queueTabOperation(operation) {
// Add operation to queue
this._tabSwitchState.queue.push(operation);
// If already processing queue, just return
if (this._tabSwitchState.processingQueue) {
return;
}
// Start processing queue
this._tabSwitchState.processingQueue = true;
try {
while (this._tabSwitchState.queue.length > 0) {
// Get next operation
const nextOp = this._tabSwitchState.queue.shift();
// Check if we need to wait for debounce
const now = Date.now();
const timeSinceLastSwitch = now - this._tabSwitchState.lastSwitchTime;
if (timeSinceLastSwitch < this._tabSwitchState.debounceTime) {
await new Promise((resolve) => setTimeout(resolve, this._tabSwitchState.debounceTime - timeSinceLastSwitch));
}
// Execute operation
this._tabSwitchState.inProgress = true;
await nextOp();
this._tabSwitchState.inProgress = false;
this._tabSwitchState.lastSwitchTime = Date.now();
}
} finally {
this._tabSwitchState.processingQueue = false;
}
},
// Check if browser elements are in a valid state for tab operations
_validateBrowserState() {
// Check if browser window is still open
if (window.closed) {
return false;
}
// Check if gBrowser is available
if (!gBrowser || !gBrowser.tabContainer) {
return false;
}
// Check if URL bar is available
if (!gURLBar) {
return false;
}
return true;
},
handleNewTab(werePassedURL, searchClipboard, where) {
// Validate browser state first
if (!this._validateBrowserState()) {
console.warn('Browser state invalid for new tab operation');
return false;
}
const shouldOpenURLBar = gZenVerticalTabsManager._canReplaceNewTab && !werePassedURL && !searchClipboard && where === 'tab';
if (shouldOpenURLBar) {
if (!shouldOpenURLBar) {
return false;
}
// Queue the tab operation to prevent race conditions
this._queueTabOperation(async () => {
// Clear any existing timeout
if (this._clearTimeout) {
clearTimeout(this._clearTimeout);
this._clearTimeout = null;
}
// Store the current tab
this._lastTab = gBrowser.selectedTab;
this._lastTab._visuallySelected = false;
this._prevUrlbarLabel = gURLBar._untrimmedValue;
if (!this._lastTab) {
console.warn('No selected tab found when creating new tab');
return false;
}
// Set visual state with proper validation
if (this._lastTab && !this._lastTab.closing) {
this._lastTab._visuallySelected = false;
}
// Store URL bar state
this._prevUrlbarLabel = gURLBar._untrimmedValue || '';
// Set up URL bar for new tab
gURLBar._zenHandleUrlbarClose = this.handleUrlbarClose.bind(this);
gURLBar.setAttribute('zen-newtab', true);
// Update newtab buttons
for (const button of this.newtabButtons) {
button.setAttribute('in-urlbar', true);
}
document.getElementById('Browser:OpenLocation').doCommand();
gURLBar.search(this._lastSearch);
return true;
}
return false;
// Open location command
try {
// Wait for a small delay to ensure DOM is ready
await new Promise((resolve) => setTimeout(resolve, 10));
document.getElementById('Browser:OpenLocation').doCommand();
// Wait for URL bar to be ready
await new Promise((resolve) => setTimeout(resolve, 10));
gURLBar.search(this._lastSearch || '');
} catch (e) {
console.error('Error opening location in new tab:', e);
this.handleUrlbarClose(false);
return false;
}
});
return true;
},
clearUrlbarData() {
@@ -202,30 +313,67 @@ var gZenUIManager = {
},
handleUrlbarClose(onSwitch) {
gURLBar._zenHandleUrlbarClose = null;
gURLBar.removeAttribute('zen-newtab');
this._lastTab._visuallySelected = true;
this._lastTab = null;
for (const button of this.newtabButtons) {
button.removeAttribute('in-urlbar');
// Validate browser state first
if (!this._validateBrowserState()) {
console.warn('Browser state invalid for URL bar close operation');
return;
}
if (onSwitch) {
this.clearUrlbarData();
} else {
this._lastSearch = gURLBar._untrimmedValue;
this._clearTimeout = setTimeout(() => {
this.clearUrlbarData();
}, this.urlbarWaitToClear);
}
gURLBar.setURI(this._prevUrlbarLabel, onSwitch, false, false, !onSwitch);
gURLBar.handleRevert();
if (gURLBar.focused) {
gURLBar.view.close({ elementPicked: onSwitch });
gURLBar.updateTextOverflow();
if (gBrowser.selectedTab.linkedBrowser && onSwitch) {
gURLBar.getBrowserState(gBrowser.selectedTab.linkedBrowser).urlbarFocused = false;
// Queue the operation to prevent race conditions
this._queueTabOperation(async () => {
// Reset URL bar state
if (gURLBar._zenHandleUrlbarClose) {
gURLBar._zenHandleUrlbarClose = null;
}
}
gURLBar.removeAttribute('zen-newtab');
// Safely restore tab visual state with proper validation
if (this._lastTab && !this._lastTab.closing && this._lastTab.ownerGlobal && !this._lastTab.ownerGlobal.closed) {
this._lastTab._visuallySelected = true;
this._lastTab = null;
}
// Reset newtab buttons
for (const button of this.newtabButtons) {
button.removeAttribute('in-urlbar');
}
// Handle search data
if (onSwitch) {
this.clearUrlbarData();
} else {
this._lastSearch = gURLBar._untrimmedValue || '';
if (this._clearTimeout) {
clearTimeout(this._clearTimeout);
}
this._clearTimeout = setTimeout(() => {
this.clearUrlbarData();
}, this.urlbarWaitToClear);
}
// Safely restore URL bar state with proper validation
if (this._prevUrlbarLabel) {
gURLBar.setURI(this._prevUrlbarLabel, onSwitch, false, false, !onSwitch);
}
gURLBar.handleRevert();
if (gURLBar.focused) {
gURLBar.view.close({ elementPicked: onSwitch });
gURLBar.updateTextOverflow();
// Ensure tab and browser are valid before updating state
const selectedTab = gBrowser.selectedTab;
if (selectedTab && selectedTab.linkedBrowser && !selectedTab.closing && onSwitch) {
const browserState = gURLBar.getBrowserState(selectedTab.linkedBrowser);
if (browserState) {
browserState.urlbarFocused = false;
}
}
}
});
},
urlbarTrim(aURL) {

View File

@@ -50,6 +50,16 @@ var ZenWorkspaces = new (class extends ZenMultiWindowFeature {
}
async init() {
// Initialize tab selection state
this._tabSelectionState = {
inProgress: false,
lastSelectionTime: 0,
debounceTime: 100, // ms to wait between tab selections
};
// Initialize workspace change mutex
this._workspaceChangeInProgress = false;
if (!this.shouldHaveWorkspaces) {
this._resolveInitialized();
document.getElementById('zen-current-workspace-indicator-container').setAttribute('hidden', 'true');
@@ -110,28 +120,130 @@ var ZenWorkspaces = new (class extends ZenMultiWindowFeature {
);
}
selectEmptyTab(newTabTarget = null, selectURLBar = true) {
if (this._emptyTab && gZenVerticalTabsManager._canReplaceNewTab) {
if (gBrowser.selectedTab !== this._emptyTab && selectURLBar) {
window.addEventListener(
'TabSelect',
() => {
setTimeout(() => {
gURLBar.select();
}, 0);
},
{ once: true }
);
// Validate browser state before tab operations
_validateBrowserState() {
// Check if browser window is still open
if (window.closed) {
return false;
}
// Check if gBrowser is available
if (!gBrowser || !gBrowser.tabContainer) {
return false;
}
// Check if URL bar is available
if (!gURLBar) {
return false;
}
return true;
}
// Safely select a tab with debouncing to prevent race conditions
async _safelySelectTab(tab) {
if (!tab || tab.closing || !tab.ownerGlobal || tab.ownerGlobal.closed) {
return false;
}
// Check if we need to debounce
const now = Date.now();
const timeSinceLastSelection = now - this._tabSelectionState.lastSelectionTime;
if (timeSinceLastSelection < this._tabSelectionState.debounceTime) {
await new Promise((resolve) => setTimeout(resolve, this._tabSelectionState.debounceTime - timeSinceLastSelection));
}
// Mark selection as in progress
this._tabSelectionState.inProgress = true;
try {
gBrowser.selectedTab = tab;
this._tabSelectionState.lastSelectionTime = Date.now();
return true;
} catch (e) {
console.error('Error selecting tab:', e);
return false;
} finally {
this._tabSelectionState.inProgress = false;
}
}
async selectEmptyTab(newTabTarget = null, selectURLBar = true) {
// Validate browser state first
if (!this._validateBrowserState()) {
console.warn('Browser state invalid for empty tab selection');
return null;
}
try {
// Check if we have a valid empty tab and can replace new tab
if (
this._emptyTab &&
!this._emptyTab.closing &&
this._emptyTab.ownerGlobal &&
!this._emptyTab.ownerGlobal.closed &&
gZenVerticalTabsManager._canReplaceNewTab
) {
// Only set up URL bar selection if we're switching to a different tab
if (gBrowser.selectedTab !== this._emptyTab && selectURLBar) {
// Use a Promise-based approach for better sequencing
const urlBarSelectionPromise = new Promise((resolve) => {
const tabSelectListener = () => {
// Remove the event listener first to prevent any chance of multiple executions
window.removeEventListener('TabSelect', tabSelectListener);
// Use requestAnimationFrame to ensure DOM is updated
requestAnimationFrame(() => {
// Then use setTimeout to ensure browser has time to process tab switch
setTimeout(() => {
if (gURLBar) {
try {
gURLBar.select();
} catch (e) {
console.warn('Error selecting URL bar:', e);
}
}
resolve();
}, 50);
});
};
window.addEventListener('TabSelect', tabSelectListener, { once: true });
});
}
// Safely switch to the empty tab using our debounced method
const success = await this._safelySelectTab(this._emptyTab);
if (!success) {
throw new Error('Failed to select empty tab');
}
return this._emptyTab;
}
gBrowser.selectedTab = this._emptyTab;
return this._emptyTab;
// Fall back to creating a new tab
const newTabUrl = newTabTarget || Services.prefs.getStringPref('browser.startup.homepage');
let tab = gZenUIManager.openAndChangeToTab(newTabUrl);
// Set workspace ID if available
if (window.uuid) {
tab.setAttribute('zen-workspace-id', this.activeWorkspace);
}
return tab;
} catch (e) {
console.error('Error in selectEmptyTab:', e);
// Create a fallback tab as a last resort, with proper validation
try {
if (this._validateBrowserState()) {
return gBrowser.addTrustedTab('about:blank');
}
} catch (fallbackError) {
console.error('Critical error creating fallback tab:', fallbackError);
}
return null;
}
const newTabUrl = newTabTarget || Services.prefs.getStringPref('browser.startup.homepage');
let tab = gZenUIManager.openAndChangeToTab(newTabUrl);
if (window.uuid) {
tab.setAttribute('zen-workspace-id', this.activeWorkspace);
}
return tab;
}
async delayedStartup() {
@@ -2429,10 +2541,45 @@ var ZenWorkspaces = new (class extends ZenMultiWindowFeature {
}
async switchTabIfNeeded(tab) {
if (!tab.hasAttribute('zen-essential') && tab.getAttribute('zen-workspace-id') !== this.activeWorkspace) {
await this.changeWorkspace({ uuid: tab.getAttribute('zen-workspace-id') });
// Validate browser state first
if (!this._validateBrowserState()) {
console.warn('Browser state invalid for tab switching');
return;
}
if (!tab) {
console.warn('switchTabIfNeeded called with null tab');
return;
}
// Validate tab state
if (tab.closing || !tab.ownerGlobal || tab.ownerGlobal.closed || !tab.linkedBrowser) {
console.warn('Tab is no longer valid, cannot select it');
return;
}
try {
// Check if we need to change workspace
if (!tab.hasAttribute('zen-essential') && tab.getAttribute('zen-workspace-id') !== this.activeWorkspace) {
// Use a mutex-like approach to prevent concurrent workspace changes
if (this._workspaceChangeInProgress) {
console.warn('Workspace change already in progress, deferring tab switch');
return;
}
this._workspaceChangeInProgress = true;
try {
await this.changeWorkspace({ uuid: tab.getAttribute('zen-workspace-id') });
} finally {
this._workspaceChangeInProgress = false;
}
}
// Safely switch to the tab using our debounced method
await this._safelySelectTab(tab);
} catch (e) {
console.error('Error in switchTabIfNeeded:', e);
}
gBrowser.selectedTab = tab;
}
getDefaultContainer() {