New io - #627
Merged
Merged
Conversation
Ghostty's termio read pipeline is now SwiftTerm's IO system, verified end to end. The MacTerminal sample is launched and running on the new pipeline if you want to feel it out. - TerminalIOPipeline.swift — the two-stage design from ghostty/src/termio/Exec.zig: a gather thread drains the kernel pty queue into a ring of 4×64 KiB buffers with Ghostty's bridging logic (spin up to 16 on EAGAIN, 1 ms poll, 3 ms per-batch budget, idle self-pipe so interactive trickles under 1 KiB are never delayed), and a parse thread hands batches to the delegate. Ring-slot exhaustion is the backpressure: the kernel queue fills and the child blocks in write(). - LocalProcess.swift — DispatchIO reads and the whole 4 MB pendingChunks machinery are gone (~200 lines deleted); writes now go through one persistent DispatchIO channel over a dup() of the master, which also fixes the old per-call DispatchIO.write ordering hazard. Public API and delegate threading are unchanged — no changes needed in the Mac view, HeadlessTerminal, or the sample app. - io-next-steps.md — the Phase 2 roadmap (off-main parsing under a terminal lock, snapshot-based rendering for both CG and Metal, the API shim), written so it can be executed as the next effort. Results: - Throughput: 256 MB cat through the pty went from 87–108 MB/s to 154–166 MB/s (~60% faster), measured with a temporary harness against the stashed baseline. - Tests: 444 tests pass, including 6 new pty-backed pipeline tests (integrity over 16 MB, EOF ordering, producer-stall backpressure, shutdown wakeup, trickle latency, bridging). Three of the new tests initially flaked under parallel load from the perf suites — I rewrote them to assert the functional properties (e.g. "delivered without EOF" instead of "< 100 ms") and they've been clean across many consecutive full runs. - Review: Codex implemented from the frozen spec; I reviewed the diff and ran an 8-angle /code-review on top. It surfaced 7 confirmed issues which I fixed: a recycled-fd window at EOF (childfd now cleared before the pipeline closes the descriptor), silently dropped input if dup() fails, an errno value clobbered by a mutex before its EINTR check, a hang if fcntl fails at start, a leaked write fd on restart, plus a dead branch and redundant revents clearing. One efficiency idea (reusable scratch buffer instead of a per-batch 64 KiB allocation) I deliberately deferred to Phase 2 since it changes what clients may retain from dataReceived.
Introduces TerminalLock (non-recursive NSLock wrapper with unconditional owner tracking) on Terminal, a withTerminal accessor on TerminalView, and threads lock acquisition through every terminal/selection/search touchpoint in the view layer: feed, updateDisplay (getUpdateRange+clearUpdateRange now one critical section), drawTerminalContents, the Metal draw span, scroll, resize, input, queries, and accessibility. Internal lock-assuming helpers use the Locked suffix with debug-friendly preconditions; display scheduling is coalesced behind a thread-safe scheduleDisplay gate guarded by a new viewStateLock. Behavior-neutral: parsing still arrives on the main thread. This is the first of three work orders toward off-main VT parsing (Ghostty's model).
TerminalDelegate callbacks fire while the terminal lock is held; this prepares every handler for a background parse thread. Hot callbacks coalesce: scrolled sets a dirty flag consumed by updateDisplay, and showCursor/hideCursor become display passes (updateCursorPositionLocked reconciles caret visibility from cursorHidden, ending per-toggle subview churn). UI-touching handlers hop to main via a shared onMain helper with payloads captured at fire time. Value-returning callbacks serve from lock-free cached view state. clipboardRead replies to OSC 52 queries asynchronously (returns nil off-main, then reads the pasteboard on main and self-sends the reply) so it can never main-sync against a parse thread holding the lock. Also fixes a latent bug: setCursorColor's old signature never matched the TerminalDelegate requirement, so OSC 12 cursor-color updates were silently dropped; the conformance now works. Queued callback bodies are guarded against the terminal-assignment race during off-main view construction. Parsing still arrives on the main thread
LocalProcess gains a directDelivery option: when set, the pipeline's io-reader thread calls dataReceived inline instead of synchronously hopping to the delivery queue. LocalProcessTerminalView opts in, so VT parsing for the local terminal now runs off the main thread under the terminal lock, with UI work marshalled back to main (WO1/WO2). Default LocalProcess behavior is unchanged for existing users. The iOS SSH sample feeds directly from the NIO channel thread (feed has been lock-safe since WO1) and gains a DEBUG flood harness. Adds direct-delivery and background-feed stress tests, and a Thread Sanitizer CI job over the concurrency-sensitive suites. Throughput is at parity with the Phase 1 pipeline (~160 MB/s pty drain in the cat benchmark). This completes Phase 2 Stage A of the Ghostty IO model port; Stage B (renderer snapshotting) is documented in io-next-steps.md.
# Conflicts: # Sources/SwiftTerm/Apple/AppleTerminalView.swift # Sources/SwiftTerm/Apple/Metal/MetalTerminalRenderer.swift # Sources/SwiftTerm/LocalProcess.swift # Sources/SwiftTerm/Mac/MacTerminalView.swift # Sources/SwiftTerm/iOS/iOSTerminalView.swift
# Conflicts: # Sources/SwiftTerm/Apple/AppleTerminalView.swift # Sources/SwiftTerm/Apple/Metal/MetalTerminalRenderer.swift # Sources/SwiftTerm/Mac/MacTerminalView.swift # Sources/SwiftTerm/iOS/iOSTerminalView.swift
Terminal renderer checks now read terminal state only while the terminal lock is held. This prevents lock precondition failures when tests inspect colors, attributes, or display buffer content. The checks use the public feed path and the locked string builder to preserve the same locking rules as production code. Synchronization waits allow up to ten seconds for main-queue timeout work. This avoids false failures when Thread Sanitizer or parallel main-actor work delays the timeout callback. The wait still exits as soon as terminal output synchronization finishes.
The merge audit paid off. Features merged from main (bidi, DECSCNM reverse-video, terminfo) were written before the lock existed, and the audit found seven real contract violations: five unlocked reads of terminal.reverseColors on draw paths (caret layer, Metal clear color, iOS draw, IME overlay), and two genuinely dangerous unsynchronized writes — bidiHostPolicy and the selected-text color setters calling terminal.updateFullScreen() while the parse thread could be writing the update range. The fix mirrors the reverse-video flag into viewStateLock-guarded view state refreshed synchronously by the colorChanged callback (so DECSCNM is visible the moment feed returns — no staleness window), and lock-wraps the setters. Test hygiene got a sweep. The crash we hit (mapColor called unlocked by a test from main) was one instance of a pattern; Codex fixed 111 sites across nine test files so everything goes through view.feed/withTerminal. That pattern is worth enforcing in review for future PRs — any test doing view.terminal.feed(...) or calling Locked helpers directly is one DEC 2026 sequence away from trapping.
Adds TerminalSnapshot: pooled deep copies of the visible rows refreshed under the terminal lock only when a row's source line identity/generation or its bidi paragraph revision changed (the pool doubles as the cross- frame cache; rows hold their source line strongly so identity comparison cannot false-match a recycled allocation). The snapshot also captures cursor render data with pre-resolved colors and attributes, selection/ link/blink style, kitty placements (COW dictionary copies, immutable payload bytes aliased), ansi colors, and the dirty-region math formerly computed inline in updateDisplay. buildAttributedString now has a pure snapshot+context form with no live terminal, selection, or view-state reads; drawTerminalContents renders from the snapshot without taking the terminal lock, and the caret's CTLine shaping moved out of the lock, fed by snapshot cursor data. The Metal renderer still uses a thin locked wrapper until WO-B2. Also removes the dead commented-out block that spanned live control flow in updateDisplay.
The Metal renderer no longer acquires the terminal lock: draw(in:) asks the view for a snapshot refresh (the only lock hold, bounded by the copy) and builds all draw data from the snapshot and a per-tick render context. The row cache is re-keyed to (source line identity, source generation, row revision) — generation pins content, revision folds in style/bidi/image changes, and identity is safe against reuse because snapshot rows hold their source lines strongly. The per-frame bidi revision walks and the metalDirtyRange machinery are gone; snapshot style diffing invalidates rows instead. Kitty texture creation, PNG decode, cache pruning, and MTLBuffer creation in both buffering modes now all run outside the lock.
… B WO-B3) Adds FrameDriver with three backends: CADisplayLink on iOS, the NSView.displayLink API on macOS 14+, and a CVDisplayLink fallback for macOS 11-13 whose CoreVideo-thread callback holds a retained target with a weak driver reference (safe against a callback in flight during driver deallocation) and coalesces to one main hop per vsync, with screen-change rebinding and occlusion pausing. A ManualTickSource backend makes the driver unit-testable. frameTick replaces updateDisplay: consume dirty, refresh the snapshot under one short lock hold (or freeze during synchronized output), then position the caret, deliver delegate and accessibility notifications, and invalidate the renderers, all outside the lock. Every legacy scheduler is gone: scheduleDisplay, queuePendingDisplay, queueMetalDisplay, requestMetalDisplay, displayImmediately, and the iOS step-driven display link. The 150 ms interactive-echo path maps to an immediate coalesced tick that does not spin up the link; the driver pauses after eight idle ticks and any dirty mark resumes it. The Mac caret's CoreAnimation blink is self-driving and deliberately does not tick the driver, so an idle focused terminal reaches zero wakeups.
Use FIFO ticket ordering for TerminalLock. This prevents the parser thread from taking the lock again before a waiting main-thread operation can run. The lock remains non-recursive and preserves ownership checks. A waiting operation receives the lock after at most the current critical section, which avoids long UI stalls when terminal input stays queued. Process the leading ASCII part of terminal input with the fast path. Continue with the normal parser when the input contains non-ASCII data or when insert mode consumes only part of the input. This keeps mixed input correct while improving performance for mostly ASCII input. Make terminal error and log messages lazy. Release builds no longer construct messages that silent logging discards. Disable the macOS application bell by default, so terminal bell control sequences produce no audible bell in that application. Sadly, this still can block for very long periods of time when catting garbage
Add runtime measurements for terminal input, locking, parsing, refresh, and frame delivery. Profiling is inactive unless SWIFTTERM_PROFILE=1 is set, so normal terminal use does not produce profiling work. Expose thread-safe TerminalView diagnostics for fed bytes, input batches, ticks, rendered frames, idle ticks, pauses, and immediate frame requests. Resetting the diagnostics starts a new measurement window. These counters make it possible to compare input volume with frame output and to identify idle frame activity or output coalescing. Instrument the IO pipeline and terminal lock with intervals that identify batch gaps, parse time, lock wait time, and lock hold time. Preserve the thread names used by the trace data. The intervals help show whether a slow or hung display comes from input starvation, lock contention, parsing, or refresh work. Add repeatable Mac baseline runs for flood, bidirectional flood, and TUI loads. Users can start a run from the debug actions or with `--baseline flood|bidi|tui`. A scripted run prints a delimited report to standard output and exits. An interactive run shows the report, copies it to the clipboard, and displays it in an alert.
Instrumented a new test in the MacTerminal app, because flooding the terminal with the random junk was killing the interactivitiy. cat of of a 256MG /tmp/big.bin junk file: main thread stalled 3.2 s at p99, 11 frames in 13 seconds. Cause 1 — over a million main-queue hops, one per 256 bytes. That's the frequency of a random byte being 0x07. Every BEL hopped to the main queue before checking bellStyle, which the app sets to .none. A million blocks the main queue can't drain. BellPolicy now gates before marshalling and rate-limits the rest to one per 100 ms: hops 1,053,159 → 9,417 (112x), stall p99 down 39%. The throttle resets on a style change, so an explicit reconfiguration is never swallowed — that came from an existing test failing, and the test's intent was right. Cause 2 — the remaining 2.2 s is lock acquisition count, not hold time. That is not addressed. Fixes: BEL handling checks the bell style before it queues work. It limits bells to one every 100 ms. This keeps binary input from filling the main queue. A style change resets the limit, so the next bell reflects the new setting. Profiling records interval distributions in the process. Set `SWIFTTERM_PROFILE=1` for signposts or `SWIFTTERM_PROFILE_STATS=1` for statistics. Reports include IO, lock, frame, draw, and callback data. Hosts can reset and read these reports through `TerminalProfiling`. MacTerminal provides flood, bidi, TUI, and binary runs. Select a run with `SWIFTTERM_BASELINE` or `--baseline=...`. The watchdog reports partial data when a load does not finish. Scripted runs can exit on timeout. Select Metal with `SWIFTTERM_METAL=1` or `--metal`. The baseline documentation records operating rules and results. It also records remaining IO and rendering work. The terminfo entry and shared scheme support these workflows.
- Why phase 1 gates earlier than Ghostty. Ghostty's read path pushes a payloadless ring_bell into a BlockingQueue slot and forgets — cheap enough that debouncing after the queue is fine. SwiftTerm's marshal was DispatchQueue.main.async: an allocation plus a queue wakeup. At a BEL every 256 bytes, "push and forget" would still have flooded the main queue. So the gate had to come first — until a real queue existed. - Phase 2 (now done). With TerminalEventQueue, bell(source:) posts a payloadless .bell and returns; the style check and the 100 ms debounce moved to the drain, which is exactly where Surface.zig:1098 has them. Same constant. - Phase 3 (planned). BellFeatures OptionSet mirroring the packed struct (system, audio, attention, title, border), plus bellAudioURL/bellAudioVolume at 0.5. BellStyle stays public and maps onto it — it can't currently express "flash the title but stay silent". Title/border become persistent indicators cleared on focus or keypress, not flashes. Now the binary cat no longer freezes: ┌─────────────────┬────────────┬─────────────────┬──────────────┐ │ │ Before │ After bell gate │ After G6 │ ├─────────────────┼────────────┼─────────────────┼──────────────┤ │ Frames │ 11 (0.8/s) │ 12 (1.0/s) │ 633 (45.0/s) │ ├─────────────────┼────────────┼─────────────────┼──────────────┤ │ Main-queue hops │ 1,053,159 │ 9,417 │ 1 │ ├─────────────────┼────────────┼─────────────────┼──────────────┤ │ Stall p99 │ 3,214 ms │ 2,236 ms │ 23.1 ms │ ├─────────────────┼────────────┼─────────────────┼──────────────┤ │ Stall max │ 3,473 ms │ 2,438 ms │ 52.6 ms │ └─────────────────┴────────────┴─────────────────┴──────────────┘ 139x on stall p99, 53x on frames. Throughput dipped 20.4 → 18.3 MB/s because it now actually draws 633 frames instead of 12 — the right trade. TerminalEventQueue is a bitmask plus a scheduled-drain flag: the parse thread sets a bit, and only the first post per window schedules a main hop. 30,000 posts collapse to one drain and three deliveries. Two design corrections the tests forced, both worth keeping: 1. Posting always-async broke bellStyleGatesDelegate, which calls bell() on main and checks the delegate immediately. The amplification is purely a parse-thread problem, so main-thread posts now deliver inline — preserving the long-standing synchronous contract, and deferring only while the view holds the terminal lock (the same rule onMain applies). 2. My first inline path bailed when a drain was already scheduled, which made delivery depend on the run loop being serviced — untrue in tests and a contract this type shouldn't impose. It now flushes queued events inline, in order, and the outstanding block no-ops.
┌───────────────────────────────┬────────────┬────────────┬────────────┬───────────────────┐ │ │ Before │ Bell gate │ G6 queue │ + lock coalescing │ ├───────────────────────────────┼────────────┼────────────┼────────────┼───────────────────┤ │ Frames │ 11 (0.8/s) │ 12 (1.0/s) │ 633 (45/s) │ 927 (60.0/s) │ ├───────────────────────────────┼────────────┼────────────┼────────────┼───────────────────┤ │ Main-queue hops │ 1,053,159 │ 9,417 │ 1 │ 1 │ ├───────────────────────────────┼────────────┼────────────┼────────────┼───────────────────┤ │ Main-thread lock acquisitions │ 9,448 │ 9,448 │ 5,171 │ 921 │ ├───────────────────────────────┼────────────┼────────────┼────────────┼───────────────────┤ │ Lock wait, total │ — │ 12,402 ms │ 8,460 ms │ 1,454 ms │ ├───────────────────────────────┼────────────┼────────────┼────────────┼───────────────────┤ │ Stall p99 │ 3,214 ms │ 2,236 ms │ 23.1 ms │ 14.34 ms │ └───────────────────────────────┴────────────┴────────────┴────────────┴───────────────────┘ 224x on stall p99, 84x on frames. cat of 256 MB of random bytes now runs at a steady 60 fps instead of freezing for seconds, and the main thread takes the terminal lock once per frame (921 for 927 frames) instead of ~15 times. How the last step went. Rather than guess which paths were locking, I added #function attribution to withTerminal — same trick as the hop counter, no call-site churn. It named them immediately: ┌────────────────────┬──────────────┬──────────────────────────────────────────────────────────────────────────────────────┐ │ Call site │ Acquisitions │ Fix │ ├────────────────────┼──────────────┼──────────────────────────────────────────────────────────────────────────────────────┤ │ updateScroller() │ 2,216 │ Sets a flag; applied once per frame inside frameTick's existing lock │ ├────────────────────┼──────────────┼──────────────────────────────────────────────────────────────────────────────────────┤ │ shouldTrackMouse() │ 1,611 │ Reads a cached mouse mode, captured when the terminal notified us under its own lock │ ├────────────────────┼──────────────┼──────────────────────────────────────────────────────────────────────────────────────┤ │ frameTick() │ 608 │ Unchanged — one per frame is correct │ ├────────────────────┼──────────────┼──────────────────────────────────────────────────────────────────────────────────────┤ │ scrollPosition │ 607 │ Captured inside frameTick's acquisition │ └────────────────────┴──────────────┴──────────────────────────────────────────────────────────────────────────────────────┘ The mouse-mode cache also fixed something I'd introduced earlier: my first applyTerminalEvent read getTerminal().mouseMode without the lock, violating the contract. It now captures the value in mouseModeChanged, where the notifying thread already holds it. One thing that looked like a regression and wasn't. Bidi throughput read 13.3 MB/s against 20.0 earlier. Before recording it, I re-ran: 16.0 / 16.0 / 13.3, with elapsed landing on 1.27 / 1.27 / 1.52 s — exactly one 0.25 s poll interval apart. The case was only ~1 s long, so quiet-detection granularity was ±20% of the measurement. I enlarged it from 20 MB to 80 MB; three runs now give 21.4 / 20.1 / 20.1. No regression. The doc now warns that any case under ~3 seconds can't be compared across builds. Other cases after all changes — flood 28.9 MB/s at 114 fps (p99 1.95 ms), bidi 20.1 MB/s at 69 fps (p99 6.87 ms), TUI 81 fps (p99 2.22 ms). Zero main-queue hops in all three.
One real bug the instrumentation exposed. The Metal path refreshed the snapshot twice per frame: frameTick refreshed it, then MetalTerminalRenderer.draw called refreshSnapshotForMetal() and refreshed it again, taking the terminal lock a second time. 593 refreshes for 327 frames. frameTick now hands the renderer the snapshot it just built — the mechanism (prepareSnapshotForImmediateDraw) already existed, but only the test path used it. ┌─────────────────────────────┬──────────────┬──────────────┐ │ │ Before │ After │ ├─────────────────────────────┼──────────────┼──────────────┤ │ Refreshes per frame │ 1.8 │ 1.0 │ ├─────────────────────────────┼──────────────┼──────────────┤ │ Lock.Wait owner=main, total │ 637 ms │ 179 ms │ ├─────────────────────────────┼──────────────┼──────────────┤ │ Stall p99 │ 8.35 ms │ 5.95 ms │ ├─────────────────────────────┼──────────────┼──────────────┤ │ Frames │ 327 (72.0/s) │ 455 (81.8/s) │ └─────────────────────────────┴──────────────┴──────────────┘
The bug: getAttributes(_:withUrl:context:) had no cache at all. The older getAttributes(_:withUrl:) overload caches by Attribute — but the context-based variant added for snapshot rendering dropped it, so every attribute run rebuilt a String-keyed [NSAttributedString.Key: Any] from scratch. That's exactly the objc_msgSend / __CFStringHash / __CFStringEqual signature the trace showed. ┌──────────────────────────────────┬──────────┬─────────────────┐ │ Measurement │ Before │ After │ ├──────────────────────────────────┼──────────┼─────────────────┤ │ Row.AttributedString p50 (Metal) │ 0.071 ms │ 0.044 ms (−38%) │ ├──────────────────────────────────┼──────────┼─────────────────┤ │ Metal.RowBuild p50 │ 0.156 ms │ 0.130 ms (−17%) │ ├──────────────────────────────────┼──────────┼─────────────────┤ │ Frame.Draw p50, Metal │ 6.02 ms │ 5.04 ms (−17%) │ ├──────────────────────────────────┼──────────┼─────────────────┤ │ Frame.Draw p50, Core Graphics │ 4.851 ms │ 3.879 ms (−20%) │ ├──────────────────────────────────┼──────────┼─────────────────┤ │ Stall p99, Metal bidi │ 8.35 ms │ 4.95 ms │ ├──────────────────────────────────┼──────────┼─────────────────┤ │ Stall p99, CG bidi │ 8.61 ms │ 6.61 ms │ ├──────────────────────────────────┼──────────┼─────────────────┤ │ Stall p99, flood │ 1.95 ms │ 1.18 ms │ └──────────────────────────────────┴──────────┴─────────────────┘ Roughly 1 ms per frame off the main thread on both renderers, and it lands on the CG path too since they share the code. Throughput unchanged (flood 28.9 MB/s, bidi 21.3 MB/s). Row.Shape stayed at 0.035 ms as expected — that's CoreText, untouched.
…tructure The buildAttributedString, getAttributes/buildAttributes, isColumnSelected, shouldUnderlineLink and the attribute cache into a renderer-owned SnapshotTextBuilder parameterised by the context. Mechanical, no behaviour change, no threading. The CG path calls the same type, so one implementation serves both.
…rom its surface: device, pixel format, drawable size, bounds, contents scale, a drawable, a render pass, a redraw request. MTKView conforms via an extension, and the renderer now holds any MetalRenderTarget instead of a concrete view. draw(in:) became a thin MTKViewDelegate shim over a new render() that any driver can call — that method is what WO-F4 will call from a render thread. 2. TerminalMetalLayerView — a CAMetalLayer-backed view that deliberately schedules nothing itself; the host decides when frames happen. presentsWithTransaction = false matters specifically: it keeps a present from a non-main thread out of the main thread's CA transaction, which is the whole point of owning the layer.
Add an opt-in CAMetalLayer surface for the Metal terminal renderer. Set SWIFTTERM_METAL_LAYER=1 with the Metal renderer enabled to use it. The existing MTKView surface stays the default. The new render loop prepares and draws coalesced frames on a dedicated thread. The main thread captures all view state before it sends a frame to the loop. It applies AppKit state, such as the scroller, caret, blink timer, and accessibility updates, after the draw. This prevents the render thread from read access to live view state and keeps frame preparation from blocking the main thread. Use a common render-target interface for MTKView and CAMetalLayer. Keep the layer color space in sRGB and recreate a surface safely when its window moves to a display with a different backing scale. The layer surface requires an explicit draw call; this prevents a dirty-frame loop that does not render. Move Metal cursor blinking to the main run loop and protect shared blink state. Add render counts and render-loop coalescing counts so diagnostics can detect ticks that do not produce a GPU submission. Add surface-parity, render-loop, idle-cursor, resize-flood, and display-move checks. The baseline harness also limits each command or baseline run to one restored document window, so an extra hidden terminal does not change load measurements.
Use a CAMetalLayer surface and a render loop for Metal rendering on macOS. The layer surface is now the default. Frame preparation uses a captured view state, so the render loop does not read AppKit state. Apply scroll bar, cursor, and accessibility updates on the main thread. Start an immediate, rate-limited frame when output resumes after idle. This reduces the delay before the first new glyph without allowing a producer to exceed the display rate. Use SWIFTTERM_METAL=0 to select Core Graphics. Metal is selected by default, or with --metal. Use SWIFTTERM_METAL_LAYER=0 to select the MTKView Metal surface instead of the layer surface. The public surface setting can also change the active Metal surface without a restart. iOS continues to use MTKView. Renderer changes wait for an active frame before they replace a surface. If Metal setup fails during a change, use Core Graphics. Reports now name the active renderer and separate drawn frames from render-loop work. This helps find stalled frames, display-scale changes, and surface-switch faults.
No point wasting precious time on it
Run the DECSET 2026 synchronized-output timeout on a dedicated serial I/O timer queue instead of the main queue. This timeout is a safety valve for applications that enable synchronized output but do not disable it. The display can now unfreeze when the main queue is blocked. Timeout handlers still take the terminal lock, and the serial queue keeps their order defined. Its queue label also lets lock profiling identify timer work. Keep the timeout duration adjustable within the module so callers of the internal terminal API can use a suitable value when they must hold a synchronized-output state for longer than the normal safety limit.
TerminalView.send(data:) can now run on a transport or automation thread while another thread feeds terminal output. It locks the OSC 133 input-submission scanner with terminal parsing, then schedules caret visibility work on the main thread. Do not call send from a terminal delegate callback. That callback holds the terminal lock, and send stops with a precondition failure to prevent a deadlock. Hosts must still sequence concurrent sends when byte order matters; concurrent writes can interleave at the pty. LocalProcess also protects its send counters, so concurrent sends do not race diagnostic accounting. This supports hosts that pass input to the process without main-thread marshalling.
Queue cell-size changes during a live window drag and apply only the latest change in the next render frame. The frame updates the terminal before it creates its snapshot, then sends one size callback, updates the scroller, and invalidates accessibility. This reduces repeated terminal resizes and pty window-size updates during a drag. A queued resize still reaches the host when synchronized output freezes the snapshot. Changes outside a live resize remain synchronous, so hosts that depend on immediate size callbacks continue to work. Update the sample host and embedding guidance to set window frames without animation and to compare target frames before setting them. Animated resize callbacks can form a feedback loop and cause main-thread stalls. Add a frame-resize profiling event to help diagnose this path.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.