From 425cd996f8d9c582fa39a338d27ae36deda030df Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Mon, 29 Jun 2026 11:50:18 -0700 Subject: [PATCH 1/5] Add renderer memory-pressure reclaim regression test --- Sources/App/RendererRealizationPlanner.swift | 10 ++++- .../RendererRealizationPlannerTests.swift | 37 +++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/Sources/App/RendererRealizationPlanner.swift b/Sources/App/RendererRealizationPlanner.swift index 00c21dc08b5c..675dc5257567 100644 --- a/Sources/App/RendererRealizationPlanner.swift +++ b/Sources/App/RendererRealizationPlanner.swift @@ -14,10 +14,16 @@ struct RendererRealizationPlannerInput: Sendable { /// the rest only when they are offscreen and have been idle past `idleSeconds`. /// A currently-visible surface is never selected. enum RendererRealizationPlanner { + enum ReclaimTrigger: Sendable { + case scheduled + case systemMemoryPressure + } + static func selectedSurfaceIds( inputs: [RendererRealizationPlannerInput], settings: RendererRealizationSettings.Values, - now: TimeInterval + now: TimeInterval, + trigger: ReclaimTrigger = .scheduled ) -> Set { guard settings.enabled else { return [] } @@ -33,6 +39,8 @@ enum RendererRealizationPlanner { return lhs.lastVisibleAt > rhs.lastVisibleAt } + _ = trigger + let warmCap = max(1, settings.maxWarmRenderers) var selected: Set = [] for (index, input) in ranked.enumerated() { diff --git a/cmuxTests/RendererRealizationPlannerTests.swift b/cmuxTests/RendererRealizationPlannerTests.swift index 677ac170963f..95f9b5072526 100644 --- a/cmuxTests/RendererRealizationPlannerTests.swift +++ b/cmuxTests/RendererRealizationPlannerTests.swift @@ -137,4 +137,41 @@ struct RendererRealizationPlannerTests { #expect(selected.contains(b)) #expect(!selected.contains(a)) } + + @Test func systemMemoryPressureReclaimsAllHiddenRealizedRenderers() { + let now: TimeInterval = 1000 + let visible = UUID() + let recentHidden = UUID() + let oldHidden = UUID() + let alreadyReleased = UUID() + let selected = RendererRealizationPlanner.selectedSurfaceIds( + inputs: [ + input(visible, visible: true, lastVisibleAt: now), + input(recentHidden, lastVisibleAt: now - 1), + input(oldHidden, lastVisibleAt: now - 100), + input(alreadyReleased, realized: false, lastVisibleAt: now - 100), + ], + settings: settings(idle: 600, warm: 12), + now: now, + trigger: .systemMemoryPressure + ) + + #expect(!selected.contains(visible)) + #expect(selected.contains(recentHidden)) + #expect(selected.contains(oldHidden)) + #expect(!selected.contains(alreadyReleased)) + } + + @Test func systemMemoryPressureRespectsDisabledSetting() { + let now: TimeInterval = 1000 + let hidden = UUID() + let selected = RendererRealizationPlanner.selectedSurfaceIds( + inputs: [input(hidden, lastVisibleAt: now - 1)], + settings: settings(enabled: false, idle: 600, warm: 12), + now: now, + trigger: .systemMemoryPressure + ) + + #expect(selected.isEmpty) + } } From b6ef209d6098960683391feeeb96da1e22a7bcb0 Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Mon, 29 Jun 2026 11:51:52 -0700 Subject: [PATCH 2/5] Reclaim hidden renderers under memory pressure --- Sources/App/RendererRealizationController.swift | 11 ++++++++++- Sources/App/RendererRealizationPlanner.swift | 6 ++++-- Sources/AppDelegate.swift | 1 + 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/Sources/App/RendererRealizationController.swift b/Sources/App/RendererRealizationController.swift index 540074359bf5..4e23f3fcefbd 100644 --- a/Sources/App/RendererRealizationController.swift +++ b/Sources/App/RendererRealizationController.swift @@ -82,9 +82,17 @@ final class RendererRealizationController { } } + func reclaimForSystemMemoryPressure(now: Date) { + evaluate(now: now, trigger: .systemMemoryPressure) + } + /// Run one reclamation pass. Internal so a unit/integration test can drive it /// deterministically without the timer. func evaluate(now: Date) { + evaluate(now: now, trigger: .scheduled) + } + + private func evaluate(now: Date, trigger: RendererRealizationPlanner.ReclaimTrigger) { let settings = RendererRealizationSettings.values() guard settings.enabled else { return } @@ -120,7 +128,8 @@ final class RendererRealizationController { let selected = RendererRealizationPlanner.selectedSurfaceIds( inputs: inputs, settings: settings, - now: now.timeIntervalSince1970 + now: now.timeIntervalSince1970, + trigger: trigger ) guard !selected.isEmpty else { return } for surface in surfaces where selected.contains(surface.id) { diff --git a/Sources/App/RendererRealizationPlanner.swift b/Sources/App/RendererRealizationPlanner.swift index 675dc5257567..fbfa1b40f0fa 100644 --- a/Sources/App/RendererRealizationPlanner.swift +++ b/Sources/App/RendererRealizationPlanner.swift @@ -14,7 +14,7 @@ struct RendererRealizationPlannerInput: Sendable { /// the rest only when they are offscreen and have been idle past `idleSeconds`. /// A currently-visible surface is never selected. enum RendererRealizationPlanner { - enum ReclaimTrigger: Sendable { + enum ReclaimTrigger: Equatable, Sendable { case scheduled case systemMemoryPressure } @@ -39,7 +39,9 @@ enum RendererRealizationPlanner { return lhs.lastVisibleAt > rhs.lastVisibleAt } - _ = trigger + if trigger == .systemMemoryPressure { + return Set(ranked.lazy.filter { !$0.isVisible }.map(\.surfaceId)) + } let warmCap = max(1, settings.maxWarmRenderers) var selected: Set = [] diff --git a/Sources/AppDelegate.swift b/Sources/AppDelegate.swift index b61fc060cefc..c158f2e3fdaf 100644 --- a/Sources/AppDelegate.swift +++ b/Sources/AppDelegate.swift @@ -2108,6 +2108,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent self?.paneMemoryGuardrailDescriptors() ?? [] } guardrail.onSystemMemoryPressure = { [weak self] in + RendererRealizationController.shared.reclaimForSystemMemoryPressure(now: Date()) self?.discardHiddenBrowserWebViewsForSystemMemoryPressure() } guardrail.start() From ba87270e7065ec10c54aaa33522ba9c54892b795 Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Mon, 29 Jun 2026 11:58:55 -0700 Subject: [PATCH 3/5] Satisfy renderer reclaim guardrails --- .github/swift-file-length-budget.tsv | 2 +- Sources/App/RendererRealizationController.swift | 2 +- Sources/App/RendererRealizationPlanner.swift | 7 +------ Sources/App/RendererRealizationReclaimTrigger.swift | 7 +++++++ cmux.xcodeproj/project.pbxproj | 4 ++++ 5 files changed, 14 insertions(+), 8 deletions(-) create mode 100644 Sources/App/RendererRealizationReclaimTrigger.swift diff --git a/.github/swift-file-length-budget.tsv b/.github/swift-file-length-budget.tsv index be104210e3b9..a3775930473d 100644 --- a/.github/swift-file-length-budget.tsv +++ b/.github/swift-file-length-budget.tsv @@ -2,7 +2,7 @@ # Format: max_linesrelative path # Reduce counts as files shrink. CI fails if tracked files exceed this budget. 34658 CLI/cmux.swift -17907 Sources/AppDelegate.swift +17908 Sources/AppDelegate.swift 16491 Sources/ContentView.swift 14239 Sources/TerminalController.swift 12921 Sources/Workspace.swift diff --git a/Sources/App/RendererRealizationController.swift b/Sources/App/RendererRealizationController.swift index 4e23f3fcefbd..110f816a21db 100644 --- a/Sources/App/RendererRealizationController.swift +++ b/Sources/App/RendererRealizationController.swift @@ -92,7 +92,7 @@ final class RendererRealizationController { evaluate(now: now, trigger: .scheduled) } - private func evaluate(now: Date, trigger: RendererRealizationPlanner.ReclaimTrigger) { + private func evaluate(now: Date, trigger: RendererRealizationReclaimTrigger) { let settings = RendererRealizationSettings.values() guard settings.enabled else { return } diff --git a/Sources/App/RendererRealizationPlanner.swift b/Sources/App/RendererRealizationPlanner.swift index fbfa1b40f0fa..8ba34317106d 100644 --- a/Sources/App/RendererRealizationPlanner.swift +++ b/Sources/App/RendererRealizationPlanner.swift @@ -14,16 +14,11 @@ struct RendererRealizationPlannerInput: Sendable { /// the rest only when they are offscreen and have been idle past `idleSeconds`. /// A currently-visible surface is never selected. enum RendererRealizationPlanner { - enum ReclaimTrigger: Equatable, Sendable { - case scheduled - case systemMemoryPressure - } - static func selectedSurfaceIds( inputs: [RendererRealizationPlannerInput], settings: RendererRealizationSettings.Values, now: TimeInterval, - trigger: ReclaimTrigger = .scheduled + trigger: RendererRealizationReclaimTrigger = .scheduled ) -> Set { guard settings.enabled else { return [] } diff --git a/Sources/App/RendererRealizationReclaimTrigger.swift b/Sources/App/RendererRealizationReclaimTrigger.swift new file mode 100644 index 000000000000..f4689d3c3aa5 --- /dev/null +++ b/Sources/App/RendererRealizationReclaimTrigger.swift @@ -0,0 +1,7 @@ +import Foundation + +/// Reason a renderer realization pass is selecting surfaces to reclaim. +enum RendererRealizationReclaimTrigger: Equatable, Sendable { + case scheduled + case systemMemoryPressure +} diff --git a/cmux.xcodeproj/project.pbxproj b/cmux.xcodeproj/project.pbxproj index eaa7bf5000c3..4faa09671967 100644 --- a/cmux.xcodeproj/project.pbxproj +++ b/cmux.xcodeproj/project.pbxproj @@ -801,6 +801,7 @@ D36A00040000000000000001 /* RendererRealizationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D36A00040000000000000002 /* RendererRealizationController.swift */; }; D36A00060000000000000001 /* RendererRealizationPlanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = D36A00060000000000000002 /* RendererRealizationPlanner.swift */; }; D36A00050000000000000001 /* RendererRealizationPlannerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D36A00050000000000000002 /* RendererRealizationPlannerTests.swift */; }; + D36A00070000000000000001 /* RendererRealizationReclaimTrigger.swift in Sources */ = {isa = PBXBuildFile; fileRef = D36A00070000000000000002 /* RendererRealizationReclaimTrigger.swift */; }; F5410004A1B2C3D4E5F60718 /* RestorableAgentHookProviderHermesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5410005A1B2C3D4E5F60718 /* RestorableAgentHookProviderHermesTests.swift */; }; F5410000A1B2C3D4E5F60718 /* RestorableAgentHookProviderResumeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5410001A1B2C3D4E5F60718 /* RestorableAgentHookProviderResumeTests.swift */; }; F5410002A1B2C3D4E5F60718 /* RestorableAgentNonInteractiveTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5410003A1B2C3D4E5F60718 /* RestorableAgentNonInteractiveTests.swift */; }; @@ -1993,6 +1994,7 @@ D36A00040000000000000002 /* RendererRealizationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App/RendererRealizationController.swift; sourceTree = ""; }; D36A00060000000000000002 /* RendererRealizationPlanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App/RendererRealizationPlanner.swift; sourceTree = ""; }; D36A00050000000000000002 /* RendererRealizationPlannerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RendererRealizationPlannerTests.swift; sourceTree = ""; }; + D36A00070000000000000002 /* RendererRealizationReclaimTrigger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App/RendererRealizationReclaimTrigger.swift; sourceTree = ""; }; F5410005A1B2C3D4E5F60718 /* RestorableAgentHookProviderHermesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestorableAgentHookProviderHermesTests.swift; sourceTree = ""; }; F5410001A1B2C3D4E5F60718 /* RestorableAgentHookProviderResumeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestorableAgentHookProviderResumeTests.swift; sourceTree = ""; }; F5410003A1B2C3D4E5F60718 /* RestorableAgentNonInteractiveTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestorableAgentNonInteractiveTests.swift; sourceTree = ""; }; @@ -2692,6 +2694,7 @@ D36A00010000000000000002 /* AgentHibernationController.swift */, D36A00040000000000000002 /* RendererRealizationController.swift */, D36A00060000000000000002 /* RendererRealizationPlanner.swift */, + D36A00070000000000000002 /* RendererRealizationReclaimTrigger.swift */, 243632BA1DBBA36FD46E0610 /* SidebarAppearanceSupport.swift */, D50D9BCE46701FBA91C2EFB6 /* SidebarWorkspaceSnapshotRefreshPolicy.swift */, D35450020000000000000002 /* SidebarWorkspaceRowHoverTracker.swift */, @@ -4563,6 +4566,7 @@ D5037010000000000000003 /* RenderableSystemSymbol.swift in Sources */, D36A00040000000000000001 /* RendererRealizationController.swift in Sources */, D36A00060000000000000001 /* RendererRealizationPlanner.swift in Sources */, + D36A00070000000000000001 /* RendererRealizationReclaimTrigger.swift in Sources */, A5001660 /* RestorableAgentSession.swift in Sources */, C13519000000000000000005 /* RestorableAgentTypes.swift in Sources */, B7F9A600B7F9A600B7F9A600 /* RightSidebarChromeGeometryReporting.swift in Sources */, From 265cd899d732796cbec0b56f3167f217ae5de766 Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Mon, 29 Jun 2026 12:00:12 -0700 Subject: [PATCH 4/5] Keep memory-pressure renderer reclaim linear --- Sources/App/RendererRealizationPlanner.swift | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/Sources/App/RendererRealizationPlanner.swift b/Sources/App/RendererRealizationPlanner.swift index 8ba34317106d..4878fa52e69b 100644 --- a/Sources/App/RendererRealizationPlanner.swift +++ b/Sources/App/RendererRealizationPlanner.swift @@ -22,6 +22,14 @@ enum RendererRealizationPlanner { ) -> Set { guard settings.enabled else { return [] } + if trigger == .systemMemoryPressure { + return Set( + inputs.lazy + .filter { $0.isRealized && !$0.isVisible } + .map(\.surfaceId) + ) + } + // Only realized surfaces hold releasable GPU resources. Rank by recency // (most-recent first); visible surfaces are stamped ~now so they sort to // the top and land inside the warm set. @@ -34,10 +42,6 @@ enum RendererRealizationPlanner { return lhs.lastVisibleAt > rhs.lastVisibleAt } - if trigger == .systemMemoryPressure { - return Set(ranked.lazy.filter { !$0.isVisible }.map(\.surfaceId)) - } - let warmCap = max(1, settings.maxWarmRenderers) var selected: Set = [] for (index, input) in ranked.enumerated() { From 73c0c1944c1e25216ce17df87bd5edcca2c78346 Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Mon, 29 Jun 2026 12:06:44 -0700 Subject: [PATCH 5/5] Retry pressure renderer reclaim on dropped enqueue --- .../App/RendererRealizationController.swift | 32 +++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/Sources/App/RendererRealizationController.swift b/Sources/App/RendererRealizationController.swift index 110f816a21db..73ec42b63d14 100644 --- a/Sources/App/RendererRealizationController.swift +++ b/Sources/App/RendererRealizationController.swift @@ -17,6 +17,7 @@ final class RendererRealizationController { static let shared = RendererRealizationController() private let timerQueue = DispatchQueue(label: "com.cmux.renderer-realization", qos: .utility) + private let systemMemoryPressureRetryPasses = 2 private var timer: DispatchSourceTimer? private var settingsObserver: NSObjectProtocol? @@ -83,7 +84,11 @@ final class RendererRealizationController { } func reclaimForSystemMemoryPressure(now: Date) { - evaluate(now: now, trigger: .systemMemoryPressure) + evaluate( + now: now, + trigger: .systemMemoryPressure, + remainingSystemMemoryPressureRetries: systemMemoryPressureRetryPasses + ) } /// Run one reclamation pass. Internal so a unit/integration test can drive it @@ -92,7 +97,11 @@ final class RendererRealizationController { evaluate(now: now, trigger: .scheduled) } - private func evaluate(now: Date, trigger: RendererRealizationReclaimTrigger) { + private func evaluate( + now: Date, + trigger: RendererRealizationReclaimTrigger, + remainingSystemMemoryPressureRetries: Int = 0 + ) { let settings = RendererRealizationSettings.values() guard settings.enabled else { return } @@ -132,8 +141,27 @@ final class RendererRealizationController { trigger: trigger ) guard !selected.isEmpty else { return } + var needsSystemMemoryPressureRetry = false for surface in surfaces where selected.contains(surface.id) { surface.releaseRenderer() + // A dropped Ghostty mailbox enqueue leaves the renderer realized. + // Retry with the pressure policy; scheduled policy may keep recent + // hidden surfaces warm and skip the exact surface pressure selected. + if trigger == .systemMemoryPressure, + !surface.isRendererPortalVisible, + surface.isRendererRealized { + needsSystemMemoryPressureRetry = true + } + } + + if needsSystemMemoryPressureRetry, remainingSystemMemoryPressureRetries > 0 { + Task { @MainActor in + RendererRealizationController.shared.evaluate( + now: Date(), + trigger: .systemMemoryPressure, + remainingSystemMemoryPressureRetries: remainingSystemMemoryPressureRetries - 1 + ) + } } } }