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. 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:

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.

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:

  1. The exact same bundled executable is fast when launched directly and slower when the application bundle is launched normally.
  2. Direct execution remains fast even after detaching the process from Terminal.
  3. Different LaunchServices invocation forms still produce the slower behavior.
  4. Process QoS and final AppKit activation state are the same.
  5. A minimal Qt application reproduces the behavior.
  6. Application-side input and paint timing does not contain the externally measured delay.
  7. At 60 Hz and 100 Hz, the p95 difference closely follows one display interval.
  8. The same 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 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:

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:

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 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:

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:

  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 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:

  1. input
  2. application update
  3. paint
  4. visible on presentation N

The normal launch:

  1. input
  2. application update
  3. paint
  4. 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:

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.