Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/swift-file-length-budget.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# Format: max_lines<TAB>relative 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
Expand Down
39 changes: 38 additions & 1 deletion Sources/App/RendererRealizationController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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?

Expand Down Expand Up @@ -82,9 +83,25 @@ final class RendererRealizationController {
}
}

func reclaimForSystemMemoryPressure(now: Date) {
evaluate(
now: now,
trigger: .systemMemoryPressure,
remainingSystemMemoryPressureRetries: systemMemoryPressureRetryPasses
)
}

/// 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: RendererRealizationReclaimTrigger,
remainingSystemMemoryPressureRetries: Int = 0
) {
let settings = RendererRealizationSettings.values()
guard settings.enabled else { return }

Expand Down Expand Up @@ -120,11 +137,31 @@ final class RendererRealizationController {
let selected = RendererRealizationPlanner.selectedSurfaceIds(
inputs: inputs,
settings: settings,
now: now.timeIntervalSince1970
now: now.timeIntervalSince1970,
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
)
}
}
}
}
11 changes: 10 additions & 1 deletion Sources/App/RendererRealizationPlanner.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,19 @@ enum RendererRealizationPlanner {
static func selectedSurfaceIds(
inputs: [RendererRealizationPlannerInput],
settings: RendererRealizationSettings.Values,
now: TimeInterval
now: TimeInterval,
trigger: RendererRealizationReclaimTrigger = .scheduled
) -> Set<UUID> {
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.
Expand Down
7 changes: 7 additions & 0 deletions Sources/App/RendererRealizationReclaimTrigger.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import Foundation

/// Reason a renderer realization pass is selecting surfaces to reclaim.
enum RendererRealizationReclaimTrigger: Equatable, Sendable {
case scheduled
case systemMemoryPressure
}
1 change: 1 addition & 0 deletions Sources/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
4 changes: 4 additions & 0 deletions cmux.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -1993,6 +1994,7 @@
D36A00040000000000000002 /* RendererRealizationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App/RendererRealizationController.swift; sourceTree = "<group>"; };
D36A00060000000000000002 /* RendererRealizationPlanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App/RendererRealizationPlanner.swift; sourceTree = "<group>"; };
D36A00050000000000000002 /* RendererRealizationPlannerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RendererRealizationPlannerTests.swift; sourceTree = "<group>"; };
D36A00070000000000000002 /* RendererRealizationReclaimTrigger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App/RendererRealizationReclaimTrigger.swift; sourceTree = "<group>"; };
F5410005A1B2C3D4E5F60718 /* RestorableAgentHookProviderHermesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestorableAgentHookProviderHermesTests.swift; sourceTree = "<group>"; };
F5410001A1B2C3D4E5F60718 /* RestorableAgentHookProviderResumeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestorableAgentHookProviderResumeTests.swift; sourceTree = "<group>"; };
F5410003A1B2C3D4E5F60718 /* RestorableAgentNonInteractiveTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestorableAgentNonInteractiveTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand Down
37 changes: 37 additions & 0 deletions cmuxTests/RendererRealizationPlannerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Loading