Investigating macOS input-to-display latency
Summary
A repeatable difference in input-to-display latency has been observed on macOS depending on whether an application is launched normally as an .app bundle or its bundled Mach-O executable is run directly:
/Applications/Koi.app/Contents/MacOS/Koi
While normal application launches consistently produce substantially higher latency:
open -n /Applications/Koi.app
The executable, application bundle, rendering implementation, display, input source, and measurement methodology are otherwise identical.
The issue was initially discovered while investigating an apparent typing-latency regression in Koi Editor. The investigation first focused on Koi, Qt, build configuration, code signing, accessibility, input methods, process state, and application-side painting.
A minimal Qt QPlainTextEdit application reproduced the same behavior without Scintilla or Koi. The same normal app launch versus direct Mach-O latency difference also occurs in Xcode on Apple's current macOS Tahoe 26.6.1 running on a MacBook Air M4.
The behavior has now been reproduced with:
- Koi
- a minimal Qt QPlainTextEdit application
- Xcode
- BBEdit
- Sublime Text
- Zed
It has also been reproduced across:
- Mac mini M2 Pro, macOS Sequoia 15.7.5
- MacBook Air M4, macOS Tahoe 26.6.1
The additional latency is not visible in the application-side timing measured here, suggesting that it occurs later in the presentation path.
At 60 Hz the p95 difference in the minimal reproduction was 17.2 ms and at 100 Hz it was 10.6 ms. These values closely correspond to one refresh interval at 60 Hz and 100 Hz respectively.
Further testing shows that direct execution alone does not produce the lower-latency state. The same executable is fast when launched from Apple Terminal but slow when launched from Ghostty.
The launch context survives fork and exec, and process coalition membership provides a concrete observable difference between the tested fast and slow contexts.
The current evidence points toward macOS presentation or frame-scheduling state rather than application rendering. The state is affected by launch context and can also change dynamically during physical mouse activity.
The precise mechanism has not yet been identified.
Main observations so far:
- The exact same bundled executable can exhibit either latency state depending on its launch context.
- Direct execution from Apple Terminal is fast, while direct execution from Ghostty is slow.
- The fast state survives detaching from Terminal, changing session state, fork, and exec.
- Normal LaunchServices launches are managed by RunningBoard and receive separate application coalitions.
- Processes launched from Apple Terminal or Ghostty inherit their launcher's coalitions.
- Coalition membership correlates with the latency state but has not been shown to cause it.
- A minimal Qt application reproduces the behavior.
- Application-side input and paint timing does not contain the externally measured delay.
- At 60 Hz and 100 Hz, the p95 difference closely follows one display interval.
- Physical mouse movement temporarily reduces the latency of a normally launched application.
- The effect does not require visible cursor movement and is not reproduced by synthetic mouse events, trackpad movement, or application-side attempts to remain active.
- System Trace shows increased WindowServer and Core Animation activity during mouse movement, but this activity is not required for the lower-latency Terminal launch state.
- The behavior reproduces in Xcode and several unrelated editors across Sequoia and Tahoe.
- Summary
- Initial observation in Koi
- Methodology
- Isolating the launch method
- Ruling out Koi
- Minimal reproduction using a different editor stack
- Refresh-rate test
- Xcode reproduces the behavior
- Reproduction on macOS Tahoe 26.6.1
- Investigating the process launch context
- What the tests rule out
- Current working hypothesis
- Reproduction
- Future work
- Conclusion
Initial observation in Koi
The issue was initially noticed while investigating an apparent regression in Koi Editor's typing-latency benchmark.
Koi had historically produced very low input-to-display latency. Recent measurements, however, were substantially worse.
The initial assumption was that something had changed in Koi itself: the editor implementation, the Qt build, accessibility support, code signing, or the build environment.
During that investigation, an unexpected distinction was discovered:
- Launching Koi normally produced slower behavior.
- Directly launching the Mach-O produced the previously expected fast behavior.
Importantly, these are not different builds.
They are the exact same executable inside the exact same application bundle.
That observation changed the direction of the investigation from "what became slower in Koi?" to "what is different about the way macOS starts the same application?"
Methodology
The benchmark measures actual input-to-visible-pixel latency rather than merely measuring application event handling. The tool keypress.sh is used to generate input and detect the corresponding visual change on the display.
Conceptually, the measurement is:
- keyboard input
- application receives input
- application updates document
- application paints
- macOS composites/presents frame
- corresponding pixel changes on display
- latency measured
Internal application instrumentation did not show a comparable normal open versus direct Mach-O difference. The application reaches and completes its paint operation quickly in both cases.
The large difference appears only when measuring all the way through to the actual corresponding pixel change on the display.
The launch-dependent difference is not visible in the application-side timing measured here, suggesting that the additional latency occurs later in the presentation path.
Isolating the launch method
The first question was whether Terminal itself was responsible.
Running the bundled executable directly was fast:
/Applications/Koi.app/Contents/MacOS/Koi
Opening that executable with open was also fast:
open /Applications/Koi.app/Contents/MacOS/Koi
Launching the application bundle was slower:
open /Applications/Koi.app
The executable remained fast when detached from Terminal with nohup and disown:
nohup /Applications/Koi.app/Contents/MacOS/Koi </dev/null >/tmp/koi.out 2>/tmp/koi.err & disown
Different LaunchServices forms all produced the slower result:
open -n /Applications/Koi.app
open -na /Applications/Koi.app
open -nb ai.hackerman.koi
open -b ai.hackerman.koi
Process environment, parentage, terminal attachment, QoS, and final AppKit activation state were also compared.
A direct executable launch included environment values associated with Terminal:
__CFBundleIdentifier=com.apple.Terminal
XPC_SERVICE_NAME=0
TERM=xterm-256color
Launching normally instead contained values such as:
__CFBundleIdentifier=ai.hackerman.koi
XPC_SERVICE_NAME=application.ai.hackerman.koi....
XPC_FLAGS=1
Changing bundle identifier manually did not make the direct executable slow:
__CFBundleIdentifier=ai.hackerman.koi \
/Applications/Koi.app/Contents/MacOS/Koi
Both launch modes reported the same QoS:
qos=0x21
relative=0
Both launch methods reported the same final AppKit activation state:
activationPolicy=0
active=True
Ruling out Koi
The investigation initially focused on Koi's editor stack.
Several plausible causes were tested:
- Qt input-method support
- QScintilla accessibility
- code signing
- repaint size
- paint cost
- application-side CPU time
Disabling Qt input-method support had no effect.
QScintilla was rebuilt with its Qt accessibility support disabled using QT_NO_ACCESSIBILITY. The launch-dependent difference remained.
Koi's paintEvent() was instrumented directly. Typical paint times were well below 1 ms, and both launch modes painted equivalent regions.
Minimal reproduction using a different editor stack
To remove Koi, Scintilla, custom lexers, and the rest of the editor stack, I built a minimal Qt application using QPlainTextEdit.
The application was packaged as a normal .app bundle. It reproduced the same difference between normal app launch and direct execution.
Refresh-rate test
At 60 Hz, the minimal Qt application:
| Launch method | avg | p95 | p99 |
|---|---|---|---|
| Open normally | 29.676 ms | 37.742 ms | 43.080 ms |
| Direct Mach-O | 16.404 ms | 20.500 ms | 24.882 ms |
The p95 difference is 17.242 ms. A 60 Hz display interval is 16.667 ms.
At 100 Hz, the minimal Qt application:
| Launch method | avg | p95 | p99 |
|---|---|---|---|
| Open normally | 24.687 ms | 31.182 ms | 33.797 ms |
| Direct Mach-O | 16.295 ms | 20.623 ms | 23.286 ms |
The p95 difference is 10.559 ms. A 100 Hz display interval is 10.000 ms.
Xcode reproduces the behavior
The exact same launch-dependent behavior occurs in Xcode.
On a Mac mini running macOS Sequoia 15.7.5:
| Launch method | avg | p95 | p99 |
|---|---|---|---|
| Open normally | 23.433 ms | 29.886 ms | 31.877 ms |
| Direct Mach-O | 15.787 ms | 19.494 ms | 20.480 ms |
| Differences | ~7.6 ms | ~10.4 ms | ~11.4 ms |
Reproduction on macOS Tahoe 26.6.1
The issue was tested on current-generation hardware and macOS:
- MacBook Air M4
- macOS Tahoe 26.6.1
The same launch-dependent difference remains with several unrelated editors that were tested on the same machine.
The absolute distributions differ substantially between editors, as would be expected from different rendering implementations. The consistent result is the direction of the effect:
- Launch via .app / open normally results in higher input-to-display latency
- Launch via direct Mach-O results in lower input-to-display latency
All four tested applications show the same relationship:
| Application | Open avg | Direct avg | Open p95 | Direct p95 | Open p99 | Direct p99 |
|---|---|---|---|---|---|---|
| Koi | 33.110 ms | 22.029 ms | 39.924 ms | 25.635 ms | 42.296 ms | 26.728 ms |
| BBEdit | 29.743 ms | 21.381 ms | 38.176 ms | 22.685 ms | 45.684 ms | 23.812 ms |
| Xcode | 35.830 ms | 23.639 ms | 40.122 ms | 27.233 ms | 53.996 ms | 37.855 ms |
| Sublime Text | 35.253 ms | 22.356 ms | 56.696 ms | 34.390 ms | 61.820 ms | 35.505 ms |
The absolute values vary by application. The relevant result here is that every tested application is faster when its bundled executable is run directly.
Zed was also tested on the Mac mini and showed the same direction of effect, where p95 increased from 27.514 ms when run directly to 48.208 ms when launched normally.
Investigating the process launch context
Further testing showed that the distinction is more specific than normal application launch versus direct Mach-O execution.
Direct execution is not sufficient
Initial testing suggested a simple distinction:
- launching the app normally was slow
- executing its bundled Mach-O directly was fast
This does not hold for every launch context.
The same QtLaunchTest executable was:
- fast when executed directly from Apple Terminal
- slow when executed directly from Ghostty
- slow when executed through an SSH connection to localhost
The relevant distinction is therefore not simply LaunchServices versus direct execution.
Apple Terminal and Ghostty produce different results
Apple Terminal and Ghostty provide a useful comparison because both can execute exactly the same Mach-O directly from a shell:
dist/QtLaunchTest.app/Contents/MacOS/QtLaunchTest
From Apple Terminal this was consistently fast.
From Ghostty it was consistently slow.
The executable and application bundle were unchanged. This suggests that some property inherited from the launching process affects the resulting latency state.
TTY and session state are not responsible
A process launched interactively from Terminal normally has a controlling TTY and remains associated with the Terminal process hierarchy.
To test this, QtLaunchTest was launched through an intermediary that detached it and established a separate process session.
The resulting process had:
PPID = 1
no controlling TTY
session leader
It remained fast.
The executable also remained fast when detached using nohup and disown.
The lower-latency state therefore does not require an attached terminal, controlling TTY, Terminal as the immediate parent, or membership in the same Unix process session.
Launch context survives fork and exec
A small launcher started from Apple Terminal spawned QtLaunchTest using fork and exec.
The resulting application remained fast:
- Apple Terminal
- launcher
- fork / exec
- QtLaunchTest
A separate launcher started normally through LaunchServices replaced itself with the same 'QtLaunchTest' executable using 'exec()'.
That application remained slow:
- LaunchServices
- launcher.app
- exec()
- QtLaunchTest
The relevant state therefore appears to be established before the final executable starts and survives 'exec()'.
RunningBoard application state
launchctl procinfo exposes several differences between the two process contexts.
A representative slow LaunchServices process reported:
managed_by = com.apple.runningboard
spawn type = app (1)
spawn role = ui (2)
jetsam priority = 100
started suspended = 1
trampolined = 1
It also had an application-specific XPC service:
XPC_SERVICE_NAME = application.ai.hackerman.qtlaunchtest...
XPC_FLAGS = 1
A fast process executed from Apple Terminal was not managed as a RunningBoard application job and reported:
jetsam priority = 180
Changing environment variables or the bundle identifier did not change the latency state.
Fast and slow processes also reported the same Mach task QoS:
TASK_BASE_QOS_POLICY:
latency = 0xff0001
throughput = 0xfe0001
TASK_OVERRIDE_QOS_POLICY:
latency = 0x0
throughput = 0x0
None of these individual differences has been shown to cause the latency difference.
Process coalitions
Process coalition membership provided another observable difference.
Apple Terminal and 'QtLaunchTest' launched from it belonged to the same coalitions:
resource coalition: 649
jetsam coalition: 650
The intermediate 'fork' and 'exec' test inherited the same coalitions and remained fast.
Ghostty and 'QtLaunchTest' launched from it also shared coalitions:
resource coalition: 10587
jetsam coalition: 10915
This case was slow.
A normal LaunchServices launch received separate application coalitions and was also slow:
resource coalition: 10926
jetsam coalition: 10927
The observed relationship was:
| Launch context | Resource coalition | Jetsam coalition | Result |
|---|---|---|---|
| Apple Terminal, QtLaunchTest | 649 | 650 | Fast |
| Apple Terminal, intermediary, QtLaunchTest | 649 | 650 | Fast |
| Ghostty, QtLaunchTest | 10587 | 10915 | Slow |
| LaunchServices, QtLaunchTest | 10926 | 10927 | Slow |
The coalition IDs are specific to these processes and have no significance by themselves.
The same inheritance behavior was confirmed with VLC, where direct execution from Apple Terminal inherited Terminal's coalitions, while a normal application launch received separate application coalitions.
Coalition membership correlates with latency
In the tests above:
- QtLaunchTest in Apple Terminal's coalition was fast
- QtLaunchTest in Ghostty's coalition was slow
- QtLaunchTest in its LaunchServices application coalition was slow
This does not establish coalition membership as the cause.
A controlled experiment was attempted by creating a new coalition from the fast Terminal context and spawning QtLaunchTest into it, but macOS rejected coalition creation:
coalition_create(resource): Operation not permitted
Coalition membership is therefore currently an observable marker of the different process contexts, not an identified mechanism.
Physical mouse activity reduces latency
Physical mouse movement substantially reduces the measured input-to-display latency while the mouse is moving.
On the Mac mini at 100 Hz:
| State | avg | p95 | p99 |
|---|---|---|---|
| Mouse moving | 14.656 ms | 18.030 ms | 23.509 ms |
| Mouse idle | 27.446 ms | 36.406 ms | 38.112 ms |
At 50 Hz:
| State | avg | p95 | p99 |
|---|---|---|---|
| Mouse moving | 16.614 ms | 20.132 ms | 22.807 ms |
| Mouse idle | 27.827 ms | 34.773 ms | 38.298 ms |
The transition occurs quickly. Stopping physical mouse movement returns the application to the higher-latency state.
The effect does not require visible cursor movement. Using CGAssociateMouseAndMouseCursorPosition(false) to decouple the mouse from the cursor did not change the result. Physical mouse movement continued to reduce latency while the cursor remained stationary.
Several related tests did not reproduce the effect:
- trackpad movement
- scrolling
- synthetic mouse movement
- synthetic cursor warping
- forcing the cursor to remain visible
- continuous Qt timer activity
- polling the AppKit event queue
- continuous Qt repaint requests
- repeated NSWindow.displayIfNeeded()
- NSProcessInfo latency-critical activity
- increased GPU activity
The physical mouse result was also reproduced on the MacBook Air. Trackpad movement did not produce the same effect, while movement from a physical mouse did.
System Trace shows substantially increased WindowServer, NSEvent, and Core Animation activity during physical mouse movement. This activity is not required for the lower-latency state, however. An application launched from Apple Terminal remains fast while the mouse is idle and Core Animation activity is sparse.
What the tests rule out
The tests make several initially plausible explanations unlikely:
- Koi-specific editor code, custom lexers, or configuration
- QScintilla accessibility
- Qt input-method handling
- Qt or Python generally, because Xcode and other unrelated applications reproduce the effect
- direct Mach-O execution itself
- shell environment
- bundle identifier
- terminal attachment or controlling TTY
- parent process or Unix session state
- process QoS
- final AppKit activation state
- repaint size or application-side paint cost
- a single machine or Sequoia-specific installation
- cursor visibility or visible cursor movement
- generic application run-loop activity
- continuous application repainting
- AppKit displayIfNeeded() activity
- NSProcessInfo latency-critical activity
- a simple GPU power or utilization difference
Current working hypothesis
The evidence currently points toward inherited macOS process state affecting the presentation pipeline.
The observed pipeline appears approximately to be:
- keyboard input
- application event processing
- application rendering
- application paint/update completes
- AppKit / Core Animation / WindowServer
- display presentation
- display scanout
- visible pixel change
The launch-dependent difference is not present in the application-side timing measured through paintEvent(), which suggests that the additional delay occurs later in the presentation path. The refresh-rate experiment is consistent with the slower state reaching the display one presentation opportunity later.
A simplified conceptual model would be:
-
Lower-latency state
- input
- application update
- paint
- visible on presentation N
-
Higher-latency state
- input
- application update
- paint
- visible on presentation N+1
This is a model consistent with the measurements, not a directly observed presentation sequence.
The process-context experiments show that the latency state can be inherited across fork and exec. The physical mouse experiments show that it can also change dynamically after the application has started.
RunningBoard state and process coalitions remain observable differences between the tested launch contexts, but neither has been demonstrated to cause the latency difference.
The current evidence points more generally toward macOS presentation or frame-scheduling state downstream of application painting.
Reproduction
A minimal reproduction can be constructed with:
import sys
from PyQt6.QtWidgets import QApplication, QPlainTextEdit
app = QApplication(sys.argv)
editor = QPlainTextEdit()
editor.resize(1200, 800)
editor.show()
sys.exit(app.exec())
Package it as a .app bundle with py2app:
QtLaunchTest.app
Then compare normal application launch with direct executable:
open -n dist/QtLaunchTest.app
dist/QtLaunchTest.app/Contents/MacOS/QtLaunchTest
Use the same external keypress.sh input-to-pixel benchmark for both cases. Each run contains 200 measurements and reports the average, p95, and p99 directly from those samples.
The experiment should ideally be repeated at multiple fixed display refresh rates.
Future work
Areas worth investigating include:
- Core Animation transaction and presentation timing
- WindowServer frame scheduling
- physical mouse HID activity and its effect on presentation scheduling
- RunningBoard application and lifecycle state
- process coalition policy
- differences in WindowServer connections
- other inherited process state that survives exec()
System Trace should be used to compare individual benchmark updates between the slow normal-launch state, the fast Terminal-launch state, and the fast physical-mouse-active state.
A particularly useful measurement would identify when the application update is committed and when the corresponding content becomes eligible for display presentation.
Conclusion
This investigation began as an apparent performance regression in Koi Editor.
A minimal Qt application then reproduced the same launch-dependent behavior.
Changing display refresh rate provided the first strong clue about the mechanism. In the minimal reproduction, the p95 penalty changed from 17.242 ms at 60 Hz to 10.559 ms at 100 Hz, closely corresponding to the respective frame intervals of 16.667 ms and 10 ms.
On macOS Sequoia 15.7.5, Xcode measured:
| Launch method | p95 |
|---|---|
| Open normally | 29.886 ms |
| Direct Mach-O | 19.494 ms |
The same phenomenon remains present on macOS Tahoe 26.6.1 on a MacBook Air M4:
| Launch method | p95 |
|---|---|
| Open normally | 40.122 ms |
| Direct Mach-O | 27.233 ms |
On that same Tahoe machine, Koi, BBEdit, Xcode, and Sublime Text all showed lower input-to-pixel latency when their bundled executables were run directly. Zed showed the same behavior in the earlier Mac mini tests.
Further testing showed that direct execution itself is not sufficient to produce the lower-latency state. The same QtLaunchTest executable was fast when launched directly from Apple Terminal and slow when launched directly from Ghostty.
The state survives fork, exec, terminal detachment, and Unix session changes. Processes launched from Apple Terminal and Ghostty inherit their respective process coalitions, while normal LaunchServices launches receive separate application coalitions.
A second way to reach the lower-latency state was found during physical mouse movement. A normally launched application becomes substantially faster while a physical mouse is moving and returns to the slower state shortly after movement stops. The effect does not require visible cursor movement and was reproduced on both tested Macs.
The strongest current evidence places the additional delay after application painting and somewhere in the macOS presentation path. The refresh-rate results remain consistent with visible updates in the slower state reaching the display approximately one presentation opportunity later.
The exact mechanism remains unknown.
Further investigation should concentrate on Core Animation and WindowServer presentation scheduling, including how launch context and physical mouse activity can affect that scheduling.