Fix browser pane hangs from redundant portal refreshes (#2353)
* Fix browser pane hangs from redundant portal refreshes (#2326) * Preserve force-refresh recovery for browser panes * Keep browser refresh generations monotonic * Preserve inspector reattach refreshes during churn * Fix hosted inspector restore after portal reattach
This commit is contained in:
parent
0666a98ae9
commit
90a9edb761
1 changed files with 78 additions and 8 deletions
|
|
@ -60,6 +60,10 @@ private extension WKWebView {
|
|||
}
|
||||
}
|
||||
|
||||
var browserPortalRequiresRenderingStateReattach: Bool {
|
||||
browserPortalNeedsRenderingStateReattach
|
||||
}
|
||||
|
||||
func browserPortalNotifyHidden(reason: String) {
|
||||
browserPortalNeedsRenderingStateReattach = true
|
||||
let firedSelectors = ["viewDidHide", "_exitInWindow"].filter {
|
||||
|
|
@ -2060,6 +2064,9 @@ final class WindowBrowserPortal: NSObject {
|
|||
private var hasDeferredFullSyncScheduled = false
|
||||
private var hasExternalGeometrySyncScheduled = false
|
||||
private var geometryObservers: [NSObjectProtocol] = []
|
||||
// Keep generations monotonic even if a pending entry is cleared during hide/detach churn.
|
||||
private var nextHostedWebViewRefreshGeneration: UInt64 = 0
|
||||
private var pendingHostedWebViewRefreshes: [ObjectIdentifier: PendingHostedWebViewRefresh] = [:]
|
||||
|
||||
private struct Entry {
|
||||
weak var webView: WKWebView?
|
||||
|
|
@ -2075,6 +2082,12 @@ final class WindowBrowserPortal: NSObject {
|
|||
var transientRecoveryRetriesRemaining: Int
|
||||
}
|
||||
|
||||
private struct PendingHostedWebViewRefresh {
|
||||
var generation: UInt64 = 0
|
||||
var asyncWorkItem: DispatchWorkItem?
|
||||
var delayedWorkItem: DispatchWorkItem?
|
||||
}
|
||||
|
||||
private var entriesByWebViewId: [ObjectIdentifier: Entry] = [:]
|
||||
private var webViewByAnchorId: [ObjectIdentifier: ObjectIdentifier] = [:]
|
||||
|
||||
|
|
@ -2503,6 +2516,22 @@ final class WindowBrowserPortal: NSObject {
|
|||
#endif
|
||||
}
|
||||
|
||||
private func cancelPendingHostedWebViewRefreshes(
|
||||
for webViewId: ObjectIdentifier,
|
||||
keepGeneration: Bool = false
|
||||
) {
|
||||
guard var pending = pendingHostedWebViewRefreshes[webViewId] else { return }
|
||||
pending.asyncWorkItem?.cancel()
|
||||
pending.delayedWorkItem?.cancel()
|
||||
if keepGeneration {
|
||||
pending.asyncWorkItem = nil
|
||||
pending.delayedWorkItem = nil
|
||||
pendingHostedWebViewRefreshes[webViewId] = pending
|
||||
} else {
|
||||
pendingHostedWebViewRefreshes.removeValue(forKey: webViewId)
|
||||
}
|
||||
}
|
||||
|
||||
private func invalidateHostedWebViewGeometry(
|
||||
_ webView: WKWebView,
|
||||
in containerView: WindowBrowserSlotView,
|
||||
|
|
@ -2523,6 +2552,16 @@ final class WindowBrowserPortal: NSObject {
|
|||
reason: String
|
||||
) {
|
||||
guard !containerView.isHidden else { return }
|
||||
let webViewId = ObjectIdentifier(webView)
|
||||
|
||||
// Bind/reveal/fullscreen refreshes can stack up during a single layout churn.
|
||||
// Keep only the latest follow-up passes so reattach work does not pile up on
|
||||
// the main thread while browser panes are moving between hosts.
|
||||
cancelPendingHostedWebViewRefreshes(for: webViewId, keepGeneration: true)
|
||||
var pending = pendingHostedWebViewRefreshes[webViewId] ?? PendingHostedWebViewRefresh()
|
||||
nextHostedWebViewRefreshGeneration &+= 1
|
||||
let generation = nextHostedWebViewRefreshGeneration
|
||||
pending.generation = generation
|
||||
|
||||
runHostedWebViewRefreshPass(
|
||||
webView,
|
||||
|
|
@ -2531,8 +2570,10 @@ final class WindowBrowserPortal: NSObject {
|
|||
phase: "immediate",
|
||||
reattachRenderingState: true
|
||||
)
|
||||
DispatchQueue.main.async { [weak self, weak webView, weak containerView] in
|
||||
|
||||
let asyncWorkItem = DispatchWorkItem { [weak self, weak webView, weak containerView] in
|
||||
guard let self, let webView, let containerView else { return }
|
||||
guard self.pendingHostedWebViewRefreshes[webViewId]?.generation == generation else { return }
|
||||
self.runHostedWebViewRefreshPass(
|
||||
webView,
|
||||
in: containerView,
|
||||
|
|
@ -2541,8 +2582,20 @@ final class WindowBrowserPortal: NSObject {
|
|||
reattachRenderingState: true
|
||||
)
|
||||
}
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.03) { [weak self, weak webView, weak containerView] in
|
||||
guard let self, let webView, let containerView else { return }
|
||||
pending.asyncWorkItem = asyncWorkItem
|
||||
|
||||
let delayedWorkItem = DispatchWorkItem { [weak self, weak webView, weak containerView] in
|
||||
guard let self else { return }
|
||||
defer {
|
||||
if var current = self.pendingHostedWebViewRefreshes[webViewId],
|
||||
current.generation == generation {
|
||||
current.asyncWorkItem = nil
|
||||
current.delayedWorkItem = nil
|
||||
self.pendingHostedWebViewRefreshes[webViewId] = current
|
||||
}
|
||||
}
|
||||
guard let webView, let containerView else { return }
|
||||
guard self.pendingHostedWebViewRefreshes[webViewId]?.generation == generation else { return }
|
||||
self.runHostedWebViewRefreshPass(
|
||||
webView,
|
||||
in: containerView,
|
||||
|
|
@ -2551,6 +2604,11 @@ final class WindowBrowserPortal: NSObject {
|
|||
reattachRenderingState: true
|
||||
)
|
||||
}
|
||||
pending.delayedWorkItem = delayedWorkItem
|
||||
pendingHostedWebViewRefreshes[webViewId] = pending
|
||||
|
||||
DispatchQueue.main.async(execute: asyncWorkItem)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.03, execute: delayedWorkItem)
|
||||
}
|
||||
|
||||
private enum HostedWebViewPresentationUpdateKind {
|
||||
|
|
@ -2633,6 +2691,7 @@ final class WindowBrowserPortal: NSObject {
|
|||
}
|
||||
|
||||
func detachWebView(withId webViewId: ObjectIdentifier) {
|
||||
cancelPendingHostedWebViewRefreshes(for: webViewId)
|
||||
guard let entry = entriesByWebViewId.removeValue(forKey: webViewId) else { return }
|
||||
if let anchor = entry.anchorView {
|
||||
webViewByAnchorId.removeValue(forKey: ObjectIdentifier(anchor))
|
||||
|
|
@ -2735,9 +2794,10 @@ final class WindowBrowserPortal: NSObject {
|
|||
|
||||
func forceRefreshWebView(withId webViewId: ObjectIdentifier, reason: String) {
|
||||
guard ensureInstalled() else { return }
|
||||
let refreshSource = "forceRefresh:\(reason)"
|
||||
synchronizeWebView(
|
||||
withId: webViewId,
|
||||
source: "forceRefresh",
|
||||
source: refreshSource,
|
||||
forcePresentationRefresh: true
|
||||
)
|
||||
guard let entry = entriesByWebViewId[webViewId],
|
||||
|
|
@ -2746,10 +2806,12 @@ final class WindowBrowserPortal: NSObject {
|
|||
!containerView.isHidden else {
|
||||
return
|
||||
}
|
||||
// Portal-host replacement/fullscreen churn relies on forceRefresh to kick
|
||||
// WebKit even when synchronizeWebView short-circuits or skips its refresh path.
|
||||
refreshHostedWebViewPresentation(
|
||||
webView,
|
||||
in: containerView,
|
||||
reason: reason
|
||||
reason: refreshSource
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -2999,6 +3061,7 @@ final class WindowBrowserPortal: NSObject {
|
|||
}
|
||||
let previousTransientRecoveryReason = entry.transientRecoveryReason
|
||||
func hideContainerView(reason: String) {
|
||||
cancelPendingHostedWebViewRefreshes(for: webViewId)
|
||||
containerView.setPaneTopChromeHeight(0)
|
||||
containerView.setSearchOverlay(nil)
|
||||
containerView.setPaneDropContext(nil)
|
||||
|
|
@ -3434,13 +3497,17 @@ final class WindowBrowserPortal: NSObject {
|
|||
let hostedInspectorAdjustedDuringSync =
|
||||
containerOwnsWebView &&
|
||||
hostView.reapplyHostedInspectorDividerIfNeeded(in: containerView, reason: "portal.sync")
|
||||
let requiresRenderingStateReattach = webView.browserPortalRequiresRenderingStateReattach
|
||||
let presentationUpdateKind = HostedWebViewPresentationUpdateKind.resolve(
|
||||
reasons: refreshReasons
|
||||
)
|
||||
let shouldReapplyHostedInspectorPostRefresh =
|
||||
presentationUpdateKind == .refresh && requiresRenderingStateReattach
|
||||
if !shouldHide, containerOwnsWebView, presentationUpdateKind != .none {
|
||||
if presentationUpdateKind == .refresh &&
|
||||
hostedInspectorAdjustedDuringSync &&
|
||||
!recoveredFromTransientGeometry {
|
||||
!recoveredFromTransientGeometry &&
|
||||
!requiresRenderingStateReattach {
|
||||
#if DEBUG
|
||||
dlog(
|
||||
"browser.portal.refresh.skip web=\(browserPortalDebugToken(webView)) " +
|
||||
|
|
@ -3468,9 +3535,12 @@ final class WindowBrowserPortal: NSObject {
|
|||
}
|
||||
}
|
||||
}
|
||||
if containerOwnsWebView, !hostedInspectorAdjustedDuringSync {
|
||||
if containerOwnsWebView,
|
||||
(!hostedInspectorAdjustedDuringSync || shouldReapplyHostedInspectorPostRefresh) {
|
||||
// Keep the existing post-sync pass for cases where the inspector candidate
|
||||
// appears only after WebKit settles, but avoid a second apply when sync already clamped it.
|
||||
// appears only after WebKit settles. Re-run it after rendering-state reattach
|
||||
// refreshes as well, because WebKit's enter/unhide relayout can overwrite the
|
||||
// preferred divider position we already clamped during portal.sync.
|
||||
_ = hostView.reapplyHostedInspectorDividerIfNeeded(in: containerView, reason: "portal.sync.postRefresh")
|
||||
}
|
||||
#if DEBUG
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue