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. Normal application launches consistently produce substantially higher latency.
open -n /Applications/Koi.app
/Applications/Koi.app/Contents/MacOS/Koi
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.9
- 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.
The current evidence therefore points toward a difference in macOS frame scheduling or presentation behavior established when an application is launched through LaunchServices.
The precise mechanism has not yet been identified.
Main observations so far:
- The exact same bundled executable is fast when launched directly and slower when the application bundle is launched normally.
- Direct execution remains fast even after detaching the process from Terminal.
- Different LaunchServices invocation forms still produce the slower behavior.
- Process QoS and final AppKit activation state are the same.
- 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.
- The same behavior reproduces in Xcode and several unrelated editors across Sequoia and Tahoe.
- 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
- 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 dist/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.9:
| 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 on macOS Tahoe 26.6.1.
| 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: p95 increased from 27.514 ms when run directly to 48.208 ms when launched normally.
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
- terminal attachment
- process QoS
- final AppKit activation state
- repaint size or application-side paint cost
- a single machine or Sequoia-specific installation
Current working hypothesis
The evidence currently points toward a distinction in the macOS presentation pipeline that is established when an application is launched through the normal application-launch path.
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 further suggests that the measurements are consistent with the slower path reaching the display one presentation opportunity later.
A simplified conceptual model would therefore be:
Direct Mach-O:
- input
- application update
- paint
- visible on presentation N
The normal launch:
- input
- application update
- paint
- visible on presentation N+1
This is a model consistent with the measurements, not a directly observed presentation sequence.
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 timing
- WindowServer frame scheduling
- display synchronization
- application presentation state
- AppKit window configuration
- LaunchServices application state
- process assertions managed by RunningBoard
- foreground/application lifecycle assertions
- differences in WindowServer connections
- differences in Core Animation commit scheduling
- macOS frame pacing
- presentation timing under fixed versus variable refresh rates
A purpose-built native AppKit or Metal reproduction could provide more direct instrumentation of transaction and presentation timing.
The Xcode results already demonstrate that the broad phenomenon is not dependent on Qt, but a minimal native test would make it easier to isolate the relevant macOS APIs.
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.9, 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 show lower input-to-pixel latency when their bundled executables are run directly. Zed showed the same behavior in the earlier Mac mini tests.
The strongest current hypothesis is that a normal macOS application launch establishes a different presentation or frame-scheduling state than direct execution of the same bundled Mach-O, causing visible updates under the tested conditions to reach the display approximately one presentation opportunity later.
The exact mechanism remains unknown, but the evidence suggests that further investigation should concentrate on LaunchServices/application lifecycle state, Core Animation, WindowServer, RunningBoard, and display presentation scheduling rather than application rendering performance.