From d532a6e260960fd427e438ff55ff74f14edc518c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 20 Feb 2025 21:38:49 -0800 Subject: [PATCH] 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. --- build.zig.zon | 4 ++-- build.zig.zon.nix | 6 +++--- build.zig.zon.txt | 2 +- build.zig.zon2json-lock | 6 +++--- src/apprt/gtk/App.zig | 22 ++++++++++++++++++++++ src/cli/version.zig | 4 ++-- src/config/Config.zig | 35 +++++++++++++++++++++++++++++++++++ src/global.zig | 14 ++++++++++++-- src/main_ghostty.zig | 1 - src/os/cf_release_thread.zig | 4 ++-- src/renderer/Metal.zig | 2 +- src/renderer/Thread.zig | 2 +- src/termio/Exec.zig | 8 ++++---- src/termio/Options.zig | 2 +- src/termio/Termio.zig | 2 +- src/termio/Thread.zig | 4 ++-- src/termio/backend.zig | 2 +- src/termio/mailbox.zig | 2 +- src/termio/stream_handler.zig | 2 +- 19 files changed, 95 insertions(+), 29 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index cc617cf51..053a2f6d8 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -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 diff --git a/build.zig.zon.nix b/build.zig.zon.nix index e1eecdd3e..ecfab0900 100644 --- a/build.zig.zon.nix +++ b/build.zig.zon.nix @@ -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="; }; } { diff --git a/build.zig.zon.txt b/build.zig.zon.txt index 5eb530d76..2e5fbca92 100644 --- a/build.zig.zon.txt +++ b/build.zig.zon.txt @@ -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 diff --git a/build.zig.zon2json-lock b/build.zig.zon2json-lock index 348a69193..03eae0882 100644 --- a/build.zig.zon2json-lock +++ b/build.zig.zon2json-lock @@ -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", diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index 227c36ec4..d8fcaa74c 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -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, diff --git a/src/cli/version.zig b/src/cli/version.zig index f6d2ea9df..235cfe40b 100644 --- a/src/cli/version.zig +++ b/src/cli/version.zig @@ -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", .{}); diff --git a/src/config/Config.zig b/src/config/Config.zig index 60a95f33b..a5f5b56b3 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -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, diff --git a/src/global.zig b/src/global.zig index d5a7af630..b9af5983d 100644 --- a/src/global.zig +++ b/src/global.zig @@ -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(); diff --git a/src/main_ghostty.zig b/src/main_ghostty.zig index 9efe8d9b0..0e57fbc43 100644 --- a/src/main_ghostty.zig +++ b/src/main_ghostty.zig @@ -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"); diff --git a/src/os/cf_release_thread.zig b/src/os/cf_release_thread.zig index 5001441e0..dbf8e6592 100644 --- a/src/os/cf_release_thread.zig +++ b/src/os/cf_release_thread.zig @@ -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; diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index ca13f87de..e90963c75 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -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"); diff --git a/src/renderer/Thread.zig b/src/renderer/Thread.zig index cc63889fa..03b41ab30 100644 --- a/src/renderer/Thread.zig +++ b/src/renderer/Thread.zig @@ -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"); diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index 5a2d2a507..4f63076de 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -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. diff --git a/src/termio/Options.zig b/src/termio/Options.zig index 023423c95..7484fd087 100644 --- a/src/termio/Options.zig +++ b/src/termio/Options.zig @@ -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"); diff --git a/src/termio/Termio.zig b/src/termio/Termio.zig index 8a2e6cc7a..1d125f049 100644 --- a/src/termio/Termio.zig +++ b/src/termio/Termio.zig @@ -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"); diff --git a/src/termio/Thread.zig b/src/termio/Thread.zig index d80046737..d8018341d 100644 --- a/src/termio/Thread.zig +++ b/src/termio/Thread.zig @@ -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; diff --git a/src/termio/backend.zig b/src/termio/backend.zig index 68b283a00..46ed3431c 100644 --- a/src/termio/backend.zig +++ b/src/termio/backend.zig @@ -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"); diff --git a/src/termio/mailbox.zig b/src/termio/mailbox.zig index cac453a1c..b144b512a 100644 --- a/src/termio/mailbox.zig +++ b/src/termio/mailbox.zig @@ -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; diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index e9bb353fb..43d2888d2 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -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");