From e519a0d1833d354d71270f504e3b9790e828a941 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Wed, 21 Oct 2026 18:15:07 -0700 Subject: [PATCH 1/5] test(next browser): a hidden page too busy to answer the probe freezes (red) Co-Authored-By: Claude Opus 5.5 Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15d2c-7e44-7fc1-abd1-eae4a427ac80&host=Air-Blue --- .../UnansweredPageProbeTests.swift | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/UnansweredPageProbeTests.swift diff --git a/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/UnansweredPageProbeTests.swift b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/UnansweredPageProbeTests.swift new file mode 100644 index 000000000000..a5de83f240e1 --- /dev/null +++ b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/UnansweredPageProbeTests.swift @@ -0,0 +1,64 @@ +import AppKit +@testable import CmuxNextApp +@testable import CmuxNextBrowser +@testable import CmuxNextSettings +import Foundation +import Testing + +/// A hidden page that does not answer the hibernation probe (a script error, or no reply within +/// its 2 s deadline) used to count as playing media, so a page too busy to answer never froze and +/// was asked again every 30 s for as long as it stayed hidden. The 2026-10-21 audit of Leo's +/// running app found its hottest Chromium renderer at 25 % CPU and a 586 MB footprint. A page +/// that does not answer now counts as unknown; the media the tab already reported still keeps it. +@MainActor @Suite(.serialized, .timeLimit(.minutes(2))) struct UnansweredPageProbeTests { + private func tab() -> MockBrowserTab { + MockBrowserEngine(kind: .cef).makeMockTab(BrowserTabConfiguration(initialURL: URL(string: "https://example.com"))) + } + + @Test func aPageThatDoesNotAnswerIsNotPlayingMedia() async { + let page = tab() + page.evaluationResult = .null + let probe = await PageProbe.run(page) + #expect(!probe.playingMedia) + #expect(probe.exemption(pressure: .normal) != .audio) + } + + /// The tab's own media report (the page script's push, no round trip) still keeps it awake. + @Test func aPageThatDoesNotAnswerButReportedAudibleMediaStaysAwake() async { + let page = tab() + page.evaluationResult = .null + page.simulate(.mediaChanged(BrowserMediaState(title: "song", isPlaying: true))) + let probe = await PageProbe.run(page) + #expect(probe.playingMedia) + #expect(probe.exemption(pressure: .critical) == .audio) + } + + /// A real hidden WebKit page whose script keeps its main thread busy: every probe misses its + /// deadline. It used to stay awake for as long as it was hidden; now it freezes. + @Test func aHiddenPageTooBusyToAnswerFreezes() async throws { + let rig = try NeighborTabsWakeTests.Rig() + defer { rig.close() } + let key = try await busyHiddenPage(rig) + try await rig.until("the busy page freezes") { rig.hibernation.frozen.contains(key) } + } + + /// A loaded page that spins its main thread from 50 ms on, installed as a tab, shown, then hidden. + private func busyHiddenPage(_ rig: NeighborTabsWakeTests.Rig) async throws -> String { + let url = rig.folder.appendingPathComponent("busy.html") + try "busy

busy

".write(to: url, atomically: true, encoding: .utf8) + let page = rig.services.cache.webKit.makeWebKitTab() + page.load(url) + try await rig.until("busy loads") { + (try? await page.evaluate("document.readyState === 'complete' && document.title", world: .page)) == .string("busy") + } + _ = try await page.evaluate("setTimeout(() => { const end = Date.now() + 15000; while (Date.now() < end) {} }, 50); true", world: .page) + let key = page.id.rawValue + rig.recorders[key] = NeighborTabsWakeTests.Recorder() + rig.keys.append(key) + rig.services.cache.install(page, for: key) + rig.services.cache.present(key, by: rig.pane, presence: .visible) + rig.services.cache.withdraw(key, by: rig.pane) + try #require(rig.services.cache.phase(of: key) == .mountedHidden) + return key + } +} From 2a85c0ed4829a3c76c6e2755372d93a1613ad444 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Wed, 21 Oct 2026 18:16:40 -0700 Subject: [PATCH 2/5] fix(next browser): a hidden page too busy to answer the probe freezes A probe the page did not answer (a script error, or no reply within its 2 s deadline) counted as playing media, so the page never froze and was asked again every 30 s while hidden. It now counts as unknown: after 3 in a row the page freezes; it hibernates only under memory pressure, since its form edits are unknown. The media the tab last reported still keeps it awake. Co-Authored-By: Claude Opus 5.5 Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15d2c-7e44-7fc1-abd1-eae4a427ac80&host=Air-Blue --- .../Hibernation/BrowserHibernation.swift | 65 ++++++++++++++++--- .../Hibernation/HibernationPlanner.swift | 3 + .../UnansweredPageProbeTests.swift | 1 + 3 files changed, 60 insertions(+), 9 deletions(-) diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Hibernation/BrowserHibernation.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Hibernation/BrowserHibernation.swift index 1c34e1034341..78cf4c8fb1dc 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Hibernation/BrowserHibernation.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Hibernation/BrowserHibernation.swift @@ -59,6 +59,13 @@ final class BrowserHibernation { private var freezeProbing: Set = [] /// When a page the probe kept awake (playing audio) is asked again. private var freezeKeptAt: [String: ContinuousClock.Instant] = [:] + /// Probes in a row each hidden page did not answer (`PageProbe.answered`). + private var unanswered: [String: Int] = [:] + /// Probes in a row a page may leave unanswered before it counts as idle. One miss can be a + /// load or a pause of a few seconds; three in a row (the freeze probes are `freezeSeconds` + /// apart) mean a page busy the whole time, the one that costs the most to keep awake. A + /// freeze is undone on show. + static let unansweredLimit = 3 private let freezeDeadline = DemandTimer(owner: "BrowserFreeze") private var pressureSource: (any DispatchSourceMemoryPressure)? /// Pinned state of a tab (the daemon record); injected by the App. @@ -164,6 +171,7 @@ final class BrowserHibernation { if frozen.contains(key) { coldShows += 1 } thaw(key) freezeKeptAt[key] = nil + unanswered[key] = nil hoveredAt[key] = nil hiddenAt[key] = nil prepared[key] = nil @@ -195,6 +203,7 @@ final class BrowserHibernation { frozen.remove(key) freezeProbing.remove(key) freezeKeptAt[key] = nil + unanswered[key] = nil hoveredAt[key] = nil awakeNeighbors.remove(key) snapshots.remove(key) @@ -349,8 +358,9 @@ final class BrowserHibernation { } /// Asks the page for playing media, Web Audio and capture, then freezes it - /// if it is still the same hidden page. A page that plays stays awake and - /// is asked again after `freezeSeconds`. + /// if it is still the same hidden page. A page that plays, or that did not + /// answer fewer than `unansweredLimit` times in a row, stays awake and is + /// asked again after `freezeSeconds`. private func freeze(_ key: String) { guard let cache, let tab = cache.browsers[key]?.tab, let page = freezer(tab), let generation = cache.lifecycle.record(key)?.generation, freezeProbing.insert(key).inserted else { return } @@ -363,17 +373,34 @@ final class BrowserHibernation { if let used = self.agentUsedAt[key], (ContinuousClock.now - used).seconds < Self.agentGrace { return } // It became a neighbor (or was hovered) while the probe ran. if self.awakeNeighbors.contains(key) || self.hoverLeft(key, now: .now) != nil { return } - if probe.capturing || probe.playingMedia || probe.webAudio { + let doubted = self.doubts(key, probe) + if probe.capturing || probe.playingMedia || probe.webAudio || doubted { self.freezeKeptAt[key] = .now self.replan() return } - page.setFrozen(true) - self.frozen.insert(key) - cache.trace(key, "frozen") + self.freeze(key, page) } } + private func freeze(_ key: String, _ page: any BrowserFreezing) { + page.setFrozen(true) + frozen.insert(key) + cache?.trace(key, "frozen") + } + + /// Counts a probe `key` did not answer, and clears the count on an answer. True while the + /// page still gets the benefit of the doubt (fewer than `unansweredLimit` in a row). + private func doubts(_ key: String, _ probe: PageProbe) -> Bool { + guard !probe.answered else { + unanswered[key] = nil + return false + } + let count = (unanswered[key] ?? 0) + 1 + unanswered[key] = count + return count < Self.unansweredLimit + } + /// Thaws a frozen page (shown, driven by an agent, about to hibernate, or /// woken early as a neighbor or hovered tab: `prewarm`). private func thaw(_ key: String, prewarm: Bool = false) { @@ -421,7 +448,11 @@ final class BrowserHibernation { thaw(key) Task { @MainActor [weak self] in let probe = user ? PageProbe() : await PageProbe.run(tab) - let exemption = probe.exemption(pressure: self?.pressure ?? .normal) ?? (source.showsFormSubmission ? .formSubmission : nil) + let pressure = self?.pressure ?? .normal + // A page that keeps not answering hibernates only under memory pressure: its form edits are unknown. + let doubted = self?.doubts(key, probe) ?? true + let exemption = probe.exemption(pressure: pressure) ?? (source.showsFormSubmission ? .formSubmission : nil) + ?? (!probe.answered && (doubted || pressure == .normal) ? .unanswered : nil) // A page that stays needs no picture (each replan probes it again). let snapshot = exemption == nil ? await self?.snapshot(tab, key: key) : nil guard let self, let cache = self.cache else { return } @@ -434,6 +465,8 @@ final class BrowserHibernation { } if let exemption { self.exemptions[key] = exemption + // The probe thawed it; a page past its doubt freezes again at once (no replan would). + if exemption == .unanswered, !doubted, let page = self.freezer(tab), !self.frozen.contains(key) { self.freeze(key, page) } return } // It became a neighbor or was hovered while the probe ran (the person's Hibernate Tab still applies). @@ -497,6 +530,9 @@ struct PageProbe { /// A running Web Audio context. It may be silent (a chat app's notification sounds, a /// game), unlike a playing element, so critical memory pressure overrides it. var webAudio = false + /// False when the page did not answer (a script error, or no reply within the deadline): + /// only what the tab reported on its own (`BrowserTabState.media`) is known. + var answered = true func exemption(pressure: MemoryPressureLevel) -> HibernationPlanner.Exemption? { if capturing { return .capturing } @@ -511,7 +547,7 @@ struct PageProbe { (try await tab.evaluate(BrowserPageBusy.fieldProbeScript, world: .isolated), try await tab.pageBusy()) } guard case .string(let json) = value, let data = json.data(using: .utf8), - let object = try? JSONSerialization.jsonObject(with: data) as? [String: Bool] else { return PageProbe(playingMedia: true) } + let object = try? JSONSerialization.jsonObject(with: data) as? [String: Bool] else { return unanswered(tab) } // Media the tab's mute silences still plays (cx-d0d.24): hibernating would drop the mute. let muted = tab.state.isAudioMuted && tab.state.media?.isPlaying == true // A video in Picture in Picture plays on in its floating window, paused or muted alike. @@ -520,9 +556,20 @@ struct PageProbe { return PageProbe(playingMedia: muted || floating || (object["media"] ?? true), editedForm: busy.editedForm ?? object["form"] ?? true, capturing: busy.capturing, webAudio: busy.audio) } catch { - return PageProbe(playingMedia: true) + return unanswered(tab) } } + + /// A page that did not answer. It used to count as playing, so a page too busy to answer + /// never froze and was asked again every `freezeSeconds` (the hottest renderer in the + /// 2026-10-21 audit: 25 % CPU, 586 MB). The media the tab last reported still keeps it: the + /// page's media script pushes that state (`BrowserMediaState+Scripts`), with no round trip. + /// WebKit's own camera and microphone state is the planner's `isCapturing`; Chromium reports none to the app. + static func unanswered(_ tab: any BrowserTab) -> PageProbe { + let media = tab.state.media + let playing = media.map { $0.isAudible || ($0.isPlaying && tab.state.isAudioMuted) || $0.isPictureInPicture } ?? false + return PageProbe(playingMedia: playing, answered: false) + } } private extension Duration { diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Hibernation/HibernationPlanner.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Hibernation/HibernationPlanner.swift index 5328abd5b0cc..5222536adb72 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Hibernation/HibernationPlanner.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Hibernation/HibernationPlanner.swift @@ -36,6 +36,9 @@ struct HibernationPlanner { case agentDriving = "agent_driving" /// Beside a pane's shown tab, or hovered (`browser.keepNeighborTabsAwake`). case neighbor + /// The page did not answer the probe: kept for `BrowserHibernation.unansweredLimit` + /// probes in a row, then until memory pressure (its form edits are unknown). + case unanswered } struct Plan: Equatable { diff --git a/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/UnansweredPageProbeTests.swift b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/UnansweredPageProbeTests.swift index a5de83f240e1..9c614140f716 100644 --- a/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/UnansweredPageProbeTests.swift +++ b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/UnansweredPageProbeTests.swift @@ -21,6 +21,7 @@ import Testing let probe = await PageProbe.run(page) #expect(!probe.playingMedia) #expect(probe.exemption(pressure: .normal) != .audio) + #expect(!probe.answered) } /// The tab's own media report (the page script's push, no round trip) still keeps it awake. From 2c50bfe2603f4df1bf05da4a6b75e691be0bedf4 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Wed, 21 Oct 2026 18:18:45 -0700 Subject: [PATCH 3/5] fix(next browser): a page refrozen after an unanswered probe keeps the freeze guards Freezing off, an agent's grace, neighbors and hovered tabs still keep it awake. Co-Authored-By: Claude Opus 5.5 Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15d2c-7e44-7fc1-abd1-eae4a427ac80&host=Air-Blue --- .../Hibernation/BrowserHibernation.swift | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Hibernation/BrowserHibernation.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Hibernation/BrowserHibernation.swift index 78cf4c8fb1dc..5d71febc2c12 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Hibernation/BrowserHibernation.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Hibernation/BrowserHibernation.swift @@ -370,9 +370,8 @@ final class BrowserHibernation { self.freezeProbing.remove(key) guard cache.browsers[key]?.tab === tab, cache.lifecycle.record(key)?.generation == generation, cache.lifecycle.phase(key) == .mountedHidden, self.hiddenAt[key] != nil else { return } - if let used = self.agentUsedAt[key], (ContinuousClock.now - used).seconds < Self.agentGrace { return } - // It became a neighbor (or was hovered) while the probe ran. - if self.awakeNeighbors.contains(key) || self.hoverLeft(key, now: .now) != nil { return } + // An agent called it, or it became a neighbor (or was hovered) while the probe ran. + guard self.mayFreeze(key) else { return } let doubted = self.doubts(key, probe) if probe.capturing || probe.playingMedia || probe.webAudio || doubted { self.freezeKeptAt[key] = .now @@ -383,6 +382,14 @@ final class BrowserHibernation { } } + /// Freezing is on, no agent called the hidden page within `agentGrace`, and it is no + /// neighbor or hovered tab. + private func mayFreeze(_ key: String) -> Bool { + guard setting.freezeSeconds != nil else { return false } + if let used = agentUsedAt[key], (ContinuousClock.now - used).seconds < Self.agentGrace { return false } + return !frozen.contains(key) && !awakeNeighbors.contains(key) && hoverLeft(key, now: .now) == nil + } + private func freeze(_ key: String, _ page: any BrowserFreezing) { page.setFrozen(true) frozen.insert(key) @@ -466,7 +473,7 @@ final class BrowserHibernation { if let exemption { self.exemptions[key] = exemption // The probe thawed it; a page past its doubt freezes again at once (no replan would). - if exemption == .unanswered, !doubted, let page = self.freezer(tab), !self.frozen.contains(key) { self.freeze(key, page) } + if exemption == .unanswered, !doubted, self.mayFreeze(key), let page = self.freezer(tab) { self.freeze(key, page) } return } // It became a neighbor or was hovered while the probe ran (the person's Hibernate Tab still applies). From 3a5a4d9ca06a1dcb9458b82607c7a9b10011e09c Mon Sep 17 00:00:00 2001 From: Leo Li Date: Wed, 21 Oct 2026 18:23:07 -0700 Subject: [PATCH 4/5] fix(next browser): unanswered probes count once per page, never after a show, and not past a reported call A freeze probe no longer starts beside the hibernation probe of the same page; a probe that ends after the page was shown counts nothing; a page whose last answer reported capture or Web Audio stays awake while later probes go unanswered (Chromium reports neither natively). Co-Authored-By: Claude Opus 5.5 Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15d2c-7e44-7fc1-abd1-eae4a427ac80&host=Air-Blue --- .../Hibernation/BrowserHibernation.swift | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Hibernation/BrowserHibernation.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Hibernation/BrowserHibernation.swift index 5d71febc2c12..0c7f4222a32f 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Hibernation/BrowserHibernation.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Hibernation/BrowserHibernation.swift @@ -61,6 +61,9 @@ final class BrowserHibernation { private var freezeKeptAt: [String: ContinuousClock.Instant] = [:] /// Probes in a row each hidden page did not answer (`PageProbe.answered`). private var unanswered: [String: Int] = [:] + /// Pages whose last answer reported capture or Web Audio, which only the page's recorder + /// knows on Chromium: they stay awake while later probes go unanswered. + private var answeredBusy: Set = [] /// Probes in a row a page may leave unanswered before it counts as idle. One miss can be a /// load or a pause of a few seconds; three in a row (the freeze probes are `freezeSeconds` /// apart) mean a page busy the whole time, the one that costs the most to keep awake. A @@ -204,6 +207,7 @@ final class BrowserHibernation { freezeProbing.remove(key) freezeKeptAt[key] = nil unanswered[key] = nil + answeredBusy.remove(key) hoveredAt[key] = nil awakeNeighbors.remove(key) snapshots.remove(key) @@ -339,7 +343,8 @@ final class BrowserHibernation { let threshold = setting.freezeSeconds var recheck: Double? let candidates = hidden.filter { candidate in - guard !frozen.contains(candidate.key), !freezeProbing.contains(candidate.key), + // `probing`: the hibernation probe just started asks the same question. + guard !frozen.contains(candidate.key), !freezeProbing.contains(candidate.key), !probing.contains(candidate.key), let tab = cache.browsers[candidate.key]?.tab, freezer(tab) != nil else { return false } if let kept = freezeKeptAt[candidate.key], let threshold { let left = threshold - (now - kept).seconds @@ -397,12 +402,16 @@ final class BrowserHibernation { } /// Counts a probe `key` did not answer, and clears the count on an answer. True while the - /// page still gets the benefit of the doubt (fewer than `unansweredLimit` in a row). + /// page still gets the benefit of the doubt: fewer than `unansweredLimit` in a row, or its + /// last answer reported capture or Web Audio. A probe that ends after a show counts nothing. private func doubts(_ key: String, _ probe: PageProbe) -> Bool { guard !probe.answered else { unanswered[key] = nil + if probe.capturing || probe.webAudio { answeredBusy.insert(key) } else { answeredBusy.remove(key) } return false } + guard hiddenAt[key] != nil else { return true } + if answeredBusy.contains(key) { return true } let count = (unanswered[key] ?? 0) + 1 unanswered[key] = count return count < Self.unansweredLimit From 61583873b19d2f2b38ec0611a58b5fd07fcfebef Mon Sep 17 00:00:00 2001 From: Leo Li Date: Wed, 21 Oct 2026 18:26:34 -0700 Subject: [PATCH 5/5] fix(next browser): only the freeze probe counts unanswered probes A page the hibernation probe thawed is refrozen by the freeze probe that runs beside it in the same replan, as before; the hibernation probe reads the count and records answers. Drops the refreeze in prepare. Co-Authored-By: Claude Opus 5.5 Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15d2c-7e44-7fc1-abd1-eae4a427ac80&host=Air-Blue --- .../Hibernation/BrowserHibernation.swift | 29 +++++++++---------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Hibernation/BrowserHibernation.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Hibernation/BrowserHibernation.swift index 0c7f4222a32f..bc50ae26004b 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Hibernation/BrowserHibernation.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Hibernation/BrowserHibernation.swift @@ -343,8 +343,7 @@ final class BrowserHibernation { let threshold = setting.freezeSeconds var recheck: Double? let candidates = hidden.filter { candidate in - // `probing`: the hibernation probe just started asks the same question. - guard !frozen.contains(candidate.key), !freezeProbing.contains(candidate.key), !probing.contains(candidate.key), + guard !frozen.contains(candidate.key), !freezeProbing.contains(candidate.key), let tab = cache.browsers[candidate.key]?.tab, freezer(tab) != nil else { return false } if let kept = freezeKeptAt[candidate.key], let threshold { let left = threshold - (now - kept).seconds @@ -387,12 +386,10 @@ final class BrowserHibernation { } } - /// Freezing is on, no agent called the hidden page within `agentGrace`, and it is no - /// neighbor or hovered tab. + /// No agent called the hidden page within `agentGrace`, and it is no neighbor or hovered tab. private func mayFreeze(_ key: String) -> Bool { - guard setting.freezeSeconds != nil else { return false } if let used = agentUsedAt[key], (ContinuousClock.now - used).seconds < Self.agentGrace { return false } - return !frozen.contains(key) && !awakeNeighbors.contains(key) && hoverLeft(key, now: .now) == nil + return !awakeNeighbors.contains(key) && hoverLeft(key, now: .now) == nil } private func freeze(_ key: String, _ page: any BrowserFreezing) { @@ -410,11 +407,14 @@ final class BrowserHibernation { if probe.capturing || probe.webAudio { answeredBusy.insert(key) } else { answeredBusy.remove(key) } return false } - guard hiddenAt[key] != nil else { return true } - if answeredBusy.contains(key) { return true } - let count = (unanswered[key] ?? 0) + 1 - unanswered[key] = count - return count < Self.unansweredLimit + guard hiddenAt[key] != nil, !answeredBusy.contains(key) else { return true } + unanswered[key, default: 0] += 1 + return doubted(key) + } + + /// The page still gets the benefit of the doubt, without counting a probe. + private func doubted(_ key: String) -> Bool { + answeredBusy.contains(key) || unanswered[key, default: 0] < Self.unansweredLimit } /// Thaws a frozen page (shown, driven by an agent, about to hibernate, or @@ -465,8 +465,9 @@ final class BrowserHibernation { Task { @MainActor [weak self] in let probe = user ? PageProbe() : await PageProbe.run(tab) let pressure = self?.pressure ?? .normal - // A page that keeps not answering hibernates only under memory pressure: its form edits are unknown. - let doubted = self?.doubts(key, probe) ?? true + // A page that keeps not answering hibernates only under memory pressure: its form edits are + // unknown. The freeze probe counts the misses (it runs beside this one and refreezes the page). + let doubted = probe.answered ? self?.doubts(key, probe) ?? false : self?.doubted(key) ?? true let exemption = probe.exemption(pressure: pressure) ?? (source.showsFormSubmission ? .formSubmission : nil) ?? (!probe.answered && (doubted || pressure == .normal) ? .unanswered : nil) // A page that stays needs no picture (each replan probes it again). @@ -481,8 +482,6 @@ final class BrowserHibernation { } if let exemption { self.exemptions[key] = exemption - // The probe thawed it; a page past its doubt freezes again at once (no replan would). - if exemption == .unanswered, !doubted, self.mayFreeze(key), let page = self.freezer(tab) { self.freeze(key, page) } return } // It became a neighbor or was hovered while the probe ran (the person's Hibernate Tab still applies).