Skip to content

Reduce GPU memory growth and UI sluggishness#39

Merged
trydis merged 2 commits into
mainfrom
fix/memory-growth-gpu-occlusion
Jun 22, 2026
Merged

Reduce GPU memory growth and UI sluggishness#39
trydis merged 2 commits into
mainfrom
fix/memory-growth-gpu-occlusion

Conversation

@trydis

@trydis trydis commented Jun 22, 2026

Copy link
Copy Markdown
Owner

Occlude surfaces in non-selected workspaces so libghostty stops their DisplayLink and redraw loops. All split panes in the active workspace remain live regardless of focus; only workspace switches toggle occlusion. Occlusion is applied reactively via SwiftUI re-renders on selectedWorkspaceId change — no manual recompute needed.

Also fixes several secondary contributors:

  • refreshGitBranch: cancel-and-replace per surface to prevent git subprocess pile-up on rapid tab cycling
  • AgentCompletionEventMonitor: batch all log-line events into one MainActor task per file read instead of one strong-self task per line
  • CoalescingWorkspacePersistence.save(): async instead of sync so the main thread is never blocked scheduling debounced workspace saves
  • WorkspacePersistence: reuse JSONEncoder instance
  • deleteWorkspace: call clearProgressReport for released surfaces
  • Codex monitor poll loop: cap at 300 iterations (150 s) to prevent orphaned monitors after abrupt close

Summary by CodeRabbit

  • Bug Fixes

    • Added iteration bounds to prevent infinite polling loops in agent runtime.
    • Improved progress report cleanup when deleting workspaces.
  • Performance Improvements

    • Optimized surface rendering by controlling occlusion based on workspace selection, halting render loops for inactive workspaces.
    • Refactored event processing for better task efficiency.
    • Enhanced workspace persistence with async debouncing.
  • Improvements

    • Better Git branch tracking with per-surface task management.

Occlude surfaces in non-selected workspaces so libghostty stops their
DisplayLink and redraw loops. All split panes in the active workspace
remain live regardless of focus; only workspace switches toggle occlusion.
Occlusion is applied reactively via SwiftUI re-renders on selectedWorkspaceId
change — no manual recompute needed.

Also fixes several secondary contributors:
- refreshGitBranch: cancel-and-replace per surface to prevent git
  subprocess pile-up on rapid tab cycling
- AgentCompletionEventMonitor: batch all log-line events into one
  MainActor task per file read instead of one strong-self task per line
- CoalescingWorkspacePersistence.save(): async instead of sync so the
  main thread is never blocked scheduling debounced workspace saves
- WorkspacePersistence: reuse JSONEncoder instance
- deleteWorkspace: call clearProgressReport for released surfaces
- Codex monitor poll loop: cap at 300 iterations (150 s) to prevent
  orphaned monitors after abrupt close
@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@trydis, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 42 minutes and 31 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 713b174f-8954-4bd7-88a7-4c283a3888e0

📥 Commits

Reviewing files that changed from the base of the PR and between 96ab525 and 0f68780.

📒 Files selected for processing (4)
  • Sources/Shellraiser/Features/Terminal/GhosttyTerminalView.swift
  • Sources/Shellraiser/Infrastructure/Ghostty/GhosttyRuntime.swift
  • Sources/Shellraiser/Services/Workspaces/WorkspaceManager+GitBranches.swift
  • Tests/ShellraiserTests/GhosttyTerminalViewTests.swift
📝 Walkthrough

Walkthrough

This PR adds workspace-selection-driven Ghostty surface occlusion to stop render loops on non-visible workspaces, consolidates agent event dispatch into a single weak-capturing MainActor task, bounds the Codex wrapper polling loop at 300 iterations, adds git-branch task cancellation for overlapping refreshes, switches workspace persistence saves to async dispatch, and clears progress reports on workspace deletion.

Changes

Ghostty Surface Occlusion for Workspace Selection

Layer / File(s) Summary
Occlusion protocol and runtime implementation
Sources/Shellraiser/Features/Terminal/GhosttyTerminalView.swift, Sources/Shellraiser/Infrastructure/Ghostty/GhosttyRuntime.swift
GhosttyTerminalHostView protocol gains setSurfaceOcclusion(surfaceId:occluded:); GhosttyRuntime implements it by guarding against missing handles and forwarding the inverted flag to ghostty_surface_set_occlusion.
GhosttyTerminalView occlusion logic
Sources/Shellraiser/Features/Terminal/GhosttyTerminalView.swift
isWorkspaceSelected property added; makeNSView/updateNSView pass it to syncContainerView; syncContainerView occludes the previous surface on remount and applies !isWorkspaceSelected on every sync; dismantleContainerView occludes before detaching.
PaneLeafView wiring
Sources/Shellraiser/Features/WorkspaceDetail/PaneLeafView.swift
Passes isWorkspaceSelected to GhosttyTerminalView by comparing manager.window.selectedWorkspaceId to the pane's workspaceId.
Occlusion tests
Tests/ShellraiserTests/GhosttyTerminalViewTests.swift
Existing sync/dismantle tests updated with isWorkspaceSelected arguments and occlusion assertions; new test validates occlusion toggling without remount; mock gains setSurfaceOcclusionCalls tracking.

Agent Infrastructure Fixes

Layer / File(s) Summary
Codex wrapper iteration-bounded loop
Sources/Shellraiser/Infrastructure/Agents/AgentRuntimeBridge.swift, Tests/ShellraiserTests/AgentRuntimeBridgeTests.swift
Replaces unbounded while :; do with while [ "$iteration" -lt 300 ]; test updated to assert the bounded loop structure and absence of the infinite loop.
AgentCompletionEventMonitor single-task dispatch
Sources/Shellraiser/Infrastructure/Agents/AgentCompletionEventMonitor.swift
consumePendingEvents collects parsed events via compactMap then schedules one @MainActor task with weak self capture that iterates all events, replacing the previous per-event strong-capture task pattern.

Workspace Service Hardening

Layer / File(s) Summary
Git branch task cancellation and overlap control
Sources/Shellraiser/Services/Workspaces/WorkspaceManager.swift, Sources/Shellraiser/Services/Workspaces/WorkspaceManager+GitBranches.swift
WorkspaceManager adds gitBranchTasks dictionary; refreshGitBranch cancels existing tasks before spawning a weak-capturing detached replacement; clearGitBranch cancels and removes the tracked task.
WorkspacePersistence async save and encoder reuse
Sources/Shellraiser/Services/Persistence/WorkspacePersistence.swift
WorkspacePersistence moves JSONEncoder to a stored property; CoalescingWorkspacePersistence.save switches from coordinationQueue.sync to async, cancels prior work items, and schedules persistence based on debounceInterval.
Workspace deletion progress-report cleanup
Sources/Shellraiser/Services/Workspaces/WorkspaceManager+WorkspaceLifecycle.swift
deleteWorkspace now calls clearProgressReport(surfaceId:) for each released surface during the cleanup loop.

Possibly related PRs

  • trydis/shellraiser#11: Modifies GhosttyTerminalView's syncContainerView/dismantleContainerView around mounted surface attachment — directly overlaps with this PR's occlusion additions to the same code paths.
  • trydis/shellraiser#21: Modifies the Codex wrapper monitoring loop in AgentRuntimeBridge.swift, the same shell script section this PR replaces with an iteration-bounded loop.
  • trydis/shellraiser#33: Changes debounce/coalescing persistence logic in CoalescingWorkspacePersistence.save(_:), the same method this PR changes to use async dispatch.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly reflects the main objective: addressing GPU memory growth and UI sluggishness through surface occlusion for non-selected workspaces, which is the primary change across multiple files.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/memory-growth-gpu-occlusion

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Sources/Shellraiser/Features/Terminal/GhosttyTerminalView.swift`:
- Around line 199-203: The code unconditionally sets occluded: true when calling
runtime.setSurfaceOcclusion() during the detach operations at both locations
(around the runtime.detachHost call and at the second location mentioned). In a
reparent scenario where a surface can be temporarily mounted in multiple
containers, this premature occlusion stalls redraws. Instead of always setting
occluded: true when detaching, first check if the surface (identified by
mountedSurfaceId) has any other active mounts remaining in the runtime, and only
set occluded: true when removing the last mount for that surface. Apply this
conditional logic to both detach flow locations in the code.

In `@Sources/Shellraiser/Services/Workspaces/WorkspaceManager`+GitBranches.swift:
- Around line 43-53: The code checks Task.isCancelled before the MainActor.run
block, but the task can be cancelled between this check and the actual write to
gitStatesBySurfaceId on line 52. Add a second cancellation guard at the
beginning of the MainActor.run closure (after the guard let self check and
before the workspace/surface validation checks) to re-check if Task.isCancelled
and return early if true. This prevents stale git state from being written when
the task is cancelled during the actor hop.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 14f474ca-2d5e-4fc4-98cf-765f64b9adc0

📥 Commits

Reviewing files that changed from the base of the PR and between cebaa43 and 96ab525.

📒 Files selected for processing (11)
  • Sources/Shellraiser/Features/Terminal/GhosttyTerminalView.swift
  • Sources/Shellraiser/Features/WorkspaceDetail/PaneLeafView.swift
  • Sources/Shellraiser/Infrastructure/Agents/AgentCompletionEventMonitor.swift
  • Sources/Shellraiser/Infrastructure/Agents/AgentRuntimeBridge.swift
  • Sources/Shellraiser/Infrastructure/Ghostty/GhosttyRuntime.swift
  • Sources/Shellraiser/Services/Persistence/WorkspacePersistence.swift
  • Sources/Shellraiser/Services/Workspaces/WorkspaceManager+GitBranches.swift
  • Sources/Shellraiser/Services/Workspaces/WorkspaceManager+WorkspaceLifecycle.swift
  • Sources/Shellraiser/Services/Workspaces/WorkspaceManager.swift
  • Tests/ShellraiserTests/AgentRuntimeBridgeTests.swift
  • Tests/ShellraiserTests/GhosttyTerminalViewTests.swift

Comment thread Sources/Shellraiser/Features/Terminal/GhosttyTerminalView.swift
Guard setSurfaceOcclusion(occluded:true) in both detach sites behind a
mountedHostCount check so a reparent-in-progress surface is not stalled
while a second container still shows it. Reorder dismantleContainerView
to decrement first, then check.

Add a second Task.isCancelled guard inside the MainActor.run closure in
refreshGitBranch to prevent stale git state from landing after a
replacement task cancels the in-flight one during the actor hop.
@trydis trydis merged commit c42e979 into main Jun 22, 2026
2 checks passed
@trydis trydis deleted the fix/memory-growth-gpu-occlusion branch June 22, 2026 21:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant