diff options
| author | Can Celik <ogulcancelik@gmail.com> | 2026-04-04 19:22:04 +0300 |
|---|---|---|
| committer | Can Celik <ogulcancelik@gmail.com> | 2026-04-05 01:23:06 +0300 |
| commit | 41dd733ca7123c9ac4bfc6c781697a660e95b052 (patch) | |
| tree | 3fc91815ca139ef2e0b281bba0abda4ef15175f4 /vendor/libghostty-vt/src/benchmark | |
| parent | 55c138263e190a1e79d4b997bfb9397306c0ec1f (diff) | |
feat: finish ghostty pane backend integration
Diffstat (limited to 'vendor/libghostty-vt/src/benchmark')
| -rw-r--r-- | vendor/libghostty-vt/src/benchmark/Benchmark.zig | 178 | ||||
| -rw-r--r-- | vendor/libghostty-vt/src/benchmark/CApi.zig | 34 | ||||
| -rw-r--r-- | vendor/libghostty-vt/src/benchmark/CodepointWidth.zig | 207 | ||||
| -rw-r--r-- | vendor/libghostty-vt/src/benchmark/GraphemeBreak.zig | 152 | ||||
| -rw-r--r-- | vendor/libghostty-vt/src/benchmark/IsSymbol.zig | 151 | ||||
| -rw-r--r-- | vendor/libghostty-vt/src/benchmark/OscParser.zig | 118 | ||||
| -rw-r--r-- | vendor/libghostty-vt/src/benchmark/ScreenClone.zig | 193 | ||||
| -rw-r--r-- | vendor/libghostty-vt/src/benchmark/TerminalParser.zig | 108 | ||||
| -rw-r--r-- | vendor/libghostty-vt/src/benchmark/TerminalStream.zig | 161 | ||||
| -rw-r--r-- | vendor/libghostty-vt/src/benchmark/cli.zig | 100 | ||||
| -rw-r--r-- | vendor/libghostty-vt/src/benchmark/main.zig | 13 | ||||
| -rw-r--r-- | vendor/libghostty-vt/src/benchmark/options.zig | 20 |
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; +} |
