Update libxev to use dynamic backend, support Linux configurability

Related to #3224

Previously, Ghostty used a static API for async event handling: io_uring
on Linux, kqueue on macOS. This commit changes the backend to be dynamic
on Linux so that epoll will be used if io_uring isn't available, or if
the user explicitly chooses it.

This introduces a new config `async-backend` (default "auto") which can
be set by the user to change the async backend in use. This is a
best-effort setting: if the user requests io_uring but it isn't
available, Ghostty will fall back to something that is and that choice
is up to us.

Basic benchmarking both in libxev and Ghostty (vtebench) show no
noticeable performance differences introducing the dynamic API, nor
choosing epoll over io_uring.
This commit is contained in:
Mitchell Hashimoto
2025-02-20 21:38:49 -08:00
parent 38908e0126
commit d532a6e260
19 changed files with 95 additions and 29 deletions

View File

@ -7,8 +7,8 @@
.libxev = .{
// mitchellh/libxev
.url = "https://deps.files.ghostty.org/libxev-1220ebf88622c4d502dc59e71347e4d28c47e033f11b59aff774ae5787565c40999c.tar.gz",
.hash = "1220ebf88622c4d502dc59e71347e4d28c47e033f11b59aff774ae5787565c40999c",
.url = "https://github.com/mitchellh/libxev/archive/8943932a668f338cb2c500f6e1a7396bacd8b55d.tar.gz",
.hash = "1220a67b584c9499154de8c96851ed8e92315452cb2027c06e2d7d07a39c6f900d1a",
},
.mach_glfw = .{
// mitchellh/mach-glfw

6
build.zig.zon.nix generated
View File

@ -84,11 +84,11 @@ with lib; let
in
linkFarm name [
{
name = "1220ebf88622c4d502dc59e71347e4d28c47e033f11b59aff774ae5787565c40999c";
name = "1220a67b584c9499154de8c96851ed8e92315452cb2027c06e2d7d07a39c6f900d1a";
path = fetchZigArtifact {
name = "libxev";
url = "https://deps.files.ghostty.org/libxev-1220ebf88622c4d502dc59e71347e4d28c47e033f11b59aff774ae5787565c40999c.tar.gz";
hash = "sha256-VHP90NTytIZ8UZsYRKOOxN490/I6yv6ec40sP8y5MJ8=";
url = "https://github.com/mitchellh/libxev/archive/8943932a668f338cb2c500f6e1a7396bacd8b55d.tar.gz";
hash = "sha256-TGooUoby2J8PyzbdKHwdEXnu7f2g4T2/TUHj/ktBsN4=";
};
}
{

2
build.zig.zon.txt generated
View File

@ -10,7 +10,6 @@ https://deps.files.ghostty.org/harfbuzz-1220b8588f106c996af10249bfa092c6fb2f35fb
https://deps.files.ghostty.org/highway-12205c83b8311a24b1d5ae6d21640df04f4b0726e314337c043cde1432758cbe165b.tar.gz
https://deps.files.ghostty.org/imgui-1220bc6b9daceaf7c8c60f3c3998058045ba0c5c5f48ae255ff97776d9cd8bfc6402.tar.gz
https://deps.files.ghostty.org/libpng-1220aa013f0c83da3fb64ea6d327f9173fa008d10e28bc9349eac3463457723b1c66.tar.gz
https://deps.files.ghostty.org/libxev-1220ebf88622c4d502dc59e71347e4d28c47e033f11b59aff774ae5787565c40999c.tar.gz
https://deps.files.ghostty.org/mach_glfw-12206ed982e709e565d536ce930701a8c07edfd2cfdce428683f3f2a601d37696a62.tar.gz
https://deps.files.ghostty.org/oniguruma-1220c15e72eadd0d9085a8af134904d9a0f5dfcbed5f606ad60edc60ebeccd9706bb.tar.gz
https://deps.files.ghostty.org/pixels-12207ff340169c7d40c570b4b6a97db614fe47e0d83b5801a932dcd44917424c8806.tar.gz
@ -32,6 +31,7 @@ https://github.com/getsentry/breakpad/archive/b99f444ba5f6b98cac261cbb391d8766b3
https://github.com/GNOME/libxml2/archive/refs/tags/v2.11.5.tar.gz
https://github.com/mbadolato/iTerm2-Color-Schemes/archive/efb1bb1843500a751eb30afa58fe48a6bec8952c.tar.gz
https://github.com/mitchellh/glfw/archive/b552c6ec47326b94015feddb36058ea567b87159.tar.gz
https://github.com/mitchellh/libxev/archive/8943932a668f338cb2c500f6e1a7396bacd8b55d.tar.gz
https://github.com/mitchellh/vulkan-headers/archive/04c8a0389d5a0236a96312988017cd4ce27d8041.tar.gz
https://github.com/mitchellh/wayland-headers/archive/5f991515a29f994d87b908115a2ab0b899474bd1.tar.gz
https://github.com/mitchellh/x11-headers/archive/2ffbd62d82ff73ec929dd8de802bc95effa0ef88.tar.gz

View File

@ -1,8 +1,8 @@
{
"1220ebf88622c4d502dc59e71347e4d28c47e033f11b59aff774ae5787565c40999c": {
"1220a67b584c9499154de8c96851ed8e92315452cb2027c06e2d7d07a39c6f900d1a": {
"name": "libxev",
"url": "https://deps.files.ghostty.org/libxev-1220ebf88622c4d502dc59e71347e4d28c47e033f11b59aff774ae5787565c40999c.tar.gz",
"hash": "sha256-VHP90NTytIZ8UZsYRKOOxN490/I6yv6ec40sP8y5MJ8="
"url": "https://github.com/mitchellh/libxev/archive/8943932a668f338cb2c500f6e1a7396bacd8b55d.tar.gz",
"hash": "sha256-TGooUoby2J8PyzbdKHwdEXnu7f2g4T2/TUHj/ktBsN4="
},
"12206ed982e709e565d536ce930701a8c07edfd2cfdce428683f3f2a601d37696a62": {
"name": "mach_glfw",

View File

@ -15,6 +15,7 @@ const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const builtin = @import("builtin");
const build_config = @import("../../build_config.zig");
const xev = @import("../../global.zig").xev;
const build_options = @import("build_options");
const apprt = @import("../../apprt.zig");
const configpkg = @import("../../config.zig");
@ -137,6 +138,27 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
}
}
// Setup our event loop backend
if (config.@"async-backend" != .auto) {
const result: bool = switch (config.@"async-backend") {
.auto => unreachable,
.epoll => xev.prefer(.epoll),
.io_uring => xev.prefer(.io_uring),
};
if (result) {
log.info(
"libxev manual backend={s}",
.{@tagName(xev.backend)},
);
} else {
log.warn(
"libxev manual backend failed, using default={s}",
.{@tagName(xev.backend)},
);
}
}
var gdk_debug: struct {
/// output OpenGL debug information
opengl: bool = false,

View File

@ -4,7 +4,7 @@ const Allocator = std.mem.Allocator;
const builtin = @import("builtin");
const build_config = @import("../build_config.zig");
const internal_os = @import("../os/main.zig");
const xev = @import("xev");
const xev = @import("../global.zig").xev;
const renderer = @import("../renderer.zig");
const gtk = if (build_config.app_runtime == .gtk) @import("../apprt/gtk/c.zig").c else void;
@ -37,7 +37,7 @@ pub fn run(alloc: Allocator) !u8 {
try stdout.print(" - app runtime: {}\n", .{build_config.app_runtime});
try stdout.print(" - font engine: {}\n", .{build_config.font_backend});
try stdout.print(" - renderer : {}\n", .{renderer.Renderer});
try stdout.print(" - libxev : {}\n", .{xev.backend});
try stdout.print(" - libxev : {s}\n", .{@tagName(xev.backend)});
if (comptime build_config.app_runtime == .gtk) {
try stdout.print(" - desktop env: {s}\n", .{@tagName(internal_os.desktopEnvironment())});
try stdout.print(" - GTK version:\n", .{});

View File

@ -2235,6 +2235,34 @@ term: []const u8 = "xterm-ghostty",
/// running. Defaults to an empty string if not set.
@"enquiry-response": []const u8 = "",
/// Configures the low-level API to use for async IO, eventing, etc.
///
/// Most users should leave this set to `auto`. This will automatically detect
/// scenarios where APIs may not be available (for example `io_uring` on
/// certain hardened kernels) and fall back to a different API. However, if
/// you want to force a specific backend for any reason, you can set this
/// here.
///
/// Based on various benchmarks, we haven't found a statistically significant
/// difference between the backends with regards to memory, CPU, or latency.
/// The choice of backend is more about compatibility and features.
///
/// Available options:
///
/// * `auto` - Automatically choose the best backend for the platform
/// based on available options.
/// * `epoll` - Use the `epoll` API
/// * `io_uring` - Use the `io_uring` API
///
/// If the selected backend is not available on the platform, Ghostty will
/// fall back to an automatically chosen backend that is available.
///
/// Changing this value requires a full application restart to take effect.
///
/// This is only supported on Linux, since this is the only platform
/// where we have multiple options. On macOS, we always use `kqueue`.
@"async-backend": AsyncBackend = .auto,
/// Control the auto-update functionality of Ghostty. This is only supported
/// on macOS currently, since Linux builds are distributed via package
/// managers that are not centrally controlled by Ghostty.
@ -5912,6 +5940,13 @@ pub const LinuxCgroup = enum {
@"single-instance",
};
/// See async-backend
pub const AsyncBackend = enum {
auto,
epoll,
io_uring,
};
/// See auto-updates
pub const AutoUpdate = enum {
off,

View File

@ -9,7 +9,11 @@ const harfbuzz = @import("harfbuzz");
const oni = @import("oniguruma");
const crash = @import("crash/main.zig");
const renderer = @import("renderer.zig");
const xev = @import("xev");
/// We export the xev backend we want to use so that the rest of
/// Ghostty can import this once and have access to the proper
/// backend.
pub const xev = @import("xev").Dynamic;
/// Global process state. This is initialized in main() for exe artifacts
/// and by ghostty_init() for lib artifacts. This should ONLY be used by
@ -114,6 +118,12 @@ pub const GlobalState = struct {
// Setup our signal handlers before logging
initSignals();
// Setup our Xev backend if we're dynamic
if (comptime xev.dynamic) xev.detect() catch |err| {
std.log.warn("failed to detect xev backend, falling back to " ++
"most compatible backend err={}", .{err});
};
// Output some debug information right away
std.log.info("ghostty version={s}", .{build_config.version_string});
std.log.info("ghostty build optimize={s}", .{build_config.mode_string});
@ -126,7 +136,7 @@ pub const GlobalState = struct {
std.log.info("dependency fontconfig={d}", .{fontconfig.version()});
}
std.log.info("renderer={}", .{renderer.Renderer});
std.log.info("libxev backend={}", .{xev.backend});
std.log.info("libxev default backend={s}", .{@tagName(xev.backend)});
// As early as possible, initialize our resource limits.
self.rlimits = ResourceLimits.init();

View File

@ -13,7 +13,6 @@ const macos = @import("macos");
const oni = @import("oniguruma");
const cli = @import("cli.zig");
const internal_os = @import("os/main.zig");
const xev = @import("xev");
const fontconfig = @import("fontconfig");
const harfbuzz = @import("harfbuzz");
const renderer = @import("renderer.zig");

View File

@ -6,9 +6,9 @@ pub const Thread = @This();
const std = @import("std");
const builtin = @import("builtin");
const xev = @import("xev");
const macos = @import("macos");
const xev = @import("../global.zig").xev;
const BlockingQueue = @import("../datastruct/main.zig").BlockingQueue;
const Allocator = std.mem.Allocator;
@ -106,7 +106,7 @@ pub fn threadMain(self: *Thread) void {
// If our loop is not stopped, then we need to keep running so that
// messages are drained and we can wait for the surface to send a stop
// message.
if (!self.loop.flags.stopped) {
if (!self.loop.stopped()) {
log.warn("abrupt cf release thread exit detected, starting xev to drain mailbox", .{});
defer log.debug("cf release thread fully exiting after abnormal failure", .{});
self.flags.drain = true;

View File

@ -11,7 +11,7 @@ const objc = @import("objc");
const macos = @import("macos");
const imgui = @import("imgui");
const glslang = @import("glslang");
const xev = @import("xev");
const xev = @import("../global.zig").xev;
const apprt = @import("../apprt.zig");
const configpkg = @import("../config.zig");
const font = @import("../font/main.zig");

View File

@ -5,7 +5,7 @@ pub const Thread = @This();
const std = @import("std");
const builtin = @import("builtin");
const assert = std.debug.assert;
const xev = @import("xev");
const xev = @import("../global.zig").xev;
const crash = @import("../crash/main.zig");
const internal_os = @import("../os/main.zig");
const renderer = @import("../renderer.zig");

View File

@ -9,7 +9,7 @@ const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const posix = std.posix;
const xev = @import("xev");
const xev = @import("../global.zig").xev;
const build_config = @import("../build_config.zig");
const configpkg = @import("../config.zig");
const crash = @import("../crash/main.zig");
@ -589,7 +589,7 @@ fn ttyWrite(
_: *xev.Completion,
_: xev.Stream,
_: xev.WriteBuffer,
r: xev.Stream.WriteError!usize,
r: xev.WriteError!usize,
) xev.CallbackAction {
const td = td_.?;
td.write_req_pool.put();
@ -634,13 +634,13 @@ pub const ThreadData = struct {
/// This is the pool of available (unused) write requests. If you grab
/// one from the pool, you must put it back when you're done!
write_req_pool: SegmentedPool(xev.Stream.WriteRequest, WRITE_REQ_PREALLOC) = .{},
write_req_pool: SegmentedPool(xev.WriteRequest, WRITE_REQ_PREALLOC) = .{},
/// The pool of available buffers for writing to the pty.
write_buf_pool: SegmentedPool([64]u8, WRITE_REQ_PREALLOC) = .{},
/// The write queue for the data stream.
write_queue: xev.Stream.WriteQueue = .{},
write_queue: xev.WriteQueue = .{},
/// This is used for both waiting for the process to exit and then
/// subsequently to wait for the data_stream to close.

View File

@ -1,7 +1,7 @@
//! The options that are used to configure a terminal IO implementation.
const builtin = @import("builtin");
const xev = @import("xev");
const xev = @import("../global.zig").xev;
const apprt = @import("../apprt.zig");
const renderer = @import("../renderer.zig");
const Command = @import("../Command.zig");

View File

@ -18,7 +18,7 @@ const Pty = @import("../pty.zig").Pty;
const StreamHandler = @import("stream_handler.zig").StreamHandler;
const terminal = @import("../terminal/main.zig");
const terminfo = @import("../terminfo/main.zig");
const xev = @import("xev");
const xev = @import("../global.zig").xev;
const renderer = @import("../renderer.zig");
const apprt = @import("../apprt.zig");
const fastmem = @import("../fastmem.zig");

View File

@ -14,7 +14,7 @@ pub const Thread = @This();
const std = @import("std");
const ArenaAllocator = std.heap.ArenaAllocator;
const builtin = @import("builtin");
const xev = @import("xev");
const xev = @import("../global.zig").xev;
const crash = @import("../crash/main.zig");
const termio = @import("../termio.zig");
const renderer = @import("../renderer.zig");
@ -189,7 +189,7 @@ pub fn threadMain(self: *Thread, io: *termio.Termio) void {
// If our loop is not stopped, then we need to keep running so that
// messages are drained and we can wait for the surface to send a stop
// message.
if (!self.loop.flags.stopped) {
if (!self.loop.stopped()) {
log.warn("abrupt io thread exit detected, starting xev to drain mailbox", .{});
defer log.debug("io thread fully exiting after abnormal failure", .{});
self.flags.drain = true;

View File

@ -3,7 +3,7 @@ const builtin = @import("builtin");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const posix = std.posix;
const xev = @import("xev");
const xev = @import("../global.zig").xev;
const build_config = @import("../build_config.zig");
const configpkg = @import("../config.zig");
const internal_os = @import("../os/main.zig");

View File

@ -2,7 +2,7 @@ const std = @import("std");
const builtin = @import("builtin");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const xev = @import("xev");
const xev = @import("../global.zig").xev;
const renderer = @import("../renderer.zig");
const termio = @import("../termio.zig");
const BlockingQueue = @import("../datastruct/main.zig").BlockingQueue;

View File

@ -2,7 +2,7 @@ const std = @import("std");
const builtin = @import("builtin");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const xev = @import("xev");
const xev = @import("../global.zig").xev;
const apprt = @import("../apprt.zig");
const build_config = @import("../build_config.zig");
const configpkg = @import("../config.zig");