aboutsummaryrefslogtreecommitdiff
path: root/vendor/libghostty-vt/src/benchmark
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-04-04 19:22:04 +0300
committerCan Celik <ogulcancelik@gmail.com>2026-04-05 01:23:06 +0300
commit41dd733ca7123c9ac4bfc6c781697a660e95b052 (patch)
tree3fc91815ca139ef2e0b281bba0abda4ef15175f4 /vendor/libghostty-vt/src/benchmark
parent55c138263e190a1e79d4b997bfb9397306c0ec1f (diff)
feat: finish ghostty pane backend integration
Diffstat (limited to 'vendor/libghostty-vt/src/benchmark')
-rw-r--r--vendor/libghostty-vt/src/benchmark/Benchmark.zig178
-rw-r--r--vendor/libghostty-vt/src/benchmark/CApi.zig34
-rw-r--r--vendor/libghostty-vt/src/benchmark/CodepointWidth.zig207
-rw-r--r--vendor/libghostty-vt/src/benchmark/GraphemeBreak.zig152
-rw-r--r--vendor/libghostty-vt/src/benchmark/IsSymbol.zig151
-rw-r--r--vendor/libghostty-vt/src/benchmark/OscParser.zig118
-rw-r--r--vendor/libghostty-vt/src/benchmark/ScreenClone.zig193
-rw-r--r--vendor/libghostty-vt/src/benchmark/TerminalParser.zig108
-rw-r--r--vendor/libghostty-vt/src/benchmark/TerminalStream.zig161
-rw-r--r--vendor/libghostty-vt/src/benchmark/cli.zig100
-rw-r--r--vendor/libghostty-vt/src/benchmark/main.zig13
-rw-r--r--vendor/libghostty-vt/src/benchmark/options.zig20
12 files changed, 1435 insertions, 0 deletions
diff --git a/vendor/libghostty-vt/src/benchmark/Benchmark.zig b/vendor/libghostty-vt/src/benchmark/Benchmark.zig
new file mode 100644
index 00000000..41c3695d
--- /dev/null
+++ b/vendor/libghostty-vt/src/benchmark/Benchmark.zig
@@ -0,0 +1,178 @@
+//! A single benchmark case.
+const Benchmark = @This();
+
+const std = @import("std");
+const builtin = @import("builtin");
+const assert = std.debug.assert;
+const macos = @import("macos");
+const build_config = @import("../build_config.zig");
+
+ptr: *anyopaque,
+vtable: VTable,
+
+/// Create a new benchmark from a pointer and a vtable.
+///
+/// This usually is only called by benchmark implementations, not
+/// benchmark users.
+pub fn init(
+ pointer: anytype,
+ vtable: VTable,
+) Benchmark {
+ const Ptr = @TypeOf(pointer);
+ assert(@typeInfo(Ptr) == .pointer); // Must be a pointer
+ assert(@typeInfo(Ptr).pointer.size == .one); // Must be a single-item pointer
+ assert(@typeInfo(@typeInfo(Ptr).pointer.child) == .@"struct"); // Must point to a struct
+ return .{ .ptr = pointer, .vtable = vtable };
+}
+
+/// Run the benchmark.
+pub fn run(
+ self: Benchmark,
+ mode: RunMode,
+) Error!RunResult {
+ // Run our setup function if it exists. We do this first because
+ // we don't want this part of our benchmark and we want to fail fast.
+ if (self.vtable.setupFn) |func| try func(self.ptr);
+ defer if (self.vtable.teardownFn) |func| func(self.ptr);
+
+ // Our result accumulator. This will be returned at the end of the run.
+ var result: RunResult = .{};
+
+ // If we're on macOS, we setup signposts so its easier to find
+ // the results in Instruments. There's a lot of nasty comptime stuff
+ // here but its just to ensure this does nothing on other platforms.
+ const signpost_name = "ghostty";
+ const signpost: if (builtin.target.os.tag.isDarwin()) struct {
+ log: *macos.os.Log,
+ id: macos.os.signpost.Id,
+ } else void = if (builtin.target.os.tag.isDarwin()) darwin: {
+ macos.os.signpost.init();
+ const log = macos.os.Log.create(
+ build_config.bundle_id,
+ macos.os.signpost.Category.points_of_interest,
+ );
+ const id = macos.os.signpost.Id.forPointer(log, self.ptr);
+ macos.os.signpost.intervalBegin(log, id, signpost_name);
+ break :darwin .{ .log = log, .id = id };
+ } else {};
+ defer if (comptime builtin.target.os.tag.isDarwin()) {
+ macos.os.signpost.intervalEnd(
+ signpost.log,
+ signpost.id,
+ signpost_name,
+ );
+ signpost.log.release();
+ };
+
+ const start = std.time.Instant.now() catch return error.BenchmarkFailed;
+ while (true) {
+ // Run our step function. If it fails, we return the error.
+ try self.vtable.stepFn(self.ptr);
+ result.iterations += 1;
+
+ // Get our current monotonic time and check our exit conditions.
+ const now = std.time.Instant.now() catch return error.BenchmarkFailed;
+ const exit = switch (mode) {
+ .once => true,
+ .duration => |ns| now.since(start) >= ns,
+ };
+
+ if (exit) {
+ result.duration = now.since(start);
+ return result;
+ }
+ }
+
+ // We exit within the loop body.
+ unreachable;
+}
+
+/// The type of benchmark run. This is used to determine how the benchmark
+/// is executed.
+pub const RunMode = union(enum) {
+ /// Run the benchmark exactly once.
+ once,
+
+ /// Run the benchmark for a fixed duration in nanoseconds. This
+ /// will not interrupt a running step so if the granularity of the
+ /// duration is too low, benchmark results may be inaccurate.
+ duration: u64,
+};
+
+/// The result of a benchmark run.
+pub const RunResult = struct {
+ /// The total iterations that step was executed. For "once" run
+ /// modes this will always be 1.
+ iterations: u32 = 0,
+
+ /// The total time taken for the run. For "duration" run modes
+ /// this will be relatively close to the requested duration.
+ /// The units are nanoseconds.
+ duration: u64 = 0,
+};
+
+/// The possible errors that can occur during various stages of the
+/// benchmark. Right now its just "failure" which ends the benchmark.
+pub const Error = error{BenchmarkFailed};
+
+/// The vtable that must be provided to invoke the real implementation.
+pub const VTable = struct {
+ /// A single step to execute the benchmark. This should do the work
+ /// that is under test. This may be called multiple times if we're
+ /// testing throughput.
+ stepFn: *const fn (ptr: *anyopaque) Error!void,
+
+ /// Setup and teardown functions. These are called once before
+ /// the first step and once after the last step. They are not part
+ /// of the benchmark results (unless you're benchmarking the full
+ /// binary).
+ setupFn: ?*const fn (ptr: *anyopaque) Error!void = null,
+ teardownFn: ?*const fn (ptr: *anyopaque) void = null,
+};
+
+test Benchmark {
+ // This test fails on FreeBSD and Windows so skip:
+ //
+ // /home/runner/work/ghostty/ghostty/src/benchmark/Benchmark.zig:165:5: 0x3cd2de1 in decltest.Benchmark (ghostty-test)
+ // try testing.expect(result.duration > 0);
+ // ^
+ switch (builtin.os.tag) {
+ .freebsd,
+ .windows,
+ => return error.SkipZigTest,
+ else => {},
+ }
+
+ const testing = std.testing;
+ const Simple = struct {
+ const Self = @This();
+
+ setup_i: usize = 0,
+ step_i: usize = 0,
+
+ pub fn benchmark(self: *Self) Benchmark {
+ return .init(self, .{
+ .stepFn = step,
+ .setupFn = setup,
+ });
+ }
+
+ fn setup(ptr: *anyopaque) Error!void {
+ const self: *Self = @ptrCast(@alignCast(ptr));
+ self.setup_i += 1;
+ }
+
+ fn step(ptr: *anyopaque) Error!void {
+ const self: *Self = @ptrCast(@alignCast(ptr));
+ self.step_i += 1;
+ }
+ };
+
+ var s: Simple = .{};
+ const b = s.benchmark();
+ const result = try b.run(.once);
+ try testing.expectEqual(1, s.setup_i);
+ try testing.expectEqual(1, s.step_i);
+ try testing.expectEqual(1, result.iterations);
+ try testing.expect(result.duration > 0);
+}
diff --git a/vendor/libghostty-vt/src/benchmark/CApi.zig b/vendor/libghostty-vt/src/benchmark/CApi.zig
new file mode 100644
index 00000000..3bef8b26
--- /dev/null
+++ b/vendor/libghostty-vt/src/benchmark/CApi.zig
@@ -0,0 +1,34 @@
+const std = @import("std");
+const cli = @import("cli.zig");
+const state = &@import("../global.zig").state;
+
+const log = std.log.scoped(.benchmark);
+
+/// Run the Ghostty benchmark CLI with the given action and arguments.
+export fn ghostty_benchmark_cli(
+ action_name_: [*:0]const u8,
+ args: [*:0]const u8,
+) bool {
+ const action_name = std.mem.sliceTo(action_name_, 0);
+ const action: cli.Action = std.meta.stringToEnum(
+ cli.Action,
+ action_name,
+ ) orelse {
+ log.warn("unknown action={s}", .{action_name});
+ return false;
+ };
+
+ cli.mainAction(
+ state.alloc,
+ action,
+ .{ .string = std.mem.sliceTo(args, 0) },
+ ) catch |err| {
+ log.warn("failed to run action={s} err={}", .{
+ @tagName(action),
+ err,
+ });
+ return false;
+ };
+
+ return true;
+}
diff --git a/vendor/libghostty-vt/src/benchmark/CodepointWidth.zig b/vendor/libghostty-vt/src/benchmark/CodepointWidth.zig
new file mode 100644
index 00000000..30d3f91e
--- /dev/null
+++ b/vendor/libghostty-vt/src/benchmark/CodepointWidth.zig
@@ -0,0 +1,207 @@
+//! This benchmark tests the throughput of codepoint width calculation.
+//! This is a common operation in terminal character printing and the
+//! motivating factor to write this benchmark was discovering that our
+//! codepoint width function was 30% of the runtime of every character
+//! print.
+const CodepointWidth = @This();
+
+const std = @import("std");
+const builtin = @import("builtin");
+const assert = std.debug.assert;
+const Allocator = std.mem.Allocator;
+const Benchmark = @import("Benchmark.zig");
+const options = @import("options.zig");
+const UTF8Decoder = @import("../terminal/UTF8Decoder.zig");
+const simd = @import("../simd/main.zig");
+const table = @import("../unicode/main.zig").table;
+
+const log = std.log.scoped(.@"terminal-stream-bench");
+
+opts: Options,
+
+/// The file, opened in the setup function.
+data_f: ?std.fs.File = null,
+
+pub const Options = struct {
+ /// The type of codepoint width calculation to use.
+ mode: Mode = .noop,
+
+ /// The data to read as a filepath. If this is "-" then
+ /// we will read stdin. If this is unset, then we will
+ /// do nothing (benchmark is a noop). It'd be more unixy to
+ /// use stdin by default but I find that a hanging CLI command
+ /// with no interaction is a bit annoying.
+ data: ?[]const u8 = null,
+};
+
+pub const Mode = enum {
+ /// The baseline mode copies the data from the fd into a buffer. This
+ /// is used to show the minimal overhead of reading the fd into memory
+ /// and establishes a baseline for the other modes.
+ noop,
+
+ /// libc wcwidth
+ wcwidth,
+
+ /// Our SIMD implementation.
+ simd,
+
+ /// Test our lookup table implementation.
+ table,
+};
+
+/// Create a new terminal stream handler for the given arguments.
+pub fn create(
+ alloc: Allocator,
+ opts: Options,
+) !*CodepointWidth {
+ const ptr = try alloc.create(CodepointWidth);
+ errdefer alloc.destroy(ptr);
+ ptr.* = .{ .opts = opts };
+ return ptr;
+}
+
+pub fn destroy(self: *CodepointWidth, alloc: Allocator) void {
+ alloc.destroy(self);
+}
+
+pub fn benchmark(self: *CodepointWidth) Benchmark {
+ return .init(self, .{
+ .stepFn = switch (self.opts.mode) {
+ .noop => stepNoop,
+ .wcwidth => stepWcwidth,
+ .table => stepTable,
+ .simd => stepSimd,
+ },
+ .setupFn = setup,
+ .teardownFn = teardown,
+ });
+}
+
+fn setup(ptr: *anyopaque) Benchmark.Error!void {
+ const self: *CodepointWidth = @ptrCast(@alignCast(ptr));
+
+ // Open our data file to prepare for reading. We can do more
+ // validation here eventually.
+ assert(self.data_f == null);
+ self.data_f = options.dataFile(self.opts.data) catch |err| {
+ log.warn("error opening data file err={}", .{err});
+ return error.BenchmarkFailed;
+ };
+}
+
+fn teardown(ptr: *anyopaque) void {
+ const self: *CodepointWidth = @ptrCast(@alignCast(ptr));
+ if (self.data_f) |f| {
+ f.close();
+ self.data_f = null;
+ }
+}
+
+fn stepNoop(ptr: *anyopaque) Benchmark.Error!void {
+ _ = ptr;
+}
+
+extern "c" fn wcwidth(c: u32) c_int;
+
+fn stepWcwidth(ptr: *anyopaque) Benchmark.Error!void {
+ if (comptime builtin.os.tag == .windows) {
+ log.warn("wcwidth is not available on Windows", .{});
+ return;
+ }
+
+ const self: *CodepointWidth = @ptrCast(@alignCast(ptr));
+
+ const f = self.data_f orelse return;
+ var read_buf: [4096]u8 align(std.atomic.cache_line) = undefined;
+ var f_reader = f.reader(&read_buf);
+ var r = &f_reader.interface;
+
+ var d: UTF8Decoder = .{};
+ var buf: [4096]u8 align(std.atomic.cache_line) = undefined;
+ while (true) {
+ const n = r.readSliceShort(&buf) catch {
+ log.warn("error reading data file err={?}", .{f_reader.err});
+ return error.BenchmarkFailed;
+ };
+ if (n == 0) break; // EOF reached
+
+ for (buf[0..n]) |c| {
+ const cp_, const consumed = d.next(c);
+ assert(consumed);
+ if (cp_) |cp| {
+ std.mem.doNotOptimizeAway(wcwidth(cp));
+ }
+ }
+ }
+}
+
+fn stepTable(ptr: *anyopaque) Benchmark.Error!void {
+ const self: *CodepointWidth = @ptrCast(@alignCast(ptr));
+
+ const f = self.data_f orelse return;
+ var read_buf: [4096]u8 align(std.atomic.cache_line) = undefined;
+ var f_reader = f.reader(&read_buf);
+ var r = &f_reader.interface;
+
+ var d: UTF8Decoder = .{};
+ var buf: [4096]u8 align(std.atomic.cache_line) = undefined;
+ while (true) {
+ const n = r.readSliceShort(&buf) catch {
+ log.warn("error reading data file err={?}", .{f_reader.err});
+ return error.BenchmarkFailed;
+ };
+ if (n == 0) break; // EOF reached
+
+ for (buf[0..n]) |c| {
+ const cp_, const consumed = d.next(c);
+ assert(consumed);
+ if (cp_) |cp| {
+ // This is the same trick we do in terminal.zig so we
+ // keep it here.
+ std.mem.doNotOptimizeAway(if (cp <= 0xFF)
+ 1
+ else
+ table.get(@intCast(cp)).width);
+ }
+ }
+ }
+}
+
+fn stepSimd(ptr: *anyopaque) Benchmark.Error!void {
+ const self: *CodepointWidth = @ptrCast(@alignCast(ptr));
+
+ const f = self.data_f orelse return;
+ var read_buf: [4096]u8 align(std.atomic.cache_line) = undefined;
+ var f_reader = f.reader(&read_buf);
+ var r = &f_reader.interface;
+
+ var d: UTF8Decoder = .{};
+ var buf: [4096]u8 align(std.atomic.cache_line) = undefined;
+ while (true) {
+ const n = r.readSliceShort(&buf) catch {
+ log.warn("error reading data file err={?}", .{f_reader.err});
+ return error.BenchmarkFailed;
+ };
+ if (n == 0) break; // EOF reached
+
+ for (buf[0..n]) |c| {
+ const cp_, const consumed = d.next(c);
+ assert(consumed);
+ if (cp_) |cp| {
+ std.mem.doNotOptimizeAway(simd.codepointWidth(cp));
+ }
+ }
+ }
+}
+
+test CodepointWidth {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ const impl: *CodepointWidth = try .create(alloc, .{});
+ defer impl.destroy(alloc);
+
+ const bench = impl.benchmark();
+ _ = try bench.run(.once);
+}
diff --git a/vendor/libghostty-vt/src/benchmark/GraphemeBreak.zig b/vendor/libghostty-vt/src/benchmark/GraphemeBreak.zig
new file mode 100644
index 00000000..8278c5c2
--- /dev/null
+++ b/vendor/libghostty-vt/src/benchmark/GraphemeBreak.zig
@@ -0,0 +1,152 @@
+//! This benchmark tests the throughput of grapheme break calculation.
+//! This is a common operation in terminal character printing for terminals
+//! that support grapheme clustering.
+const GraphemeBreak = @This();
+
+const std = @import("std");
+const assert = std.debug.assert;
+const Allocator = std.mem.Allocator;
+const Benchmark = @import("Benchmark.zig");
+const options = @import("options.zig");
+const UTF8Decoder = @import("../terminal/UTF8Decoder.zig");
+const unicode = @import("../unicode/main.zig");
+const uucode = @import("uucode");
+
+const log = std.log.scoped(.@"terminal-stream-bench");
+
+opts: Options,
+
+/// The file, opened in the setup function.
+data_f: ?std.fs.File = null,
+
+pub const Options = struct {
+ /// The type of codepoint width calculation to use.
+ mode: Mode = .table,
+
+ /// The data to read as a filepath. If this is "-" then
+ /// we will read stdin. If this is unset, then we will
+ /// do nothing (benchmark is a noop). It'd be more unixy to
+ /// use stdin by default but I find that a hanging CLI command
+ /// with no interaction is a bit annoying.
+ data: ?[]const u8 = null,
+};
+
+pub const Mode = enum {
+ /// The baseline mode copies the data from the fd into a buffer. This
+ /// is used to show the minimal overhead of reading the fd into memory
+ /// and establishes a baseline for the other modes.
+ noop,
+
+ /// Ghostty's table-based approach.
+ table,
+};
+
+/// Create a new terminal stream handler for the given arguments.
+pub fn create(
+ alloc: Allocator,
+ opts: Options,
+) !*GraphemeBreak {
+ const ptr = try alloc.create(GraphemeBreak);
+ errdefer alloc.destroy(ptr);
+ ptr.* = .{ .opts = opts };
+ return ptr;
+}
+
+pub fn destroy(self: *GraphemeBreak, alloc: Allocator) void {
+ alloc.destroy(self);
+}
+
+pub fn benchmark(self: *GraphemeBreak) Benchmark {
+ return .init(self, .{
+ .stepFn = switch (self.opts.mode) {
+ .noop => stepNoop,
+ .table => stepTable,
+ },
+ .setupFn = setup,
+ .teardownFn = teardown,
+ });
+}
+
+fn setup(ptr: *anyopaque) Benchmark.Error!void {
+ const self: *GraphemeBreak = @ptrCast(@alignCast(ptr));
+
+ // Open our data file to prepare for reading. We can do more
+ // validation here eventually.
+ assert(self.data_f == null);
+ self.data_f = options.dataFile(self.opts.data) catch |err| {
+ log.warn("error opening data file err={}", .{err});
+ return error.BenchmarkFailed;
+ };
+}
+
+fn teardown(ptr: *anyopaque) void {
+ const self: *GraphemeBreak = @ptrCast(@alignCast(ptr));
+ if (self.data_f) |f| {
+ f.close();
+ self.data_f = null;
+ }
+}
+
+fn stepNoop(ptr: *anyopaque) Benchmark.Error!void {
+ const self: *GraphemeBreak = @ptrCast(@alignCast(ptr));
+
+ const f = self.data_f orelse return;
+ var read_buf: [4096]u8 align(std.atomic.cache_line) = undefined;
+ var f_reader = f.reader(&read_buf);
+ var r = &f_reader.interface;
+
+ var d: UTF8Decoder = .{};
+ var buf: [4096]u8 align(std.atomic.cache_line) = undefined;
+ while (true) {
+ const n = r.readSliceShort(&buf) catch {
+ log.warn("error reading data file err={?}", .{f_reader.err});
+ return error.BenchmarkFailed;
+ };
+ if (n == 0) break; // EOF reached
+
+ for (buf[0..n]) |c| {
+ _ = d.next(c);
+ }
+ }
+}
+
+fn stepTable(ptr: *anyopaque) Benchmark.Error!void {
+ const self: *GraphemeBreak = @ptrCast(@alignCast(ptr));
+
+ const f = self.data_f orelse return;
+ var read_buf: [4096]u8 align(std.atomic.cache_line) = undefined;
+ var f_reader = f.reader(&read_buf);
+ var r = &f_reader.interface;
+
+ var d: UTF8Decoder = .{};
+ var state: uucode.grapheme.BreakState = .default;
+ var cp1: u21 = 0;
+ var buf: [4096]u8 align(std.atomic.cache_line) = undefined;
+ while (true) {
+ const n = r.readSliceShort(&buf) catch {
+ log.warn("error reading data file err={?}", .{f_reader.err});
+ return error.BenchmarkFailed;
+ };
+ if (n == 0) break; // EOF reached
+
+ for (buf[0..n]) |c| {
+ const cp_, const consumed = d.next(c);
+ assert(consumed);
+ if (cp_) |cp2| {
+ std.mem.doNotOptimizeAway(unicode.graphemeBreak(cp1, @intCast(cp2), &state));
+ cp1 = cp2;
+ }
+ }
+ }
+}
+
+test GraphemeBreak {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ const impl: *GraphemeBreak = try .create(alloc, .{});
+ defer impl.destroy(alloc);
+
+ const bench = impl.benchmark();
+ _ = try bench.run(.once);
+}
diff --git a/vendor/libghostty-vt/src/benchmark/IsSymbol.zig b/vendor/libghostty-vt/src/benchmark/IsSymbol.zig
new file mode 100644
index 00000000..4fbffd1e
--- /dev/null
+++ b/vendor/libghostty-vt/src/benchmark/IsSymbol.zig
@@ -0,0 +1,151 @@
+//! This benchmark tests the throughput of grapheme break calculation.
+//! This is a common operation in terminal character printing for terminals
+//! that support grapheme clustering.
+const IsSymbol = @This();
+
+const std = @import("std");
+const assert = std.debug.assert;
+const Allocator = std.mem.Allocator;
+const Benchmark = @import("Benchmark.zig");
+const options = @import("options.zig");
+const UTF8Decoder = @import("../terminal/UTF8Decoder.zig");
+const uucode = @import("uucode");
+const symbols_table = @import("../unicode/symbols_table.zig").table;
+
+const log = std.log.scoped(.@"is-symbol-bench");
+
+opts: Options,
+
+/// The file, opened in the setup function.
+data_f: ?std.fs.File = null,
+
+pub const Options = struct {
+ /// Which test to run.
+ mode: Mode = .uucode,
+
+ /// The data to read as a filepath. If this is "-" then
+ /// we will read stdin. If this is unset, then we will
+ /// do nothing (benchmark is a noop). It'd be more unixy to
+ /// use stdin by default but I find that a hanging CLI command
+ /// with no interaction is a bit annoying.
+ data: ?[]const u8 = null,
+};
+
+pub const Mode = enum {
+ /// uucode implementation
+ uucode,
+
+ /// Ghostty's table-based approach.
+ table,
+};
+
+/// Create a new terminal stream handler for the given arguments.
+pub fn create(
+ alloc: Allocator,
+ opts: Options,
+) !*IsSymbol {
+ const ptr = try alloc.create(IsSymbol);
+ errdefer alloc.destroy(ptr);
+ ptr.* = .{ .opts = opts };
+ return ptr;
+}
+
+pub fn destroy(self: *IsSymbol, alloc: Allocator) void {
+ alloc.destroy(self);
+}
+
+pub fn benchmark(self: *IsSymbol) Benchmark {
+ return .init(self, .{
+ .stepFn = switch (self.opts.mode) {
+ .uucode => stepUucode,
+ .table => stepTable,
+ },
+ .setupFn = setup,
+ .teardownFn = teardown,
+ });
+}
+
+fn setup(ptr: *anyopaque) Benchmark.Error!void {
+ const self: *IsSymbol = @ptrCast(@alignCast(ptr));
+
+ // Open our data file to prepare for reading. We can do more
+ // validation here eventually.
+ assert(self.data_f == null);
+ self.data_f = options.dataFile(self.opts.data) catch |err| {
+ log.warn("error opening data file err={}", .{err});
+ return error.BenchmarkFailed;
+ };
+}
+
+fn teardown(ptr: *anyopaque) void {
+ const self: *IsSymbol = @ptrCast(@alignCast(ptr));
+ if (self.data_f) |f| {
+ f.close();
+ self.data_f = null;
+ }
+}
+
+fn stepUucode(ptr: *anyopaque) Benchmark.Error!void {
+ const self: *IsSymbol = @ptrCast(@alignCast(ptr));
+
+ const f = self.data_f orelse return;
+ var read_buf: [4096]u8 align(std.atomic.cache_line) = undefined;
+ var f_reader = f.reader(&read_buf);
+ var r = &f_reader.interface;
+
+ var d: UTF8Decoder = .{};
+ var buf: [4096]u8 align(std.atomic.cache_line) = undefined;
+ while (true) {
+ const n = r.readSliceShort(&buf) catch {
+ log.warn("error reading data file err={?}", .{f_reader.err});
+ return error.BenchmarkFailed;
+ };
+ if (n == 0) break; // EOF reached
+
+ for (buf[0..n]) |c| {
+ const cp_, const consumed = d.next(c);
+ assert(consumed);
+ if (cp_) |cp| {
+ std.mem.doNotOptimizeAway(uucode.get(.is_symbol, cp));
+ }
+ }
+ }
+}
+
+fn stepTable(ptr: *anyopaque) Benchmark.Error!void {
+ const self: *IsSymbol = @ptrCast(@alignCast(ptr));
+
+ const f = self.data_f orelse return;
+ var read_buf: [4096]u8 align(std.atomic.cache_line) = undefined;
+ var f_reader = f.reader(&read_buf);
+ var r = &f_reader.interface;
+
+ var d: UTF8Decoder = .{};
+ var buf: [4096]u8 align(std.atomic.cache_line) = undefined;
+ while (true) {
+ const n = r.readSliceShort(&buf) catch {
+ log.warn("error reading data file err={?}", .{f_reader.err});
+ return error.BenchmarkFailed;
+ };
+ if (n == 0) break; // EOF reached
+
+ for (buf[0..n]) |c| {
+ const cp_, const consumed = d.next(c);
+ assert(consumed);
+ if (cp_) |cp| {
+ std.mem.doNotOptimizeAway(symbols_table.get(cp));
+ }
+ }
+ }
+}
+
+test IsSymbol {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ const impl: *IsSymbol = try .create(alloc, .{});
+ defer impl.destroy(alloc);
+
+ const bench = impl.benchmark();
+ _ = try bench.run(.once);
+}
diff --git a/vendor/libghostty-vt/src/benchmark/OscParser.zig b/vendor/libghostty-vt/src/benchmark/OscParser.zig
new file mode 100644
index 00000000..d4b416de
--- /dev/null
+++ b/vendor/libghostty-vt/src/benchmark/OscParser.zig
@@ -0,0 +1,118 @@
+//! This benchmark tests the throughput of the OSC parser.
+const OscParser = @This();
+
+const std = @import("std");
+const builtin = @import("builtin");
+const assert = std.debug.assert;
+const Allocator = std.mem.Allocator;
+const Benchmark = @import("Benchmark.zig");
+const options = @import("options.zig");
+const Parser = @import("../terminal/osc.zig").Parser;
+const log = std.log.scoped(.@"osc-parser-bench");
+
+opts: Options,
+
+/// The file, opened in the setup function.
+data_f: ?std.fs.File = null,
+
+parser: Parser,
+
+pub const Options = struct {
+ /// The data to read as a filepath. If this is "-" then
+ /// we will read stdin. If this is unset, then we will
+ /// do nothing (benchmark is a noop). It'd be more unixy to
+ /// use stdin by default but I find that a hanging CLI command
+ /// with no interaction is a bit annoying.
+ data: ?[]const u8 = null,
+};
+
+/// Create a new terminal stream handler for the given arguments.
+pub fn create(
+ alloc: Allocator,
+ opts: Options,
+) !*OscParser {
+ const ptr = try alloc.create(OscParser);
+ errdefer alloc.destroy(ptr);
+ ptr.* = .{
+ .opts = opts,
+ .data_f = null,
+ .parser = .init(alloc),
+ };
+ return ptr;
+}
+
+pub fn destroy(self: *OscParser, alloc: Allocator) void {
+ self.parser.deinit();
+ alloc.destroy(self);
+}
+
+pub fn benchmark(self: *OscParser) Benchmark {
+ return .init(self, .{
+ .stepFn = step,
+ .setupFn = setup,
+ .teardownFn = teardown,
+ });
+}
+
+fn setup(ptr: *anyopaque) Benchmark.Error!void {
+ const self: *OscParser = @ptrCast(@alignCast(ptr));
+
+ // Open our data file to prepare for reading. We can do more
+ // validation here eventually.
+ assert(self.data_f == null);
+ self.data_f = options.dataFile(self.opts.data) catch |err| {
+ log.warn("error opening data file err={}", .{err});
+ return error.BenchmarkFailed;
+ };
+ self.parser.reset();
+}
+
+fn teardown(ptr: *anyopaque) void {
+ const self: *OscParser = @ptrCast(@alignCast(ptr));
+ if (self.data_f) |f| {
+ f.close();
+ self.data_f = null;
+ }
+}
+
+fn step(ptr: *anyopaque) Benchmark.Error!void {
+ const self: *OscParser = @ptrCast(@alignCast(ptr));
+
+ const f = self.data_f orelse return;
+ var read_buf: [4096]u8 align(std.atomic.cache_line) = undefined;
+ var r = f.reader(&read_buf);
+
+ var osc_buf: [4096]u8 align(std.atomic.cache_line) = undefined;
+ while (true) {
+ r.interface.fill(@bitSizeOf(usize) / 8) catch |err| switch (err) {
+ error.EndOfStream => return,
+ error.ReadFailed => return error.BenchmarkFailed,
+ };
+ const len = r.interface.takeInt(usize, .little) catch |err| switch (err) {
+ error.EndOfStream => return,
+ error.ReadFailed => return error.BenchmarkFailed,
+ };
+
+ if (len > osc_buf.len) return error.BenchmarkFailed;
+
+ r.interface.readSliceAll(osc_buf[0..len]) catch |err| switch (err) {
+ error.EndOfStream => return,
+ error.ReadFailed => return error.BenchmarkFailed,
+ };
+
+ for (osc_buf[0..len]) |c| @call(.always_inline, Parser.next, .{ &self.parser, c });
+ std.mem.doNotOptimizeAway(self.parser.end(std.ascii.control_code.bel));
+ self.parser.reset();
+ }
+}
+
+test OscParser {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ const impl: *OscParser = try .create(alloc, .{});
+ defer impl.destroy(alloc);
+
+ const bench = impl.benchmark();
+ _ = try bench.run(.once);
+}
diff --git a/vendor/libghostty-vt/src/benchmark/ScreenClone.zig b/vendor/libghostty-vt/src/benchmark/ScreenClone.zig
new file mode 100644
index 00000000..108eaa0c
--- /dev/null
+++ b/vendor/libghostty-vt/src/benchmark/ScreenClone.zig
@@ -0,0 +1,193 @@
+//! This benchmark tests the performance of the Screen.clone
+//! function. This is useful because it is one of the primary lock
+//! holders that impact IO performance when the renderer is active.
+//! We do this very frequently.
+const ScreenClone = @This();
+
+const std = @import("std");
+const assert = std.debug.assert;
+const Allocator = std.mem.Allocator;
+const terminalpkg = @import("../terminal/main.zig");
+const Benchmark = @import("Benchmark.zig");
+const options = @import("options.zig");
+const Terminal = terminalpkg.Terminal;
+
+const log = std.log.scoped(.@"terminal-stream-bench");
+
+opts: Options,
+terminal: Terminal,
+
+pub const Options = struct {
+ /// The type of codepoint width calculation to use.
+ mode: Mode = .clone,
+
+ /// The size of the terminal. This affects benchmarking when
+ /// dealing with soft line wrapping and the memory impact
+ /// of page sizes.
+ @"terminal-rows": u16 = 80,
+ @"terminal-cols": u16 = 120,
+
+ /// The data to read as a filepath. If this is "-" then
+ /// we will read stdin. If this is unset, then we will
+ /// do nothing (benchmark is a noop). It'd be more unixy to
+ /// use stdin by default but I find that a hanging CLI command
+ /// with no interaction is a bit annoying.
+ ///
+ /// This will be used to initialize the terminal screen state before
+ /// cloning. This data can switch to alt screen if it wants. The time
+ /// to read this is not part of the benchmark.
+ data: ?[]const u8 = null,
+};
+
+pub const Mode = enum {
+ /// The baseline mode copies the screen by value.
+ noop,
+
+ /// Full clone
+ clone,
+
+ /// RenderState rather than a screen clone.
+ render,
+};
+
+pub fn create(
+ alloc: Allocator,
+ opts: Options,
+) !*ScreenClone {
+ const ptr = try alloc.create(ScreenClone);
+ errdefer alloc.destroy(ptr);
+
+ ptr.* = .{
+ .opts = opts,
+ .terminal = try .init(alloc, .{
+ .rows = opts.@"terminal-rows",
+ .cols = opts.@"terminal-cols",
+ }),
+ };
+
+ return ptr;
+}
+
+pub fn destroy(self: *ScreenClone, alloc: Allocator) void {
+ self.terminal.deinit(alloc);
+ alloc.destroy(self);
+}
+
+pub fn benchmark(self: *ScreenClone) Benchmark {
+ return .init(self, .{
+ .stepFn = switch (self.opts.mode) {
+ .noop => stepNoop,
+ .clone => stepClone,
+ .render => stepRender,
+ },
+ .setupFn = setup,
+ .teardownFn = teardown,
+ });
+}
+
+fn setup(ptr: *anyopaque) Benchmark.Error!void {
+ const self: *ScreenClone = @ptrCast(@alignCast(ptr));
+
+ // Always reset our terminal state
+ self.terminal.fullReset();
+
+ // Force a style on every single row, which
+ var s = self.terminal.vtStream();
+ defer s.deinit();
+ s.nextSlice("\x1b[48;2;20;40;60m");
+ for (0..self.terminal.rows - 1) |_| s.nextSlice("hello\r\n");
+ s.nextSlice("hello");
+
+ // Setup our terminal state
+ const data_f: std.fs.File = (options.dataFile(
+ self.opts.data,
+ ) catch |err| {
+ log.warn("error opening data file err={}", .{err});
+ return error.BenchmarkFailed;
+ }) orelse return;
+
+ var stream = self.terminal.vtStream();
+ defer stream.deinit();
+
+ var read_buf: [4096]u8 align(std.atomic.cache_line) = undefined;
+ var f_reader = data_f.reader(&read_buf);
+ const r = &f_reader.interface;
+
+ var buf: [4096]u8 = undefined;
+ while (true) {
+ const n = r.readSliceShort(&buf) catch {
+ log.warn("error reading data file err={?}", .{f_reader.err});
+ return error.BenchmarkFailed;
+ };
+ if (n == 0) break; // EOF reached
+ stream.nextSlice(buf[0..n]);
+ }
+}
+
+fn teardown(ptr: *anyopaque) void {
+ const self: *ScreenClone = @ptrCast(@alignCast(ptr));
+ _ = self;
+}
+
+fn stepNoop(ptr: *anyopaque) Benchmark.Error!void {
+ const self: *ScreenClone = @ptrCast(@alignCast(ptr));
+
+ // We loop because its so fast that a single benchmark run doesn't
+ // properly capture our speeds.
+ for (0..1000) |_| {
+ const s: terminalpkg.Screen = self.terminal.screens.active.*;
+ std.mem.doNotOptimizeAway(s);
+ }
+}
+
+fn stepClone(ptr: *anyopaque) Benchmark.Error!void {
+ const self: *ScreenClone = @ptrCast(@alignCast(ptr));
+
+ // We loop because its so fast that a single benchmark run doesn't
+ // properly capture our speeds.
+ for (0..1000) |_| {
+ const s: *terminalpkg.Screen = self.terminal.screens.active;
+ const copy = s.clone(
+ s.alloc,
+ .{ .viewport = .{} },
+ null,
+ ) catch |err| {
+ log.warn("error cloning screen err={}", .{err});
+ return error.BenchmarkFailed;
+ };
+ std.mem.doNotOptimizeAway(copy);
+
+ // Note: we purposely do not free memory because we don't want
+ // to benchmark that. We'll free when the benchmark exits.
+ }
+}
+
+fn stepRender(ptr: *anyopaque) Benchmark.Error!void {
+ const self: *ScreenClone = @ptrCast(@alignCast(ptr));
+
+ // We do this once out of the loop because a significant slowdown
+ // on the first run is allocation. After that first run, even with
+ // a full rebuild, it is much faster. Let's ignore that first run
+ // slowdown.
+ const alloc = self.terminal.screens.active.alloc;
+ var state: terminalpkg.RenderState = .empty;
+ state.update(alloc, &self.terminal) catch |err| {
+ log.warn("error cloning screen err={}", .{err});
+ return error.BenchmarkFailed;
+ };
+
+ // We loop because its so fast that a single benchmark run doesn't
+ // properly capture our speeds.
+ for (0..1000) |_| {
+ // Forces a full rebuild because it thinks our screen changed
+ state.screen = .alternate;
+ state.update(alloc, &self.terminal) catch |err| {
+ log.warn("error cloning screen err={}", .{err});
+ return error.BenchmarkFailed;
+ };
+ std.mem.doNotOptimizeAway(state);
+
+ // Note: we purposely do not free memory because we don't want
+ // to benchmark that. We'll free when the benchmark exits.
+ }
+}
diff --git a/vendor/libghostty-vt/src/benchmark/TerminalParser.zig b/vendor/libghostty-vt/src/benchmark/TerminalParser.zig
new file mode 100644
index 00000000..e0008176
--- /dev/null
+++ b/vendor/libghostty-vt/src/benchmark/TerminalParser.zig
@@ -0,0 +1,108 @@
+//! This benchmark tests the throughput of the terminal escape code parser.
+const TerminalParser = @This();
+
+const std = @import("std");
+const assert = std.debug.assert;
+const Allocator = std.mem.Allocator;
+const terminalpkg = @import("../terminal/main.zig");
+const Benchmark = @import("Benchmark.zig");
+const options = @import("options.zig");
+
+const log = std.log.scoped(.@"terminal-stream-bench");
+
+opts: Options,
+
+/// The file, opened in the setup function.
+data_f: ?std.fs.File = null,
+
+pub const Options = struct {
+ /// The data to read as a filepath. If this is "-" then
+ /// we will read stdin. If this is unset, then we will
+ /// do nothing (benchmark is a noop). It'd be more unixy to
+ /// use stdin by default but I find that a hanging CLI command
+ /// with no interaction is a bit annoying.
+ data: ?[]const u8 = null,
+};
+
+pub fn create(
+ alloc: Allocator,
+ opts: Options,
+) !*TerminalParser {
+ const ptr = try alloc.create(TerminalParser);
+ errdefer alloc.destroy(ptr);
+ ptr.* = .{ .opts = opts };
+ return ptr;
+}
+
+pub fn destroy(self: *TerminalParser, alloc: Allocator) void {
+ alloc.destroy(self);
+}
+
+pub fn benchmark(self: *TerminalParser) Benchmark {
+ return .init(self, .{
+ .stepFn = step,
+ .setupFn = setup,
+ .teardownFn = teardown,
+ });
+}
+
+fn setup(ptr: *anyopaque) Benchmark.Error!void {
+ const self: *TerminalParser = @ptrCast(@alignCast(ptr));
+
+ // Open our data file to prepare for reading. We can do more
+ // validation here eventually.
+ assert(self.data_f == null);
+ self.data_f = options.dataFile(self.opts.data) catch |err| {
+ log.warn("error opening data file err={}", .{err});
+ return error.BenchmarkFailed;
+ };
+}
+
+fn teardown(ptr: *anyopaque) void {
+ const self: *TerminalParser = @ptrCast(@alignCast(ptr));
+ if (self.data_f) |f| {
+ f.close();
+ self.data_f = null;
+ }
+}
+
+fn step(ptr: *anyopaque) Benchmark.Error!void {
+ const self: *TerminalParser = @ptrCast(@alignCast(ptr));
+
+ // Get our buffered reader so we're not predominantly
+ // waiting on file IO. It'd be better to move this fully into
+ // memory. If we're IO bound though that should show up on
+ // the benchmark results and... I know writing this that we
+ // aren't currently IO bound.
+ const f = self.data_f orelse return;
+ var read_buf: [4096]u8 align(std.atomic.cache_line) = undefined;
+ var f_reader = f.reader(&read_buf);
+ var r = &f_reader.interface;
+
+ var p: terminalpkg.Parser = .init();
+
+ var buf: [4096]u8 = undefined;
+ while (true) {
+ const n = r.readSliceShort(&buf) catch {
+ log.warn("error reading data file err={?}", .{f_reader.err});
+ return error.BenchmarkFailed;
+ };
+ if (n == 0) break; // EOF reached
+ for (buf[0..n]) |c| {
+ const actions = p.next(c);
+ //std.log.warn("actions={any}", .{actions});
+ _ = actions;
+ }
+ }
+}
+
+test TerminalParser {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ const impl: *TerminalParser = try .create(alloc, .{});
+ defer impl.destroy(alloc);
+
+ const bench = impl.benchmark();
+ _ = try bench.run(.once);
+}
diff --git a/vendor/libghostty-vt/src/benchmark/TerminalStream.zig b/vendor/libghostty-vt/src/benchmark/TerminalStream.zig
new file mode 100644
index 00000000..1cac656e
--- /dev/null
+++ b/vendor/libghostty-vt/src/benchmark/TerminalStream.zig
@@ -0,0 +1,161 @@
+//! This benchmark tests the performance of the terminal stream
+//! handler from input to terminal state update. This is useful to
+//! test general throughput of VT parsing and handling.
+//!
+//! Note that the handler used for this benchmark isn't the full
+//! terminal handler, since that requires a significant amount of
+//! state. This is a simplified version that only handles specific
+//! terminal operations like printing characters. We should expand
+//! this to include more operations to improve the accuracy of the
+//! benchmark.
+//!
+//! It is a fairly broad benchmark that can be used to determine
+//! if we need to optimize something more specific (e.g. the parser).
+const TerminalStream = @This();
+
+const std = @import("std");
+const assert = std.debug.assert;
+const Allocator = std.mem.Allocator;
+const terminalpkg = @import("../terminal/main.zig");
+const Benchmark = @import("Benchmark.zig");
+const options = @import("options.zig");
+const Terminal = terminalpkg.Terminal;
+const Stream = terminalpkg.Stream(*Handler);
+
+const log = std.log.scoped(.@"terminal-stream-bench");
+
+opts: Options,
+terminal: Terminal,
+handler: Handler,
+stream: Stream,
+
+/// The file, opened in the setup function.
+data_f: ?std.fs.File = null,
+
+pub const Options = struct {
+ /// The size of the terminal. This affects benchmarking when
+ /// dealing with soft line wrapping and the memory impact
+ /// of page sizes.
+ @"terminal-rows": u16 = 80,
+ @"terminal-cols": u16 = 120,
+
+ /// The data to read as a filepath. If this is "-" then
+ /// we will read stdin. If this is unset, then we will
+ /// do nothing (benchmark is a noop). It'd be more unixy to
+ /// use stdin by default but I find that a hanging CLI command
+ /// with no interaction is a bit annoying.
+ data: ?[]const u8 = null,
+};
+
+/// Create a new terminal stream handler for the given arguments.
+pub fn create(
+ alloc: Allocator,
+ opts: Options,
+) !*TerminalStream {
+ const ptr = try alloc.create(TerminalStream);
+ errdefer alloc.destroy(ptr);
+
+ ptr.* = .{
+ .opts = opts,
+ .terminal = try .init(alloc, .{
+ .rows = opts.@"terminal-rows",
+ .cols = opts.@"terminal-cols",
+ }),
+ .handler = .{ .t = &ptr.terminal },
+ .stream = .init(&ptr.handler),
+ };
+
+ return ptr;
+}
+
+pub fn destroy(self: *TerminalStream, alloc: Allocator) void {
+ self.terminal.deinit(alloc);
+ alloc.destroy(self);
+}
+
+pub fn benchmark(self: *TerminalStream) Benchmark {
+ return .init(self, .{
+ .stepFn = step,
+ .setupFn = setup,
+ .teardownFn = teardown,
+ });
+}
+
+fn setup(ptr: *anyopaque) Benchmark.Error!void {
+ const self: *TerminalStream = @ptrCast(@alignCast(ptr));
+
+ // Always reset our terminal state
+ self.terminal.fullReset();
+
+ // Open our data file to prepare for reading. We can do more
+ // validation here eventually.
+ assert(self.data_f == null);
+ self.data_f = options.dataFile(self.opts.data) catch |err| {
+ log.warn("error opening data file err={}", .{err});
+ return error.BenchmarkFailed;
+ };
+}
+
+fn teardown(ptr: *anyopaque) void {
+ const self: *TerminalStream = @ptrCast(@alignCast(ptr));
+ if (self.data_f) |f| {
+ f.close();
+ self.data_f = null;
+ }
+}
+
+fn step(ptr: *anyopaque) Benchmark.Error!void {
+ const self: *TerminalStream = @ptrCast(@alignCast(ptr));
+
+ // Get our buffered reader so we're not predominantly
+ // waiting on file IO. It'd be better to move this fully into
+ // memory. If we're IO bound though that should show up on
+ // the benchmark results and... I know writing this that we
+ // aren't currently IO bound.
+ const f = self.data_f orelse return;
+
+ var read_buf: [4096]u8 align(std.atomic.cache_line) = undefined;
+ var f_reader = f.reader(&read_buf);
+ const r = &f_reader.interface;
+
+ var buf: [4096]u8 = undefined;
+ while (true) {
+ const n = r.readSliceShort(&buf) catch {
+ log.warn("error reading data file err={?}", .{f_reader.err});
+ return error.BenchmarkFailed;
+ };
+ if (n == 0) break; // EOF reached
+ self.stream.nextSlice(buf[0..n]);
+ }
+}
+
+/// Implements the handler interface for the terminal.Stream.
+/// We should expand this to include more operations to make
+/// our benchmark more realistic.
+const Handler = struct {
+ t: *Terminal,
+
+ pub fn vt(
+ self: *Handler,
+ comptime action: Stream.Action.Tag,
+ value: Stream.Action.Value(action),
+ ) void {
+ switch (action) {
+ .print => self.t.print(value.cp) catch |err| {
+ log.warn("error processing benchmark print err={}", .{err});
+ },
+ else => {},
+ }
+ }
+};
+
+test TerminalStream {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ const impl: *TerminalStream = try .create(alloc, .{});
+ defer impl.destroy(alloc);
+
+ const bench = impl.benchmark();
+ _ = try bench.run(.once);
+}
diff --git a/vendor/libghostty-vt/src/benchmark/cli.zig b/vendor/libghostty-vt/src/benchmark/cli.zig
new file mode 100644
index 00000000..13f07077
--- /dev/null
+++ b/vendor/libghostty-vt/src/benchmark/cli.zig
@@ -0,0 +1,100 @@
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const cli = @import("../cli.zig");
+
+/// The available actions for the CLI. This is the list of available
+/// benchmarks. View docs for each individual one in the predictably
+/// named files.
+pub const Action = enum {
+ @"codepoint-width",
+ @"grapheme-break",
+ @"screen-clone",
+ @"terminal-parser",
+ @"terminal-stream",
+ @"is-symbol",
+ @"osc-parser",
+
+ /// Returns the struct associated with the action. The struct
+ /// should have a few decls:
+ ///
+ /// - `const Options`: The CLI options for the action.
+ /// - `fn create`: Create a new instance of the action from options.
+ /// - `fn benchmark`: Returns a `Benchmark` instance for the action.
+ ///
+ /// See TerminalStream for an example.
+ pub fn Struct(comptime action: Action) type {
+ return switch (action) {
+ .@"screen-clone" => @import("ScreenClone.zig"),
+ .@"terminal-stream" => @import("TerminalStream.zig"),
+ .@"codepoint-width" => @import("CodepointWidth.zig"),
+ .@"grapheme-break" => @import("GraphemeBreak.zig"),
+ .@"terminal-parser" => @import("TerminalParser.zig"),
+ .@"is-symbol" => @import("IsSymbol.zig"),
+ .@"osc-parser" => @import("OscParser.zig"),
+ };
+ }
+};
+
+/// An entrypoint for the benchmark CLI.
+pub fn main() !void {
+ const alloc = std.heap.c_allocator;
+ const action_ = try cli.action.detectArgs(Action, alloc);
+ const action = action_ orelse return error.NoAction;
+ try mainAction(alloc, action, .cli);
+}
+
+/// Arguments that can be passed to the benchmark.
+pub const Args = union(enum) {
+ /// The arguments passed to the CLI via argc/argv.
+ cli,
+
+ /// Simple string arguments, parsed via std.process.ArgIteratorGeneral.
+ string: []const u8,
+};
+
+pub fn mainAction(
+ alloc: Allocator,
+ action: Action,
+ args: Args,
+) !void {
+ switch (action) {
+ inline else => |comptime_action| {
+ const BenchmarkImpl = Action.Struct(comptime_action);
+ try mainActionImpl(BenchmarkImpl, alloc, args);
+ },
+ }
+}
+
+fn mainActionImpl(
+ comptime BenchmarkImpl: type,
+ alloc: Allocator,
+ args: Args,
+) !void {
+ // First, parse our CLI options.
+ const Options = BenchmarkImpl.Options;
+ var opts: Options = .{};
+ defer if (@hasDecl(Options, "deinit")) opts.deinit();
+ switch (args) {
+ .cli => {
+ var iter = try cli.args.argsIterator(alloc);
+ defer iter.deinit();
+ try cli.args.parse(Options, alloc, &opts, &iter);
+ },
+ .string => |str| {
+ var iter = try std.process.ArgIteratorGeneral(.{}).init(
+ alloc,
+ str,
+ );
+ defer iter.deinit();
+ try cli.args.parse(Options, alloc, &opts, &iter);
+ },
+ }
+
+ // Create our implementation
+ const impl = try BenchmarkImpl.create(alloc, opts);
+ defer impl.destroy(alloc);
+
+ // Initialize our benchmark
+ const b = impl.benchmark();
+ _ = try b.run(.once);
+}
diff --git a/vendor/libghostty-vt/src/benchmark/main.zig b/vendor/libghostty-vt/src/benchmark/main.zig
new file mode 100644
index 00000000..5673044f
--- /dev/null
+++ b/vendor/libghostty-vt/src/benchmark/main.zig
@@ -0,0 +1,13 @@
+pub const cli = @import("cli.zig");
+pub const Benchmark = @import("Benchmark.zig");
+pub const CApi = @import("CApi.zig");
+pub const TerminalStream = @import("TerminalStream.zig");
+pub const CodepointWidth = @import("CodepointWidth.zig");
+pub const GraphemeBreak = @import("GraphemeBreak.zig");
+pub const ScreenClone = @import("ScreenClone.zig");
+pub const TerminalParser = @import("TerminalParser.zig");
+pub const IsSymbol = @import("IsSymbol.zig");
+
+test {
+ @import("std").testing.refAllDecls(@This());
+}
diff --git a/vendor/libghostty-vt/src/benchmark/options.zig b/vendor/libghostty-vt/src/benchmark/options.zig
new file mode 100644
index 00000000..049e80f4
--- /dev/null
+++ b/vendor/libghostty-vt/src/benchmark/options.zig
@@ -0,0 +1,20 @@
+//! This file contains helpers for CLI options.
+
+const std = @import("std");
+
+/// Returns the data file for the given path in a way that is consistent
+/// across our CLI. If the path is not set then no file is returned.
+/// If the path is "-", then we will return stdin. If the path is
+/// a file then we will open and return the handle.
+pub fn dataFile(path_: ?[]const u8) !?std.fs.File {
+ const path = path_ orelse return null;
+
+ // Stdin
+ if (std.mem.eql(u8, path, "-")) return .stdin();
+
+ // Normal file
+ const file = try std.fs.cwd().openFile(path, .{});
+ errdefer file.close();
+
+ return file;
+}