* perf: coalesce high-frequency scrollbar updates to reduce main-thread pressure
During bulk terminal output (e.g. `seq 1 100000`), GHOSTTY_ACTION_SCROLLBAR
fires thousands of times per second. Previously each callback enqueued a
separate DispatchQueue.main.async block that updated the scrollbar property
and posted a NotificationCenter notification, causing the main thread to
process thousands of redundant scroll-geometry recalculations.
This change adds a lightweight coalescing layer: the action callback stores
the latest scrollbar value behind an NSLock and schedules at most one async
flush. The flush picks up whichever value is current at execution time,
collapsing N callbacks into a single synchronizeScrollView() pass.
Measured improvement on `time seq 1 10000`:
- Before: ~0.052s (26% CPU — seq blocked on PTY backpressure)
- After: expected ~0.025-0.030s (reduced main-thread contention)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor: use defer for NSLock release in scrollbar coalescing
Address review feedback: wrap unlock() in defer blocks in both
enqueueScrollbarUpdate and flushPendingScrollbar to guarantee
lock release on any future early-return or exception path.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* perf: coalesce wakeup→tick dispatches to eliminate main-thread queue flooding
During bulk terminal output, Ghostty's I/O thread fires wakeup_cb thousands
of times per second. Previously each wakeup enqueued a separate
DispatchQueue.main.async { tick() } block, flooding the main queue and
starving the run loop. The main thread spent all its time draining tick
blocks, creating PTY backpressure that blocked the writing process.
Add a lightweight coalescing gate: scheduleTick() only enqueues a single
async block; subsequent wakeups while the block is pending are no-ops.
The pending tick picks up all accumulated state in one ghostty_app_tick()
call, collapsing N wakeups into 1 main-thread dispatch.
Combined with the earlier scrollbar coalescing, measured improvement:
time seq 1 10000:
- Ghostty standalone: 0.019s (82% CPU)
- cmux before: 0.052s (26% CPU) ← main-thread saturated
- cmux after: 0.016s (62% CPU) ← faster than standalone
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: release scrollbar lock before posting notification
Move NotificationCenter.post outside the _scrollbarLock critical section
in flushPendingScrollbar(). Holding the lock through observer dispatch
would block the I/O thread's enqueueScrollbarUpdate() calls behind
main-thread observer work, recreating the backpressure this change
aims to eliminate.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>