apprt/embedded: initialize the TIS keymap lazily

The embedded apprt App init created the keyboard layout keymap
eagerly, which requires talking to the text input system (TIS). The
first TIS call in a process is slow: 6.6ms measured inside
ghostty_app_new during app launch (up to ~30ms in a cold process).

The keymap is only used for keyboard layout queries (option-as-alt
detection, layout change reload), which happen once keyboard events
are flowing. By then AppKit has already warmed TIS and the call is
effectively free (~0.2us measured warm). So initialize the keymap
lazily on first use. If the layout changes before the keymap was ever
created, reload is a no-op since lazy init picks up the current
layout.

Measured on macOS (Apple Silicon) with local timing instrumentation
during app launch:

  embedded app init before: ~6.7ms (keymap 6614us)
  embedded app init after:  ~60us (config clone only)
This commit is contained in:
Mitchell Hashimoto
2026-08-09 15:45:21 -07:00
parent de1336fadd
commit db6d20dce1

View File

@@ -120,7 +120,13 @@ pub const App = struct {
core_app: *CoreApp,
opts: Options,
keymap: input.Keymap,
/// The keyboard layout keymap. This is lazily initialized on first
/// use because creating it requires talking to the text input
/// system (TIS on macOS), and the first such call in a process is
/// slow (multiple milliseconds). It is only needed once keyboard
/// events start flowing, at which point the system is warm.
keymap: ?input.Keymap,
/// The configuration for the app. This is owned by this structure.
config: Config,
@@ -136,19 +142,16 @@ pub const App = struct {
var config_clone = try config.clone(alloc);
errdefer config_clone.deinit();
var keymap = try input.Keymap.init();
errdefer keymap.deinit();
self.* = .{
.core_app = core_app,
.config = config_clone,
.opts = opts,
.keymap = keymap,
.keymap = null,
};
}
pub fn terminate(self: *App) void {
self.keymap.deinit();
if (self.keymap) |*v| v.deinit();
self.config.deinit();
}
@@ -208,8 +211,10 @@ pub const App = struct {
/// This should be called whenever the keyboard layout was changed.
pub fn reloadKeymap(self: *App) !void {
// Reload the keymap
try self.keymap.reload();
// Reload the keymap. If it was never initialized we don't need
// to do anything since lazy initialization will pick up the
// current layout.
if (self.keymap) |*v| try v.reload();
}
/// Loads the keyboard layout.
@@ -217,13 +222,25 @@ pub const App = struct {
/// Kind of expensive so this should be avoided if possible. When I say
/// "kind of expensive" I mean that its not something you probably want
/// to run on every keypress.
pub fn keyboardLayout(self: *const App) input.KeyboardLayout {
pub fn keyboardLayout(self: *App) input.KeyboardLayout {
// We only support keyboard layout detection on macOS.
if (comptime builtin.os.tag != .macos) return .unknown;
// Lazily initialize the keymap.
const keymap: *input.Keymap = keymap: {
if (self.keymap == null) {
self.keymap = input.Keymap.init() catch |err| {
log.warn("error initializing keymap err={}", .{err});
return .unknown;
};
}
break :keymap &self.keymap.?;
};
// Any layout larger than this is not something we can handle.
var buf: [256]u8 = undefined;
const id = self.keymap.sourceId(&buf) catch |err| {
const id = keymap.sourceId(&buf) catch |err| {
comptime assert(@TypeOf(err) == error{OutOfMemory});
return .unknown;
};