From c454a3bf47cd72945b7f4db3b53f8af332e167c9 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 9 Aug 2026 15:42:09 -0700 Subject: [PATCH] font: support warmup threads The first CoreText font query in a process initializes the system font database, which takes multiple milliseconds (~7ms measured in an isolated process; 2-4ms observed inside Ghostty startup). This cost was previously paid during the first surface's font grid initialization, on the critical path to the first window. App.create can now spawn a background thread that performs the warmup. --- src/App.zig | 27 +++++++++++++++++++++++++++ src/font/discovery.zig | 16 ++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/src/App.zig b/src/App.zig index cf7316e8c..8f749890f 100644 --- a/src/App.zig +++ b/src/App.zig @@ -78,9 +78,36 @@ pub fn create(alloc: Allocator) CreateError!*App { var app = try alloc.create(App); errdefer alloc.destroy(app); try app.init(alloc); + + // Warm up system services on background threads. These are + // multi-millisecond one-time costs that would otherwise be paid + // during the first surface's initialization. Doing it here overlaps + // them with the rest of app startup (config, app runtime, window + // creation). + threadedWarmup(); + return app; } +/// Warm up system services whose first use in a process is slow. Each +/// subsystem gets its own background thread so that one slow subsystem +/// doesn't delay the others. Only operations that are safe to run on +/// any thread belong here. +fn threadedWarmup() void { + // If font discovery supports warmup, then we call it. Some font + // mechanisms (e.g. CoreText) have a multi-millisecond one-time cost + // on startup. + if (comptime @hasDecl(font.Discover, "warmup")) { + if (std.Thread.spawn( + .{}, + font.Discover.warmup, + .{}, + )) |thr| thr.detach() else |err| { + log.warn("font warmup thread spawn failed err={}", .{err}); + } + } +} + /// Initialize the main app instance. This creates the main window, sets /// up the renderer state, compiles the shaders, etc. This is the primary /// "startup" logic. diff --git a/src/font/discovery.zig b/src/font/discovery.zig index 1e44026ce..1847693a6 100644 --- a/src/font/discovery.zig +++ b/src/font/discovery.zig @@ -350,6 +350,22 @@ pub const CoreText = struct { _ = self; } + /// Warm up the system font registry. + /// + /// The first CoreText query in a process initializes the system font + /// database, which takes multiple milliseconds, while subsequent + /// queries are microseconds. + pub fn warmup() void { + const name = macos.foundation.String.createWithBytes( + "AppleColorEmoji", + .utf8, + false, + ) catch return; + defer name.release(); + const ct_font = macos.text.Font.createWithName(name, 12) catch return; + ct_font.release(); + } + /// Discover fonts from a descriptor. This returns an iterator that can /// be used to build up the deferred fonts. pub fn discover(self: *const CoreText, alloc: Allocator, desc: Descriptor) !DiscoverIterator {