Skip to content

Avoid blocking iOS surface mailbox during stream handling - #87

Open
lawrencecchen wants to merge 2 commits into
mainfrom
task-ios-surface-mailbox-nonblocking
Open

Avoid blocking iOS surface mailbox during stream handling#87
lawrencecchen wants to merge 2 commits into
mainfrom
task-ios-surface-mailbox-nonblocking

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Jun 25, 2026

Copy link
Copy Markdown

Pins the Ghostty-side part of the cmux iOS render-stall fix.\n\nChanges:\n- On iOS, avoid falling back to a forever mailbox push when the surface mailbox is saturated.\n- Deinitialize owned payloads for dropped surface messages to avoid leaks.\n\nParent cmux PR: manaflow-ai/cmux#6543


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.


Summary by cubic

Avoids render stalls on iOS by not blocking when the surface mailbox is full; instead, we drop the message and deinit its owned payloads to prevent leaks. Aligns with the parent cmux fix: manaflow-ai/cmux#6543

  • Bug Fixes
    • Clean up dropped surface messages by deiniting clipboard_write.req, pwd_change, and tmux_control.data.

Written for commit 0b1cb1d. Summary will update on new commits.

Review in cubic

@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown

Warning

Review limit reached

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

More reviews will be available in 1 minute and 5 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 review availability.

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, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 86c86222-fb86-4ab7-a9f7-d91a3b4f66c1

📥 Commits

Reviewing files that changed from the base of the PR and between 49cb510 and 0b1cb1d.

📒 Files selected for processing (1)
  • src/termio/stream_handler.zig
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch task-ios-surface-mailbox-nonblocking

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.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0b1cb1d3b6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +133 to +135
if (comptime builtin.os.tag == .ios) {
self.deinitDroppedSurfaceMessage(msg);
return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve non-coalescible surface events

On iOS, when the app/surface mailbox is saturated, this drops every surface message, but several messages sent through this helper are one-shot state transitions rather than redraw hints. For example dcsCommand sends .tmux_control enter/exit events via surfaceMessageWriter; if the queue is full during a pane-output burst and the .exit event is dropped here, StreamHandler has already destroyed tmux_viewer while the embedded runtime never receives the exit callback and can remain stuck in tmux-control UI state. Please only drop messages that are safe to coalesce/rederive, or keep retrying/prioritizing critical surface events without an unbounded block.

Useful? React with 👍 / 👎.

@greptile-apps

greptile-apps Bot commented Jun 25, 2026

Copy link
Copy Markdown

Greptile Summary

This PR ports the iOS render-stall fix from the parent cmux PR into the Ghostty stream_handler. It mirrors the pattern already applied to rendererMessageWriter — on iOS, when an instant push to the surface mailbox fails, the code now returns immediately instead of blocking forever (which would deadlock the serial dispatch queue). A new deinitDroppedSurfaceMessage helper cleans up the three Message variants that carry heap-allocated WriteReq data (clipboard_write, pwd_change, tmux_control) so dropped messages don't leak memory.

  • iOS drop path in surfaceMessageWriter: When push(.instant) returns 0 on iOS, owned resources are freed and the function returns, preventing the serial-queue deadlock that caused render stalls.
  • deinitDroppedSurfaceMessage: Correctly identifies and deinits all current WriteReq-bearing union variants; uses else => {} for the rest, which is a maintenance risk if new owned-resource variants are added later.

Confidence Score: 4/5

The change is a narrow, well-scoped iOS-only fix that correctly frees owned resources and avoids the deadlock. The non-iOS path is untouched.

The deinit logic correctly covers all three WriteReq-bearing Message variants in the current union, and the iOS-only guard is compile-time checked. The main concern is the else => {} catch-all, which would silently skip cleanup for any future owned-resource variant added to Message, and clipboard_write drops happen without any log warning, making them invisible in production diagnostics.

src/termio/stream_handler.zig — specifically deinitDroppedSurfaceMessage and the silent drop site in surfaceMessageWriter.

Important Files Changed

Filename Overview
src/termio/stream_handler.zig Adds iOS-specific early-return in surfaceMessageWriter to drop messages instead of blocking forever, plus deinitDroppedSurfaceMessage to free WriteReq allocations. The deinit logic correctly covers all three MessageData-bearing variants (clipboard_write, pwd_change, tmux_control), but the else => {} catch-all is a maintenance hazard if new owned-resource variants are added.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[surfaceMessageWriter called] --> B["push(msg, .instant)"]
    B -->|"returns > 0 (success)"| C[Done — message queued]
    B -->|"returns 0 (mailbox full)"| D{iOS?}
    D -->|Yes| E[deinitDroppedSurfaceMessage]
    E --> F{msg variant}
    F -->|clipboard_write| G["w.req.deinit()"]
    F -->|pwd_change| H["w.deinit()"]
    F -->|tmux_control| I["w.data.deinit()"]
    F -->|"else (no owned heap)"| J[no-op]
    G --> K[return — message dropped]
    H --> K
    I --> K
    J --> K
    D -->|No — macOS/Linux| L[unlock renderer mutex]
    L --> M["push(msg, .forever) — blocks until space"]
    M --> N[re-lock renderer mutex]
    N --> O[Done — message queued]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A[surfaceMessageWriter called] --> B["push(msg, .instant)"]
    B -->|"returns > 0 (success)"| C[Done — message queued]
    B -->|"returns 0 (mailbox full)"| D{iOS?}
    D -->|Yes| E[deinitDroppedSurfaceMessage]
    E --> F{msg variant}
    F -->|clipboard_write| G["w.req.deinit()"]
    F -->|pwd_change| H["w.deinit()"]
    F -->|tmux_control| I["w.data.deinit()"]
    F -->|"else (no owned heap)"| J[no-op]
    G --> K[return — message dropped]
    H --> K
    I --> K
    J --> K
    D -->|No — macOS/Linux| L[unlock renderer mutex]
    L --> M["push(msg, .forever) — blocks until space"]
    M --> N[re-lock renderer mutex]
    N --> O[Done — message queued]
Loading

Reviews (1): Last reviewed commit: "chore: refresh GhosttyKit artifact" | Re-trigger Greptile

Comment on lines +148 to +153
switch (msg) {
.clipboard_write => |w| w.req.deinit(),
.pwd_change => |w| w.deinit(),
.tmux_control => |w| w.data.deinit(),
else => {},
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Non-exhaustive switch may hide future resource leaks

The else => {} catch-all means a future Message variant that carries heap-allocated data (another WriteReq-style field) will silently skip cleanup if it reaches the drop path. Because Zig's exhaustive switch would produce a compile error when a new variant is added, using else trades that compile-time guarantee away. If a new owned-resource variant is added to apprt.surface.Message without updating this function, the alloc case of the underlying MessageData union will never be freed, causing a leak that only manifests on iOS under mailbox pressure.

Comment on lines +133 to +136
if (comptime builtin.os.tag == .ios) {
self.deinitDroppedSurfaceMessage(msg);
return;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Silent drop of clipboard_write gives no user feedback

When clipboard_write is enqueued during a mailbox-saturation burst on iOS, it is silently dropped and the user's clipboard operation disappears with no error or retry. Unlike scrollbar ticks or color-change notifications (which are effectively stateless), a clipboard_write is a one-shot user action; if the surface never receives it, the user gets no indication that the write failed. Consider at minimum a log.warn at the drop site so the iOS-only silent-drop path is diagnosable in production logs.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

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.

2 participants