Hackerman, Inc.

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:

It has also been reproduced across:

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:

  1. The exact same bundled executable can exhibit either latency state depending on its launch context.
  2. Direct execution from Apple Terminal is fast, while direct execution from Ghostty is slow.
  3. The fast state survives detaching from Terminal, changing session state, fork, and exec.
  4. Normal LaunchServices launches are managed by RunningBoard and receive separate application coalitions.
  5. Processes launched from Apple Terminal or Ghostty inherit their launcher's coalitions.
  6. Coalition membership correlates with the latency state but has not been shown to cause it.
  7. A minimal Qt application reproduces the behavior.
  8. Application-side input and paint timing does not contain the externally measured delay.
  9. At 60 Hz and 100 Hz, the p95 difference closely follows one display interval.
  10. Physical mouse movement temporarily reduces the latency of a normally launched application.
  11. 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.
  12. 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.
  13. The behavior reproduces in Xcode and several unrelated editors across Sequoia and Tahoe.

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:

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:

  1. keyboard input
  2. application receives input
  3. application updates document
  4. application paints
  5. macOS composites/presents frame
  6. corresponding pixel changes on display
  7. 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:

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:

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:

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:

This does not hold for every launch context.

The same QtLaunchTest executable was:

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:

  1. Apple Terminal
  2. launcher
  3. fork / exec
  4. QtLaunchTest

A separate launcher started normally through LaunchServices replaced itself with the same 'QtLaunchTest' executable using 'exec()'.

That application remained slow:

  1. LaunchServices
  2. launcher.app
  3. exec()
  4. 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:

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:

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:

Current working hypothesis

The evidence currently points toward inherited macOS process state affecting the presentation pipeline.

The observed pipeline appears approximately to be:

  1. keyboard input
  2. application event processing
  3. application rendering
  4. application paint/update completes
  5. AppKit / Core Animation / WindowServer
  6. display presentation
  7. display scanout
  8. 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:

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:

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.