Skip to content

Avoid blocking iOS surface mailbox writes#90

Open
lawrencecchen wants to merge 1 commit into
mainfrom
ios-surface-mailbox-nonblocking
Open

Avoid blocking iOS surface mailbox writes#90
lawrencecchen wants to merge 1 commit into
mainfrom
ios-surface-mailbox-nonblocking

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Jul 1, 2026

Copy link
Copy Markdown

Summary

  • Keep iOS surface output processing nonblocking when the app/surface mailbox is saturated.
  • Drop lossy UI side-effect messages on iOS instead of parking the Ghostty surface queue behind ghostty_app_tick.
  • Free owned payloads for dropped pwd, clipboard, and tmux-control messages.

Verification

  • Built GhosttyKit with zig build -Demit-xcframework=true -Dxcframework-target=universal -Doptimize=ReleaseFast from cmux.
  • cmux iOS XCUITest: testScrollForwardingDoesNotOutliveLocalRenderLiveness passed after the parent cmux harness reproduced the pre-fix busy snapshot state.

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


Summary by cubic

Keep iOS surface output nonblocking when the app/surface mailbox is saturated to avoid parking the surface queue behind ghostty_app_tick. This preserves render and snapshot liveness on a busy main thread.

  • Bug Fixes
    • On iOS, drop UI side-effect surface messages when the mailbox is full instead of blocking.
    • Deinit payloads for dropped messages: pwd_change, clipboard_write, and tmux_control.

Written for commit 95fa65e. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • Bug Fixes
    • Improved message delivery handling when the queue is full on iOS, preventing unnecessary retries.
    • Ensured any pending message data is released before dropped messages are discarded, reducing leak risk.
    • Kept existing behavior unchanged on non-iOS platforms.

@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: fcc227d2-4a7e-4127-a567-6687fed58763

📥 Commits

Reviewing files that changed from the base of the PR and between 1b454eb and 95fa65e.

📒 Files selected for processing (1)
  • src/termio/stream_handler.zig

📝 Walkthrough

Walkthrough

Modifies StreamHandler.surfaceMessageWriter to add iOS-specific handling when a message push with .instant fails due to a full mailbox: the message is dropped and its owned payload deinitialized, rather than falling back to a blocking .forever push. A new helper, deinitDroppedSurfaceMessage, releases resources for specific message variants.

Changes

Surface Message Drop Handling

Layer / File(s) Summary
Drop handling and cleanup helper
src/termio/stream_handler.zig
On iOS, when an instant mailbox push fails, the message is dropped and its owned payload deinitialized via a new deinitDroppedSurfaceMessage helper covering pwd_change, clipboard_write, and tmux_control variants; non-iOS behavior still unlocks the mutex and retries with a .forever push.

Estimated code review effort: 2 (Simple) | ~10 minutes

Sequence Diagram(s)

sequenceDiagram
    participant Renderer
    participant StreamHandler
    participant SurfaceMailbox

    Renderer->>StreamHandler: surfaceMessageWriter(message)
    StreamHandler->>SurfaceMailbox: push(message, .instant)
    SurfaceMailbox-->>StreamHandler: push failed (mailbox full)
    alt iOS
        StreamHandler->>StreamHandler: deinitDroppedSurfaceMessage(message)
        StreamHandler-->>Renderer: return (message dropped)
    else non-iOS
        StreamHandler->>StreamHandler: unlock renderer mutex
        StreamHandler->>SurfaceMailbox: push(message, .forever)
        SurfaceMailbox-->>StreamHandler: delivered
    end
Loading
🚥 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 clearly matches the main change: making iOS surface mailbox writes nonblocking by avoiding blocking retries when full.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 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: 95fa65e65b

ℹ️ 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 +146 to +149
if (comptime builtin.os.tag == .ios) {
var dropped = msg;
deinitDroppedSurfaceMessage(&dropped);
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 Handle protocol replies before dropping iOS messages

This iOS full-mailbox path drops every surfaceMessageWriter message, but some of those are request/response protocol messages rather than lossy UI notifications: clipboardContents('?') sends .clipboard_read, and sendSizeReport(.csi_21_t) sends .report_title, both of which rely on the app thread to write a reply back to the PTY (for OSC 52, Surface.completeClipboardReadOSC52 even replies for empty clipboard data because the client is expecting one). When the app mailbox is saturated on iOS, these requests now disappear with no fallback response, so shell programs querying the clipboard or title can hang or time out; please special-case response-producing messages instead of dropping them wholesale.

Useful? React with 👍 / 👎.

@cubic-dev-ai cubic-dev-ai 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.

1 issue found across 1 file

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/termio/stream_handler.zig">

<violation number="1" location="src/termio/stream_handler.zig:148">
P2: This drops protocol response requests such as clipboard reads and title reports when the iOS mailbox is full. Limit the nonblocking drop path to truly lossy UI side-effect messages, otherwise OSC/title queries can silently never receive a response.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

// mailbox invariant.
if (comptime builtin.os.tag == .ios) {
var dropped = msg;
deinitDroppedSurfaceMessage(&dropped);

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: This drops protocol response requests such as clipboard reads and title reports when the iOS mailbox is full. Limit the nonblocking drop path to truly lossy UI side-effect messages, otherwise OSC/title queries can silently never receive a response.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/termio/stream_handler.zig, line 148:

<comment>This drops protocol response requests such as clipboard reads and title reports when the iOS mailbox is full. Limit the nonblocking drop path to truly lossy UI side-effect messages, otherwise OSC/title queries can silently never receive a response.</comment>

<file context>
@@ -130,12 +130,39 @@ pub const StreamHandler = struct {
+            // mailbox invariant.
+            if (comptime builtin.os.tag == .ios) {
+                var dropped = msg;
+                deinitDroppedSurfaceMessage(&dropped);
+                return;
+            }
</file context>

@greptile-apps

greptile-apps Bot commented Jul 1, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds an iOS-specific non-blocking fast-path in surfaceMessageWriter: when the app/surface mailbox is full and an .instant push fails, the code now drops the message and frees any owned heap payload (for pwd_change, clipboard_write, and tmux_control variants) rather than parking the surface queue behind a .forever push that can only be drained by the main-thread ghostty_app_tick.

  • Non-blocking drop on iOS: Under mailbox saturation, surface messages (title, cwd, bell, color, progress, tmux events) are silently discarded on iOS to preserve render and snapshot liveness, matching the existing iOS renderer mailbox invariant.
  • Owned-payload cleanup: A new deinitDroppedSurfaceMessage helper correctly frees heap-allocated WriteReq data for the three message variants that can carry allocator-owned buffers, preventing memory leaks on the drop path.
  • Narrowly scoped: The change is gated with if (comptime builtin.os.tag == .ios) so it is dead code on all other platforms and does not alter the existing mutex-unlock/forever-push path for macOS, Linux, or Windows.

Confidence Score: 4/5

Safe to merge for the render-liveness fix; the trade-off of dropping messages instead of blocking is deliberate and well-documented.

The logic is sound and the three heap-owning variants are correctly cleaned up. The else => {} catch-all is a latent maintenance hazard but not a current defect. The more notable concern is that clipboard_read and report_title are request-response messages whose silent discard may cause terminal applications to hang.

src/termio/stream_handler.zig — specifically the deinitDroppedSurfaceMessage helper exhaustiveness and the behavioral implications of silently dropping clipboard_read and report_title.

Important Files Changed

Filename Overview
src/termio/stream_handler.zig Adds an iOS-specific non-blocking drop path for surfaceMessageWriter when the app mailbox is saturated; frees owned payloads for pwd_change, clipboard_write, and tmux_control via a new helper. The else => {} catch-all in the helper is a maintenance risk, and clipboard_read/report_title drops may cause application-level hangs.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant VT as VT Stream
    participant SH as StreamHandler
    participant MB as App/Surface Mailbox
    participant Tick as ghostty_app_tick

    VT->>SH: emit surface message
    SH->>MB: push(msg, .instant)
    alt instant push succeeds
        MB-->>SH: "returns > 0"
        Note over SH: message queued
    else mailbox full
        MB-->>SH: returns 0
        alt iOS comptime
            Note over SH: deinitDroppedSurfaceMessage()
            SH-->>VT: return non-blocking
        else non-iOS
            Note over SH: unlock renderer mutex
            SH->>MB: push(msg, .forever)
            MB-->>SH: space available
            Note over SH: re-lock renderer mutex
        end
    end
    Tick->>MB: drain messages
    MB-->>Tick: surface messages delivered
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"}}}%%
sequenceDiagram
    participant VT as VT Stream
    participant SH as StreamHandler
    participant MB as App/Surface Mailbox
    participant Tick as ghostty_app_tick

    VT->>SH: emit surface message
    SH->>MB: push(msg, .instant)
    alt instant push succeeds
        MB-->>SH: "returns > 0"
        Note over SH: message queued
    else mailbox full
        MB-->>SH: returns 0
        alt iOS comptime
            Note over SH: deinitDroppedSurfaceMessage()
            SH-->>VT: return non-blocking
        else non-iOS
            Note over SH: unlock renderer mutex
            SH->>MB: push(msg, .forever)
            MB-->>SH: space available
            Note over SH: re-lock renderer mutex
        end
    end
    Tick->>MB: drain messages
    MB-->>Tick: surface messages delivered
Loading

Reviews (1): Last reviewed commit: "Avoid blocking iOS surface mailbox write..." | Re-trigger Greptile

Comment on lines +157 to +164
fn deinitDroppedSurfaceMessage(msg: *apprt.surface.Message) void {
switch (msg.*) {
.pwd_change => |*w| w.deinit(),
.clipboard_write => |*w| w.req.deinit(),
.tmux_control => |*v| v.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 The else => {} catch-all silently no-ops for any apprt.surface.Message variant not explicitly listed. If a new heap-owning variant is added to the union and routed through surfaceMessageWriter, the iOS drop path will leak its allocation with no compile-time warning. Exhaustively listing the non-owning arms forces the compiler to flag new variants that need to be audited here.

Suggested change
fn deinitDroppedSurfaceMessage(msg: *apprt.surface.Message) void {
switch (msg.*) {
.pwd_change => |*w| w.deinit(),
.clipboard_write => |*w| w.req.deinit(),
.tmux_control => |*v| v.data.deinit(),
else => {},
}
}
fn deinitDroppedSurfaceMessage(msg: *apprt.surface.Message) void {
switch (msg.*) {
.pwd_change => |*w| w.deinit(),
.clipboard_write => |*w| w.req.deinit(),
.tmux_control => |*v| v.data.deinit(),
// All remaining variants are plain value types (no heap ownership).
// If a new heap-owning variant is added to Message, add it above and
// call its deinit; the exhaustive match will ensure it isn't missed.
.set_title,
.report_title,
.set_mouse_shape,
.clipboard_read,
.change_config,
.close,
.child_exited,
.desktop_notification,
.renderer_health,
.present_surface,
.password_input,
.color_change,
.selection_scroll_tick,
.ring_bell,
.progress_report,
.start_command,
.stop_command,
.scrollbar,
.search_total,
.search_selected,
=> {},
}
}

Comment on lines +146 to +150
if (comptime builtin.os.tag == .ios) {
var dropped = msg;
deinitDroppedSurfaceMessage(&dropped);
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 request-response messages may hang applications

clipboard_read and report_title (sent via surfaceMessageWriter at lines 1164 and 1562) are not one-way UI side-effects — they are request-response sequences where the surface is expected to write data back to the PTY. When these messages are dropped under saturation, the remote terminal application (e.g. a shell using OSC 52 ? to read the clipboard, or an app issuing CSI 21 t to query the window title) will receive no response and may block indefinitely or time out visibly. Worth documenting the known behavioral gap, or gating the drop on variant type.

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