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.
This commit is contained in:
Mitchell Hashimoto
2026-08-09 15:42:09 -07:00
parent afc79b8ccf
commit c454a3bf47
2 changed files with 43 additions and 0 deletions

View File

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

View File

@@ -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 {