aboutsummaryrefslogtreecommitdiff
path: root/vendor/libghostty-vt/src/benchmark
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-09-10 21:51:57 +0300
committerGitHub <noreply@github.com>2026-09-10 21:51:57 +0300
commit425c86179791cd32e2d4ce0ae7f269940b4b0663 (patch)
treee52c4383a6d69216cbe2a32638bf90841d7e2189 /vendor/libghostty-vt/src/benchmark
parent90e947a6e26e02f10ec7f1694c4eb5b78a86406b (diff)
chore: upgrade libghostty and preserve terminal behavior (#3906)
Diffstat (limited to 'vendor/libghostty-vt/src/benchmark')
-rw-r--r--vendor/libghostty-vt/src/benchmark/ApcParser.zig20
-rw-r--r--vendor/libghostty-vt/src/benchmark/Benchmark.zig11
-rw-r--r--vendor/libghostty-vt/src/benchmark/CApi.zig4
-rw-r--r--vendor/libghostty-vt/src/benchmark/CodepointWidth.zig11
-rw-r--r--vendor/libghostty-vt/src/benchmark/GraphemeBreak.zig9
-rw-r--r--vendor/libghostty-vt/src/benchmark/IsSymbol.zig9
-rw-r--r--vendor/libghostty-vt/src/benchmark/OscParser.zig7
-rw-r--r--vendor/libghostty-vt/src/benchmark/PageCompression.zig6
-rw-r--r--vendor/libghostty-vt/src/benchmark/ScreenClone.zig8
-rw-r--r--vendor/libghostty-vt/src/benchmark/ScrollbackCompression.zig9
-rw-r--r--vendor/libghostty-vt/src/benchmark/TerminalFormatter.zig482
-rw-r--r--vendor/libghostty-vt/src/benchmark/TerminalParser.zig7
-rw-r--r--vendor/libghostty-vt/src/benchmark/TerminalResize.zig357
-rw-r--r--vendor/libghostty-vt/src/benchmark/TerminalSnapshot.zig350
-rw-r--r--vendor/libghostty-vt/src/benchmark/TerminalStream.zig43
-rw-r--r--vendor/libghostty-vt/src/benchmark/cli.zig24
-rw-r--r--vendor/libghostty-vt/src/benchmark/main.zig3
-rw-r--r--vendor/libghostty-vt/src/benchmark/options.zig7
18 files changed, 1305 insertions, 62 deletions
diff --git a/vendor/libghostty-vt/src/benchmark/ApcParser.zig b/vendor/libghostty-vt/src/benchmark/ApcParser.zig
index 73b7c57a..9efdc4fb 100644
--- a/vendor/libghostty-vt/src/benchmark/ApcParser.zig
+++ b/vendor/libghostty-vt/src/benchmark/ApcParser.zig
@@ -12,6 +12,7 @@ const Allocator = std.mem.Allocator;
const terminalpkg = @import("../terminal/main.zig");
const Benchmark = @import("Benchmark.zig");
const options = @import("options.zig");
+const global = @import("../global.zig");
const log = std.log.scoped(.@"apc-parser-bench");
@@ -19,7 +20,7 @@ opts: Options,
stream: Stream,
/// The file, opened in the setup function.
-data_f: ?std.fs.File = null,
+data_f: ?std.Io.File = null,
pub const Options = struct {
/// The data to read as a filepath. If this is "-" then
@@ -51,10 +52,10 @@ const Handler = struct {
.apc_start => self.apc.start(),
.apc_put => self.apc.feed(self.alloc, value),
.apc_put_slice => self.apc.feedSlice(self.alloc, value.bytes),
- .apc_end => if (self.apc.end()) |cmd| {
- var c = cmd;
- std.mem.doNotOptimizeAway(&c);
- c.deinit(self.alloc);
+ .apc_end => if (self.apc.end()) |result| {
+ var r = result;
+ std.mem.doNotOptimizeAway(&r);
+ r.deinit(self.alloc);
},
else => {},
}
@@ -70,7 +71,10 @@ pub fn create(
errdefer alloc.destroy(ptr);
ptr.* = .{
.opts = opts,
- .stream = .init(.{ .alloc = alloc }),
+ .stream = .init(.{
+ .allocator = alloc,
+ .handler = .{ .alloc = alloc },
+ }),
};
return ptr;
}
@@ -103,7 +107,7 @@ fn setup(ptr: *anyopaque) Benchmark.Error!void {
fn teardown(ptr: *anyopaque) void {
const self: *ApcParser = @ptrCast(@alignCast(ptr));
if (self.data_f) |f| {
- f.close();
+ f.close(global.io());
self.data_f = null;
}
}
@@ -114,7 +118,7 @@ fn step(ptr: *anyopaque) Benchmark.Error!void {
const f = self.data_f orelse return;
var read_buf: [64 * 1024]u8 align(std.atomic.cache_line) = undefined;
- var f_reader = f.reader(&read_buf);
+ var f_reader = f.reader(global.io(), &read_buf);
const r = &f_reader.interface;
// This buffer size matches the read buffer size used by the
diff --git a/vendor/libghostty-vt/src/benchmark/Benchmark.zig b/vendor/libghostty-vt/src/benchmark/Benchmark.zig
index 41c3695d..d45ba181 100644
--- a/vendor/libghostty-vt/src/benchmark/Benchmark.zig
+++ b/vendor/libghostty-vt/src/benchmark/Benchmark.zig
@@ -6,6 +6,7 @@ const builtin = @import("builtin");
const assert = std.debug.assert;
const macos = @import("macos");
const build_config = @import("../build_config.zig");
+const global = @import("../global.zig");
ptr: *anyopaque,
vtable: VTable,
@@ -64,21 +65,23 @@ pub fn run(
signpost.log.release();
};
- const start = std.time.Instant.now() catch return error.BenchmarkFailed;
+ const start: std.Io.Timestamp = .now(global.io(), .awake);
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 now: std.Io.Timestamp = .now(global.io(), .awake);
+ const elapsed = start.durationTo(now).nanoseconds;
+ assert(elapsed >= 0);
const exit = switch (mode) {
.once => true,
- .duration => |ns| now.since(start) >= ns,
+ .duration => |ns| elapsed >= ns,
};
if (exit) {
- result.duration = now.since(start);
+ result.duration = @as(u64, @intCast(std.math.clamp(elapsed, 0, std.math.maxInt(u64))));
return result;
}
}
diff --git a/vendor/libghostty-vt/src/benchmark/CApi.zig b/vendor/libghostty-vt/src/benchmark/CApi.zig
index 3bef8b26..c9840671 100644
--- a/vendor/libghostty-vt/src/benchmark/CApi.zig
+++ b/vendor/libghostty-vt/src/benchmark/CApi.zig
@@ -1,6 +1,6 @@
const std = @import("std");
const cli = @import("cli.zig");
-const state = &@import("../global.zig").state;
+const global = @import("../global.zig");
const log = std.log.scoped(.benchmark);
@@ -19,7 +19,7 @@ export fn ghostty_benchmark_cli(
};
cli.mainAction(
- state.alloc,
+ global.alloc(),
action,
.{ .string = std.mem.sliceTo(args, 0) },
) catch |err| {
diff --git a/vendor/libghostty-vt/src/benchmark/CodepointWidth.zig b/vendor/libghostty-vt/src/benchmark/CodepointWidth.zig
index 30d3f91e..d0c6b808 100644
--- a/vendor/libghostty-vt/src/benchmark/CodepointWidth.zig
+++ b/vendor/libghostty-vt/src/benchmark/CodepointWidth.zig
@@ -14,13 +14,14 @@ 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 global = @import("../global.zig");
const log = std.log.scoped(.@"terminal-stream-bench");
opts: Options,
/// The file, opened in the setup function.
-data_f: ?std.fs.File = null,
+data_f: ?std.Io.File = null,
pub const Options = struct {
/// The type of codepoint width calculation to use.
@@ -93,7 +94,7 @@ fn setup(ptr: *anyopaque) Benchmark.Error!void {
fn teardown(ptr: *anyopaque) void {
const self: *CodepointWidth = @ptrCast(@alignCast(ptr));
if (self.data_f) |f| {
- f.close();
+ f.close(global.io());
self.data_f = null;
}
}
@@ -114,7 +115,7 @@ fn stepWcwidth(ptr: *anyopaque) Benchmark.Error!void {
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 f_reader = f.reader(global.io(), &read_buf);
var r = &f_reader.interface;
var d: UTF8Decoder = .{};
@@ -141,7 +142,7 @@ fn stepTable(ptr: *anyopaque) Benchmark.Error!void {
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 f_reader = f.reader(global.io(), &read_buf);
var r = &f_reader.interface;
var d: UTF8Decoder = .{};
@@ -173,7 +174,7 @@ fn stepSimd(ptr: *anyopaque) Benchmark.Error!void {
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 f_reader = f.reader(global.io(), &read_buf);
var r = &f_reader.interface;
var d: UTF8Decoder = .{};
diff --git a/vendor/libghostty-vt/src/benchmark/GraphemeBreak.zig b/vendor/libghostty-vt/src/benchmark/GraphemeBreak.zig
index 8278c5c2..8e5236fe 100644
--- a/vendor/libghostty-vt/src/benchmark/GraphemeBreak.zig
+++ b/vendor/libghostty-vt/src/benchmark/GraphemeBreak.zig
@@ -11,13 +11,14 @@ const options = @import("options.zig");
const UTF8Decoder = @import("../terminal/UTF8Decoder.zig");
const unicode = @import("../unicode/main.zig");
const uucode = @import("uucode");
+const global = @import("../global.zig");
const log = std.log.scoped(.@"terminal-stream-bench");
opts: Options,
/// The file, opened in the setup function.
-data_f: ?std.fs.File = null,
+data_f: ?std.Io.File = null,
pub const Options = struct {
/// The type of codepoint width calculation to use.
@@ -82,7 +83,7 @@ fn setup(ptr: *anyopaque) Benchmark.Error!void {
fn teardown(ptr: *anyopaque) void {
const self: *GraphemeBreak = @ptrCast(@alignCast(ptr));
if (self.data_f) |f| {
- f.close();
+ f.close(global.io());
self.data_f = null;
}
}
@@ -92,7 +93,7 @@ fn stepNoop(ptr: *anyopaque) Benchmark.Error!void {
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 f_reader = f.reader(global.io(), &read_buf);
var r = &f_reader.interface;
var d: UTF8Decoder = .{};
@@ -115,7 +116,7 @@ fn stepTable(ptr: *anyopaque) Benchmark.Error!void {
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 f_reader = f.reader(global.io(), &read_buf);
var r = &f_reader.interface;
var d: UTF8Decoder = .{};
diff --git a/vendor/libghostty-vt/src/benchmark/IsSymbol.zig b/vendor/libghostty-vt/src/benchmark/IsSymbol.zig
index 4fbffd1e..c99bc9ca 100644
--- a/vendor/libghostty-vt/src/benchmark/IsSymbol.zig
+++ b/vendor/libghostty-vt/src/benchmark/IsSymbol.zig
@@ -11,13 +11,14 @@ 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 global = @import("../global.zig");
const log = std.log.scoped(.@"is-symbol-bench");
opts: Options,
/// The file, opened in the setup function.
-data_f: ?std.fs.File = null,
+data_f: ?std.Io.File = null,
pub const Options = struct {
/// Which test to run.
@@ -80,7 +81,7 @@ fn setup(ptr: *anyopaque) Benchmark.Error!void {
fn teardown(ptr: *anyopaque) void {
const self: *IsSymbol = @ptrCast(@alignCast(ptr));
if (self.data_f) |f| {
- f.close();
+ f.close(global.io());
self.data_f = null;
}
}
@@ -90,7 +91,7 @@ fn stepUucode(ptr: *anyopaque) Benchmark.Error!void {
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 f_reader = f.reader(global.io(), &read_buf);
var r = &f_reader.interface;
var d: UTF8Decoder = .{};
@@ -117,7 +118,7 @@ fn stepTable(ptr: *anyopaque) Benchmark.Error!void {
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 f_reader = f.reader(global.io(), &read_buf);
var r = &f_reader.interface;
var d: UTF8Decoder = .{};
diff --git a/vendor/libghostty-vt/src/benchmark/OscParser.zig b/vendor/libghostty-vt/src/benchmark/OscParser.zig
index d4b416de..9b21c37a 100644
--- a/vendor/libghostty-vt/src/benchmark/OscParser.zig
+++ b/vendor/libghostty-vt/src/benchmark/OscParser.zig
@@ -9,11 +9,12 @@ 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");
+const global = @import("../global.zig");
opts: Options,
/// The file, opened in the setup function.
-data_f: ?std.fs.File = null,
+data_f: ?std.Io.File = null,
parser: Parser,
@@ -70,7 +71,7 @@ fn setup(ptr: *anyopaque) Benchmark.Error!void {
fn teardown(ptr: *anyopaque) void {
const self: *OscParser = @ptrCast(@alignCast(ptr));
if (self.data_f) |f| {
- f.close();
+ f.close(global.io());
self.data_f = null;
}
}
@@ -80,7 +81,7 @@ fn step(ptr: *anyopaque) Benchmark.Error!void {
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 r = f.reader(global.io(), &read_buf);
var osc_buf: [4096]u8 align(std.atomic.cache_line) = undefined;
while (true) {
diff --git a/vendor/libghostty-vt/src/benchmark/PageCompression.zig b/vendor/libghostty-vt/src/benchmark/PageCompression.zig
index 21153659..23ecf0bf 100644
--- a/vendor/libghostty-vt/src/benchmark/PageCompression.zig
+++ b/vendor/libghostty-vt/src/benchmark/PageCompression.zig
@@ -67,6 +67,8 @@ const options = @import("options.zig");
const compress = @import("../terminal/compress.zig");
const CompressedPage = compress.Page;
const lz4 = compress.lz4;
+const compat_file = @import("../lib/compat/file.zig");
+const global = @import("../global.zig");
const log = std.log.scoped(.@"page-compression-bench");
@@ -210,9 +212,9 @@ fn setupData(self: *PageCompression) !void {
return error.InvalidRetainedPages;
const data_file = try options.dataFile(self.opts.data) orelse return;
- defer data_file.close();
+ defer data_file.close(global.io());
- self.data = try data_file.readToEndAlloc(self.alloc, max_data_size);
+ self.data = try compat_file.readToEndAlloc(data_file, self.alloc, max_data_size);
errdefer {
self.alloc.free(self.data);
self.data = &.{};
diff --git a/vendor/libghostty-vt/src/benchmark/ScreenClone.zig b/vendor/libghostty-vt/src/benchmark/ScreenClone.zig
index 5176ce26..9b2012cc 100644
--- a/vendor/libghostty-vt/src/benchmark/ScreenClone.zig
+++ b/vendor/libghostty-vt/src/benchmark/ScreenClone.zig
@@ -11,6 +11,7 @@ const terminalpkg = @import("../terminal/main.zig");
const Benchmark = @import("Benchmark.zig");
const options = @import("options.zig");
const Terminal = terminalpkg.Terminal;
+const global = @import("../global.zig");
const log = std.log.scoped(.@"terminal-stream-bench");
@@ -79,7 +80,7 @@ pub fn create(
ptr.* = .{
.opts = opts,
- .terminal = try .init(alloc, .{
+ .terminal = try .init(global.io(), alloc, .{
.rows = opts.@"terminal-rows",
.cols = opts.@"terminal-cols",
}),
@@ -122,7 +123,7 @@ fn setup(ptr: *anyopaque) Benchmark.Error!void {
s.nextSlice("hello");
// Setup our terminal state
- const data_f: std.fs.File = (options.dataFile(
+ const data_f: std.Io.File = (options.dataFile(
self.opts.data,
) catch |err| {
log.warn("error opening data file err={}", .{err});
@@ -133,7 +134,7 @@ fn setup(ptr: *anyopaque) Benchmark.Error!void {
defer stream.deinit();
var read_buf: [4096]u8 align(std.atomic.cache_line) = undefined;
- var f_reader = data_f.reader(&read_buf);
+ var f_reader = data_f.reader(global.io(), &read_buf);
const r = &f_reader.interface;
var buf: [4096]u8 = undefined;
@@ -171,6 +172,7 @@ fn stepClone(ptr: *anyopaque) Benchmark.Error!void {
for (0..1000) |_| {
const s: *terminalpkg.Screen = self.terminal.screens.active;
const copy = s.clone(
+ s.io,
s.alloc,
.{ .viewport = .{} },
null,
diff --git a/vendor/libghostty-vt/src/benchmark/ScrollbackCompression.zig b/vendor/libghostty-vt/src/benchmark/ScrollbackCompression.zig
index 40ec20f4..d971c551 100644
--- a/vendor/libghostty-vt/src/benchmark/ScrollbackCompression.zig
+++ b/vendor/libghostty-vt/src/benchmark/ScrollbackCompression.zig
@@ -74,6 +74,7 @@ const Benchmark = @import("Benchmark.zig");
const options = @import("options.zig");
const PageList = terminalpkg.PageList;
const Terminal = terminalpkg.Terminal;
+const global = @import("../global.zig");
const log = std.log.scoped(.@"scrollback-compression-bench");
@@ -134,10 +135,10 @@ pub fn create(
ptr.* = .{
.opts = opts,
- .terminal = try .init(alloc, .{
+ .terminal = try .init(global.io(), alloc, .{
.rows = opts.@"terminal-rows",
.cols = opts.@"terminal-cols",
- .max_scrollback = opts.@"max-scrollback",
+ .max_scrollback_bytes = opts.@"max-scrollback",
}),
};
return ptr;
@@ -182,13 +183,13 @@ fn setup(ptr: *anyopaque) Benchmark.Error!void {
/// terminal-stream benchmark. Parser and file IO costs remain in setup.
fn loadCorpus(self: *ScrollbackCompression) !void {
const data_file = try options.dataFile(self.opts.data) orelse return;
- defer data_file.close();
+ defer data_file.close(global.io());
var stream = self.terminal.vtStream();
defer stream.deinit();
var read_buf: [64 * 1024]u8 align(std.atomic.cache_line) = undefined;
- var file_reader = data_file.reader(&read_buf);
+ var file_reader = data_file.reader(global.io(), &read_buf);
const reader = &file_reader.interface;
var buf: [64 * 1024]u8 = undefined;
diff --git a/vendor/libghostty-vt/src/benchmark/TerminalFormatter.zig b/vendor/libghostty-vt/src/benchmark/TerminalFormatter.zig
new file mode 100644
index 00000000..fb763ab5
--- /dev/null
+++ b/vendor/libghostty-vt/src/benchmark/TerminalFormatter.zig
@@ -0,0 +1,482 @@
+//! Benchmarks the terminal formatter (`terminal/formatter.zig`).
+//!
+//! The formatter is the hot path for clipboard copy (plain/VT/HTML),
+//! `write_screen_file`, `selectionString`, terminal search window
+//! encoding, and the libghostty-vt formatter C API. This benchmark
+//! measures formatting terminal contents that were built during setup
+//! (outside the timed region) from a pre-generated VT stream.
+//!
+//! ## Input
+//!
+//! `--data` names a pre-generated VT byte stream (for example from
+//! `ghostty-gen styled`). The stream is fed to a terminal of the
+//! requested dimensions with unlimited scrollback during setup. The
+//! resulting screen contents (scrollback included) are what each step
+//! formats.
+//!
+//! ## Modes
+//!
+//! * `noop` performs no formatting and establishes loop/setup overhead.
+//! Subtract this from `format` timings when using hyperfine.
+//! * `format` formats the configured region once per loop into a
+//! reusable buffer. Buffer growth happens on the first iteration only.
+//! * `report` formats once and prints content/output sizes. It is for
+//! computing throughput (cells/s, bytes/s), not timing comparisons.
+//!
+//! ## Examples
+//!
+//! Build benchmarks in ReleaseFast mode:
+//!
+//! zig build -Demit-bench -Doptimize=ReleaseFast -Demit-macos-app=false
+//!
+//! Generate a deterministic corpus, then measure:
+//!
+//! ghostty-gen styled --seed=42 | head -c 640000 > /tmp/plain.vt
+//! hyperfine --warmup 3 \
+//! 'ghostty-bench +terminal-formatter --mode=noop --data=/tmp/plain.vt' \
+//! 'ghostty-bench +terminal-formatter --emit=plain --loops=50 --data=/tmp/plain.vt' \
+//! 'ghostty-bench +terminal-formatter --emit=vt --loops=50 --data=/tmp/plain.vt'
+const TerminalFormatter = @This();
+
+const std = @import("std");
+const assert = std.debug.assert;
+const Allocator = std.mem.Allocator;
+const terminalpkg = @import("../terminal/main.zig");
+const formatterpkg = terminalpkg.formatter;
+const Benchmark = @import("Benchmark.zig");
+const options = @import("options.zig");
+const Terminal = terminalpkg.Terminal;
+const Selection = terminalpkg.Selection;
+const global = @import("../global.zig");
+
+const log = std.log.scoped(.@"terminal-formatter-bench");
+
+alloc: Allocator,
+opts: Options,
+terminal: ?Terminal = null,
+
+/// Reused across steps so buffer growth is a one-time setup cost.
+output: std.Io.Writer.Allocating,
+
+/// Reused pin map storage for `--pin-map=true`.
+pins: formatterpkg.PinMap.Map = .empty,
+
+pub const Options = struct {
+ /// Set by the shared CLI parser for string option ownership.
+ _arena: ?std.heap.ArenaAllocator = null,
+
+ /// Select the operation performed inside the timed benchmark step.
+ mode: Mode = .format,
+
+ /// The output format to emit.
+ emit: Emit = .vt,
+
+ /// The region of the screen to format.
+ region: Region = .screen,
+
+ /// Unwrap soft-wrapped lines.
+ unwrap: bool = false,
+
+ /// Track the source pin of every emitted byte. This exercises the
+ /// (documented as expensive) pin_map path used by selectionString
+ /// and search.
+ @"pin-map": bool = false,
+
+ /// Number of format operations per benchmark step. Increase this
+ /// when the content is too small for stable hyperfine measurements.
+ loops: u32 = 25,
+
+ /// The size of the terminal. This affects wrapping and page sizes.
+ @"terminal-rows": u16 = 24,
+ @"terminal-cols": u16 = 80,
+
+ /// Pre-generated VT stream fed to the terminal during setup. `-`
+ /// reads stdin, although a regular file is recommended so identical
+ /// state can be reused across runs. When unset, the terminal is
+ /// empty.
+ data: ?[]const u8 = null,
+
+ pub fn deinit(self: *Options) void {
+ if (self._arena) |arena| arena.deinit();
+ self.* = undefined;
+ }
+};
+
+pub const Mode = enum {
+ /// Establish the benchmark loop and setup overhead.
+ noop,
+
+ /// Format the configured region once per loop.
+ format,
+
+ /// Print content and output sizes. Not a timing benchmark.
+ report,
+
+ /// Semantic verification, not a timing benchmark: format the
+ /// terminal (must be `--emit=vt`), feed the output into a fresh
+ /// terminal of the same dimensions, format that, and verify the
+ /// two outputs converge to identical bytes. This proves the VT
+ /// output faithfully reconstructs the terminal content even when
+ /// the exact byte encoding changes.
+ roundtrip,
+};
+
+pub const Emit = enum {
+ plain,
+ vt,
+ html,
+
+ fn format(self: Emit) formatterpkg.Format {
+ return switch (self) {
+ .plain => .plain,
+ .vt => .vt,
+ .html => .html,
+ };
+ }
+};
+
+pub const Region = enum {
+ /// Everything: scrollback and active screen.
+ screen,
+
+ /// Only the active screen (bottom rows).
+ active,
+
+ /// Only the scrollback.
+ history,
+};
+
+pub fn create(
+ alloc: Allocator,
+ opts: Options,
+) !*TerminalFormatter {
+ const ptr = try alloc.create(TerminalFormatter);
+ errdefer alloc.destroy(ptr);
+ ptr.* = .{
+ .alloc = alloc,
+ .opts = opts,
+ .output = .init(alloc),
+ };
+ return ptr;
+}
+
+pub fn destroy(self: *TerminalFormatter, alloc: Allocator) void {
+ if (self.terminal) |*t| t.deinit(self.alloc);
+ self.output.deinit();
+ self.pins.deinit(self.alloc);
+ alloc.destroy(self);
+}
+
+pub fn benchmark(self: *TerminalFormatter) Benchmark {
+ return .init(self, .{
+ .stepFn = switch (self.opts.mode) {
+ .noop => stepNoop,
+ .format => stepFormat,
+ .report => stepReport,
+ .roundtrip => stepRoundtrip,
+ },
+ .setupFn = setup,
+ .teardownFn = teardown,
+ });
+}
+
+/// Build the terminal state every mode shares. All of this is outside
+/// the timed region for `Benchmark`, but is included in whole-process
+/// timings, hence the `noop` mode.
+fn setup(ptr: *anyopaque) Benchmark.Error!void {
+ const self: *TerminalFormatter = @ptrCast(@alignCast(ptr));
+ self.setupImpl() catch |err| {
+ log.warn("failed to prepare formatter benchmark err={}", .{err});
+ return error.BenchmarkFailed;
+ };
+}
+
+fn setupImpl(self: *TerminalFormatter) !void {
+ if (self.terminal) |*t| t.deinit(self.alloc);
+ self.terminal = null;
+ self.terminal = try Terminal.init(global.io(), self.alloc, .{
+ .cols = self.opts.@"terminal-cols",
+ .rows = self.opts.@"terminal-rows",
+ .max_scrollback_bytes = null,
+ .max_scrollback_lines = null,
+ });
+ const terminal = &self.terminal.?;
+
+ // Feed the input corpus through the standard VT stream.
+ if (try options.dataFile(self.opts.data)) |data_f| {
+ defer data_f.close(global.io());
+
+ var stream = terminal.vtStream();
+ defer stream.deinit();
+
+ var read_buf: [4096]u8 align(std.atomic.cache_line) = undefined;
+ var f_reader = data_f.reader(global.io(), &read_buf);
+ const r = &f_reader.interface;
+
+ var buf: [4096]u8 = undefined;
+ while (true) {
+ const n = try r.readSliceShort(&buf);
+ if (n == 0) break; // EOF reached
+ stream.nextSlice(buf[0..n]);
+ }
+ }
+}
+
+fn teardown(ptr: *anyopaque) void {
+ const self: *TerminalFormatter = @ptrCast(@alignCast(ptr));
+ if (self.terminal) |*t| t.deinit(self.alloc);
+ self.terminal = null;
+ self.output.shrinkRetainingCapacity(0);
+ self.pins.clearRetainingCapacity();
+}
+
+/// Build the screen formatter matching our options. This mirrors how
+/// Surface clipboard copy and write_screen_file construct formatters.
+fn formatter(self: *TerminalFormatter) ?formatterpkg.ScreenFormatter {
+ const screen = self.terminal.?.screens.active;
+
+ var f: formatterpkg.ScreenFormatter = .init(screen, .{
+ .emit = self.opts.emit.format(),
+ .unwrap = self.opts.unwrap,
+ });
+
+ f.content = switch (self.opts.region) {
+ .screen => .{ .selection = null },
+
+ inline .active, .history => |region| content: {
+ const tag: terminalpkg.point.Tag = switch (region) {
+ .active => .active,
+ .history => .history,
+ .screen => unreachable,
+ };
+ const tl = screen.pages.getTopLeft(tag);
+ const br = screen.pages.getBottomRight(tag) orelse return null;
+ break :content .{ .selection = Selection.init(tl, br, false) };
+ },
+ };
+
+ return f;
+}
+
+fn stepNoop(ptr: *anyopaque) Benchmark.Error!void {
+ const self: *TerminalFormatter = @ptrCast(@alignCast(ptr));
+ for (0..self.opts.loops) |_| {
+ std.mem.doNotOptimizeAway(self.output.written());
+ }
+}
+
+fn stepFormat(ptr: *anyopaque) Benchmark.Error!void {
+ const self: *TerminalFormatter = @ptrCast(@alignCast(ptr));
+ for (0..self.opts.loops) |_| {
+ self.output.shrinkRetainingCapacity(0);
+
+ var f = self.formatter() orelse continue;
+ if (self.opts.@"pin-map") {
+ self.pins.clearRetainingCapacity();
+ f.pin_map = .{ .alloc = self.alloc, .map = &self.pins };
+ }
+
+ f.format(&self.output.writer) catch |err| {
+ log.warn("formatting failed err={}", .{err});
+ return error.BenchmarkFailed;
+ };
+ std.mem.doNotOptimizeAway(self.output.written());
+ }
+}
+
+/// Print the content dimensions and emitted output size. This shares
+/// the formatting code with format mode but deliberately makes no
+/// timing claims. Use it to compute cells/s and bytes/s from timings.
+fn stepReport(ptr: *anyopaque) Benchmark.Error!void {
+ const self: *TerminalFormatter = @ptrCast(@alignCast(ptr));
+
+ self.output.shrinkRetainingCapacity(0);
+ if (self.formatter()) |f_init| {
+ var f = f_init;
+ if (self.opts.@"pin-map") {
+ self.pins.clearRetainingCapacity();
+ f.pin_map = .{ .alloc = self.alloc, .map = &self.pins };
+ }
+ f.format(&self.output.writer) catch |err| {
+ log.warn("formatting failed err={}", .{err});
+ return error.BenchmarkFailed;
+ };
+ }
+
+ // Count the total pages and rows in the pagelist.
+ const screen = self.terminal.?.screens.active;
+ var pages: usize = 0;
+ var rows: usize = 0;
+ {
+ var node = screen.pages.pages.first;
+ while (node) |n| : (node = n.next) {
+ pages += 1;
+ rows += n.page().size.rows;
+ }
+ }
+
+ // Hash the output (and the pin coordinates, which are stable across
+ // runs unlike the node pointers) so different implementations can be
+ // checked for identical output.
+ const out_hash = std.hash.Wyhash.hash(0, self.output.written());
+ var pin_hasher = std.hash.Wyhash.init(0);
+ for (self.pins.points.items) |coord| {
+ const x: u16 = coord.x;
+ const y: u16 = @intCast(coord.y);
+ pin_hasher.update(std.mem.asBytes(&x));
+ pin_hasher.update(std.mem.asBytes(&y));
+ }
+
+ std.debug.print(
+ "terminal-formatter emit={s} region={s} pages={d} rows={d} " ++
+ "cols={d} cells={d} out_bytes={d} pin_bytes={d} " ++
+ "out_hash={x} pin_hash={x}\n",
+ .{
+ @tagName(self.opts.emit),
+ @tagName(self.opts.region),
+ pages,
+ rows,
+ self.opts.@"terminal-cols",
+ rows * self.opts.@"terminal-cols",
+ self.output.written().len,
+ self.pins.count(),
+ out_hash,
+ pin_hasher.final(),
+ },
+ );
+
+ // Pin map storage details on a separate line so the main report
+ // line remains comparable across implementations.
+ if (self.opts.@"pin-map") {
+ std.debug.print(
+ "terminal-formatter-pins points={d} nodes={d}\n",
+ .{ self.pins.points.items.len, self.pins.nodes.items.len },
+ );
+ }
+}
+
+fn stepRoundtrip(ptr: *anyopaque) Benchmark.Error!void {
+ const self: *TerminalFormatter = @ptrCast(@alignCast(ptr));
+ self.stepRoundtripImpl() catch |err| {
+ log.warn("roundtrip failed err={}", .{err});
+ return error.BenchmarkFailed;
+ };
+}
+
+/// Format the terminal, replay the output into a fresh terminal of the
+/// same dimensions, format that, and require both outputs to be
+/// identical. This verifies that the emitted VT sequences faithfully
+/// reconstruct the terminal contents (text, styles, wrapping) without
+/// requiring any specific byte encoding of the first output.
+fn stepRoundtripImpl(self: *TerminalFormatter) !void {
+ // Format the original terminal.
+ self.output.shrinkRetainingCapacity(0);
+ if (self.formatter()) |f_init| {
+ var f = f_init;
+ try f.format(&self.output.writer);
+ }
+ const first = self.output.written();
+
+ // Replay into a fresh terminal.
+ var t2 = try Terminal.init(global.io(), self.alloc, .{
+ .cols = self.opts.@"terminal-cols",
+ .rows = self.opts.@"terminal-rows",
+ .max_scrollback_bytes = null,
+ .max_scrollback_lines = null,
+ });
+ defer t2.deinit(self.alloc);
+ {
+ var stream = t2.vtStream();
+ defer stream.deinit();
+ stream.nextSlice(first);
+ }
+
+ // Format the replayed terminal identically.
+ var out2: std.Io.Writer.Allocating = .init(self.alloc);
+ defer out2.deinit();
+ var f2: formatterpkg.ScreenFormatter = .init(t2.screens.active, .{
+ .emit = self.opts.emit.format(),
+ .unwrap = self.opts.unwrap,
+ });
+ try f2.format(&out2.writer);
+ const second = out2.written();
+
+ const equal = std.mem.eql(u8, first, second);
+ std.debug.print(
+ "terminal-formatter roundtrip emit={s} bytes={d} replay_bytes={d} equal={}\n",
+ .{ @tagName(self.opts.emit), first.len, second.len, equal },
+ );
+
+ if (!equal) {
+ // Find the first differing offset to ease debugging.
+ const n = @min(first.len, second.len);
+ var i: usize = 0;
+ while (i < n and first[i] == second[i]) i += 1;
+ std.debug.print(
+ "terminal-formatter roundtrip mismatch offset={d} " ++
+ "first={f} second={f}\n",
+ .{
+ i,
+ std.zig.fmtString(first[i -| 32..@min(first.len, i + 32)]),
+ std.zig.fmtString(second[i -| 32..@min(second.len, i + 32)]),
+ },
+ );
+ return error.RoundtripMismatch;
+ }
+}
+
+test TerminalFormatter {
+ const testing = std.testing;
+ const impl: *TerminalFormatter = try .create(testing.allocator, .{});
+ defer impl.destroy(testing.allocator);
+
+ const bench = impl.benchmark();
+ _ = try bench.run(.once);
+}
+
+test "TerminalFormatter roundtrip" {
+ const testing = std.testing;
+ const impl: *TerminalFormatter = try .create(testing.allocator, .{
+ .mode = .roundtrip,
+ .@"terminal-rows" = 4,
+ .@"terminal-cols" = 8,
+ });
+ defer impl.destroy(testing.allocator);
+
+ const bench = impl.benchmark();
+ _ = try bench.run(.once);
+}
+
+test "TerminalFormatter formats all emit formats and regions" {
+ const testing = std.testing;
+
+ inline for (.{ Emit.plain, Emit.vt, Emit.html }) |emit| {
+ inline for (.{ Region.screen, Region.active, Region.history }) |region| {
+ const impl: *TerminalFormatter = try .create(testing.allocator, .{
+ .emit = emit,
+ .region = region,
+ .loops = 1,
+ .@"terminal-rows" = 4,
+ .@"terminal-cols" = 8,
+ });
+ defer impl.destroy(testing.allocator);
+
+ const bench = impl.benchmark();
+ _ = try bench.run(.once);
+ }
+ }
+}
+
+test "TerminalFormatter pin map" {
+ const testing = std.testing;
+ const impl: *TerminalFormatter = try .create(testing.allocator, .{
+ .@"pin-map" = true,
+ .loops = 1,
+ .@"terminal-rows" = 4,
+ .@"terminal-cols" = 8,
+ });
+ defer impl.destroy(testing.allocator);
+
+ const bench = impl.benchmark();
+ _ = try bench.run(.once);
+}
diff --git a/vendor/libghostty-vt/src/benchmark/TerminalParser.zig b/vendor/libghostty-vt/src/benchmark/TerminalParser.zig
index 78c93312..d393301c 100644
--- a/vendor/libghostty-vt/src/benchmark/TerminalParser.zig
+++ b/vendor/libghostty-vt/src/benchmark/TerminalParser.zig
@@ -7,13 +7,14 @@ const Allocator = std.mem.Allocator;
const terminalpkg = @import("../terminal/main.zig");
const Benchmark = @import("Benchmark.zig");
const options = @import("options.zig");
+const global = @import("../global.zig");
const log = std.log.scoped(.@"terminal-stream-bench");
opts: Options,
/// The file, opened in the setup function.
-data_f: ?std.fs.File = null,
+data_f: ?std.Io.File = null,
pub const Options = struct {
/// The data to read as a filepath. If this is "-" then
@@ -61,7 +62,7 @@ fn setup(ptr: *anyopaque) Benchmark.Error!void {
fn teardown(ptr: *anyopaque) void {
const self: *TerminalParser = @ptrCast(@alignCast(ptr));
if (self.data_f) |f| {
- f.close();
+ f.close(global.io());
self.data_f = null;
}
}
@@ -76,7 +77,7 @@ fn step(ptr: *anyopaque) Benchmark.Error!void {
// 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 f_reader = f.reader(global.io(), &read_buf);
var r = &f_reader.interface;
var p: terminalpkg.Parser = .init();
diff --git a/vendor/libghostty-vt/src/benchmark/TerminalResize.zig b/vendor/libghostty-vt/src/benchmark/TerminalResize.zig
new file mode 100644
index 00000000..61b908c8
--- /dev/null
+++ b/vendor/libghostty-vt/src/benchmark/TerminalResize.zig
@@ -0,0 +1,357 @@
+//! This benchmark tests the performance of Terminal.resize, with a
+//! primary focus on column resizes that reflow soft-wrapped text.
+//! Resize happens on the IO thread while holding the terminal lock,
+//! so a slow resize directly translates into dropped input and a
+//! frozen-feeling UI while the user drags the window edge (which
+//! produces a rapid stream of resizes).
+//!
+//! The terminal is populated once during setup (synthetic fill and/or
+//! a data file replayed through the VT stream) and then each step
+//! ping-pongs the terminal between two sizes. A full cycle returns the
+//! terminal to its original dimensions so the state reaches a steady
+//! state after the first cycle and every iteration performs
+//! equivalent work.
+const TerminalResize = @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 global = @import("../global.zig");
+
+const log = std.log.scoped(.@"terminal-resize-bench");
+
+opts: Options,
+alloc: Allocator,
+terminal: Terminal,
+
+pub const Options = struct {
+ /// The resize pattern to benchmark. See Mode.
+ mode: Mode = .cols,
+
+ /// Multiplier on the number of resize cycles each step runs. This
+ /// is useful to make a benchmark run long enough for profiling.
+ loops: u32 = 1,
+
+ /// The initial size of the terminal. This is also the size that
+ /// every resize cycle returns to.
+ @"terminal-rows": u16 = 80,
+ @"terminal-cols": u16 = 120,
+
+ /// The dimensions to resize to for the cols/rows/both modes. If
+ /// unset, they default to half of the respective terminal
+ /// dimension, which forces every soft-wrapped line to rewrap.
+ @"resize-cols": ?u16 = null,
+ @"resize-rows": ?u16 = null,
+
+ /// The number of synthetic lines written to the terminal during
+ /// setup. The content is deterministic: a mix of short lines,
+ /// soft-wrapped long lines, blank lines, styled cells, and wide
+ /// characters, since all of these hit different reflow paths.
+ /// Set to 0 to only use `data`.
+ @"fill-lines": u32 = 10_000,
+
+ /// The maximum scrollback size in bytes. Defaults to the Ghostty
+ /// application default. Reflow cost scales with the amount of
+ /// scrollback, not just the visible screen.
+ @"scrollback-bytes": usize = 50_000_000,
+
+ /// The data to read as a filepath. If this is "-" then
+ /// we will read stdin. If this is unset, only the synthetic fill
+ /// is used. The data is streamed into the terminal during setup
+ /// (not part of the benchmark) to build the screen contents that
+ /// get resized.
+ data: ?[]const u8 = null,
+};
+
+pub const Mode = enum {
+ /// Resize to the current dimensions. This measures the early-exit
+ /// path (mode updates, pixel geometry) and acts as a baseline.
+ noop,
+
+ /// Alternate the column count between `terminal-cols` and
+ /// `resize-cols`. With wraparound enabled (the default), this
+ /// reflows text in both directions: shrinking wraps long lines
+ /// and growing unwraps them. This is the primary reflow benchmark.
+ cols,
+
+ /// Like `cols`, but with wraparound mode disabled so the resize
+ /// does not reflow. Useful as a baseline to isolate the cost of
+ /// reflow itself from the rest of the resize.
+ @"cols-no-reflow",
+
+ /// Alternate the row count between `terminal-rows` and
+ /// `resize-rows`. Column count is unchanged so no text reflows;
+ /// this measures growing/trimming rows against scrollback.
+ rows,
+
+ /// Alternate both dimensions at once, like a diagonal window drag.
+ both,
+};
+
+pub fn create(
+ alloc: Allocator,
+ opts: Options,
+) !*TerminalResize {
+ const ptr = try alloc.create(TerminalResize);
+ errdefer alloc.destroy(ptr);
+
+ ptr.* = .{
+ .opts = opts,
+ .alloc = alloc,
+ .terminal = try .init(global.io(), alloc, .{
+ .rows = opts.@"terminal-rows",
+ .cols = opts.@"terminal-cols",
+ .max_scrollback_bytes = opts.@"scrollback-bytes",
+ }),
+ };
+
+ return ptr;
+}
+
+pub fn destroy(self: *TerminalResize, alloc: Allocator) void {
+ self.terminal.deinit(alloc);
+ alloc.destroy(self);
+}
+
+pub fn benchmark(self: *TerminalResize) Benchmark {
+ return .init(self, .{
+ .stepFn = switch (self.opts.mode) {
+ .noop => stepNoop,
+ .cols, .@"cols-no-reflow" => stepCols,
+ .rows => stepRows,
+ .both => stepBoth,
+ },
+ .setupFn = setup,
+ });
+}
+
+/// The column count used by the cols/both modes.
+fn targetCols(self: *const TerminalResize) u16 {
+ return self.opts.@"resize-cols" orelse
+ @max(1, self.opts.@"terminal-cols" / 2);
+}
+
+/// The row count used by the rows/both modes.
+fn targetRows(self: *const TerminalResize) u16 {
+ return self.opts.@"resize-rows" orelse
+ @max(1, self.opts.@"terminal-rows" / 2);
+}
+
+fn setup(ptr: *anyopaque) Benchmark.Error!void {
+ const self: *TerminalResize = @ptrCast(@alignCast(ptr));
+
+ // Always reset our terminal state. Note this doesn't resize, but
+ // create initializes (and steps return) the terminal to the
+ // requested dimensions so we're always at terminal-rows/cols here.
+ self.terminal.fullReset();
+ assert(self.terminal.cols == self.opts.@"terminal-cols");
+ assert(self.terminal.rows == self.opts.@"terminal-rows");
+
+ // Fill with synthetic content first, then replay the data file on
+ // top if given. Both go through the VT stream so soft wraps,
+ // styles, etc. are all set exactly as they would be in a real
+ // session.
+ self.fill();
+ try self.replayData();
+
+ // Reflow only happens when wraparound mode is set (it is by
+ // default). We only disable it after filling so the fill itself
+ // still soft-wraps identically in every mode.
+ if (self.opts.mode == .@"cols-no-reflow") {
+ self.terminal.modes.set(.wraparound, false);
+ }
+}
+
+/// Write deterministic synthetic content to the terminal. The goal is
+/// content that is representative of a real session so that reflow
+/// touches its interesting paths: soft-wrapped lines (must rewrap),
+/// short lines (copied as-is), blank lines, styled cells (styles must
+/// be moved across pages), and wide characters (can't be split at the
+/// wrap column).
+fn fill(self: *TerminalResize) void {
+ if (self.opts.@"fill-lines" == 0) return;
+
+ var s = self.terminal.vtStream();
+ defer s.deinit();
+
+ var prng: std.Random.DefaultPrng = .init(0xB3);
+ const rand = prng.random();
+
+ const cols: usize = self.terminal.cols;
+ var line_buf: [8192]u8 = undefined;
+
+ for (0..self.opts.@"fill-lines") |i| {
+ // Periodically toggle a background style so reflow has to
+ // carry styled cells into new pages.
+ if (i % 64 == 0) s.nextSlice("\x1b[48;2;20;40;60m");
+ if (i % 64 == 32) s.nextSlice("\x1b[m");
+
+ // A small portion of lines are blank.
+ if (i % 16 == 15) {
+ s.nextSlice("\r\n");
+ continue;
+ }
+
+ // Line lengths between ~25% and ~250% of the terminal width
+ // so we get a mix of short lines and soft-wrapped lines.
+ const min = @max(1, cols / 4);
+ const max = @min(line_buf.len, cols * 5 / 2);
+ const len = min + rand.uintLessThan(usize, max - min);
+
+ var j: usize = 0;
+ while (j < len) {
+ // Sprinkle wide characters into every 8th line.
+ if (i % 8 == 7 and j % 16 == 8 and j + 3 <= len) {
+ line_buf[j..][0..3].* = "漢".*;
+ j += 3;
+ continue;
+ }
+
+ // Words of ASCII separated by spaces.
+ line_buf[j] = if (j % 8 == 7)
+ ' '
+ else
+ rand.intRangeAtMost(u8, 'a', 'z');
+ j += 1;
+ }
+
+ s.nextSlice(line_buf[0..len]);
+ s.nextSlice("\r\n");
+ }
+}
+
+/// Stream the data file (if any) into the terminal.
+fn replayData(self: *TerminalResize) Benchmark.Error!void {
+ const data_f: std.Io.File = (options.dataFile(
+ self.opts.data,
+ ) catch |err| {
+ log.warn("error opening data file err={}", .{err});
+ return error.BenchmarkFailed;
+ }) orelse return;
+ defer data_f.close(global.io());
+
+ 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(global.io(), &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 resizeTerminal(
+ self: *TerminalResize,
+ cols: u16,
+ rows: u16,
+) Benchmark.Error!void {
+ self.terminal.resize(self.alloc, .{
+ .cols = cols,
+ .rows = rows,
+ // Realistic cell pixel geometry: real apprt resizes always
+ // carry it, and it exercises the pixel dimension updates.
+ .cell_size_px = .{ .width = 10, .height = 20 },
+ }) catch |err| {
+ log.warn("error resizing terminal err={}", .{err});
+ return error.BenchmarkFailed;
+ };
+ std.mem.doNotOptimizeAway(&self.terminal);
+}
+
+fn stepNoop(ptr: *anyopaque) Benchmark.Error!void {
+ const self: *TerminalResize = @ptrCast(@alignCast(ptr));
+
+ const cols = self.opts.@"terminal-cols";
+ const rows = self.opts.@"terminal-rows";
+
+ // We loop because it's so fast (a few ns) that a single resize
+ // doesn't properly capture our speeds.
+ for (0..50_000_000 * @as(u64, self.opts.loops)) |_| {
+ try self.resizeTerminal(cols, rows);
+ }
+}
+
+fn stepCols(ptr: *anyopaque) Benchmark.Error!void {
+ const self: *TerminalResize = @ptrCast(@alignCast(ptr));
+
+ const cols = self.opts.@"terminal-cols";
+ const rows = self.opts.@"terminal-rows";
+ const target = self.targetCols();
+
+ // Per-cycle cost differs by orders of magnitude between the two
+ // modes (a reflow resize walks the entire scrollback; a non-reflow
+ // resize doesn't), so pick a cycle count that makes each run long
+ // enough to measure well above process startup and setup cost.
+ const cycles: u64 = switch (self.opts.mode) {
+ .cols => 25,
+ .@"cols-no-reflow" => 1_500,
+ else => unreachable,
+ };
+
+ // Each cycle shrinks (rewrapping long lines) and grows back
+ // (unwrapping them), ending at the original size.
+ for (0..cycles * @as(u64, self.opts.loops)) |_| {
+ try self.resizeTerminal(target, rows);
+ try self.resizeTerminal(cols, rows);
+ }
+}
+
+fn stepRows(ptr: *anyopaque) Benchmark.Error!void {
+ const self: *TerminalResize = @ptrCast(@alignCast(ptr));
+
+ const cols = self.opts.@"terminal-cols";
+ const rows = self.opts.@"terminal-rows";
+ const target = self.targetRows();
+
+ // Row-only resizes don't reflow so they're much cheaper (tens of
+ // ns); loop a lot more so the measurement isn't dominated by
+ // process overhead.
+ for (0..2_500_000 * @as(u64, self.opts.loops)) |_| {
+ try self.resizeTerminal(cols, target);
+ try self.resizeTerminal(cols, rows);
+ }
+}
+
+fn stepBoth(ptr: *anyopaque) Benchmark.Error!void {
+ const self: *TerminalResize = @ptrCast(@alignCast(ptr));
+
+ const cols = self.opts.@"terminal-cols";
+ const rows = self.opts.@"terminal-rows";
+ const target_cols = self.targetCols();
+ const target_rows = self.targetRows();
+
+ for (0..25 * @as(u64, self.opts.loops)) |_| {
+ try self.resizeTerminal(target_cols, target_rows);
+ try self.resizeTerminal(cols, rows);
+ }
+}
+
+test TerminalResize {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ // Small dimensions and fill so this is fast in debug builds while
+ // still exercising real reflow in both directions.
+ const impl: *TerminalResize = try .create(alloc, .{
+ .mode = .cols,
+ .@"terminal-rows" = 10,
+ .@"terminal-cols" = 20,
+ .@"fill-lines" = 50,
+ });
+ defer impl.destroy(alloc);
+
+ const bench = impl.benchmark();
+ _ = try bench.run(.once);
+}
diff --git a/vendor/libghostty-vt/src/benchmark/TerminalSnapshot.zig b/vendor/libghostty-vt/src/benchmark/TerminalSnapshot.zig
new file mode 100644
index 00000000..aea5e1f9
--- /dev/null
+++ b/vendor/libghostty-vt/src/benchmark/TerminalSnapshot.zig
@@ -0,0 +1,350 @@
+//! Benchmarks the terminal binary snapshot codecs (`terminal/snapshot`).
+//!
+//! Encoding and decoding a snapshot are the hot paths for terminal
+//! persistence and handoff, so both directions are measured against the
+//! same terminal state.
+//!
+//! ## Input
+//!
+//! `--data` names a pre-generated VT byte stream (for example from
+//! `ghostty-gen ascii`). The stream is fed to a terminal of the requested
+//! dimensions during setup, outside the timed region. The resulting screen
+//! and scrollback contents are what each step encodes or decodes.
+//!
+//! ## Modes
+//!
+//! * `noop` performs no codec work and establishes loop overhead.
+//! * `encode` encodes one complete snapshot per loop into a reusable
+//! buffer. Buffer growth happens on the first iteration only.
+//! * `decode` restores one complete snapshot per loop from bytes prepared
+//! during setup. The restored terminal is destroyed inside the step, so
+//! this measures the complete restore lifecycle.
+//! * `decode-ready` restores only the renderable prefix per loop using the
+//! incremental decoder and stops at READY, skipping history. Compared
+//! against `decode`, this is the time-to-interactive win of applying
+//! history off the critical path.
+//! * `report` encodes once and prints total and per-record-tag sizes. It
+//! is for inspecting the wire shape, not timing comparisons.
+//!
+//! ## Examples
+//!
+//! Build benchmarks in ReleaseFast mode:
+//!
+//! zig build -Demit-bench -Doptimize=ReleaseFast -Demit-macos-app=false
+//!
+//! Generate a deterministic corpus, then measure both directions:
+//!
+//! ghostty-gen ascii | head -c 1000000 > /tmp/ascii.vt
+//! hyperfine --warmup 3 \
+//! 'ghostty-bench +terminal-snapshot --mode=encode --loops=20 --data=/tmp/ascii.vt' \
+//! 'ghostty-bench +terminal-snapshot --mode=decode --loops=20 --data=/tmp/ascii.vt'
+const TerminalSnapshot = @This();
+
+const std = @import("std");
+const assert = std.debug.assert;
+const Allocator = std.mem.Allocator;
+const terminalpkg = @import("../terminal/main.zig");
+const snapshot = terminalpkg.snapshot;
+const Benchmark = @import("Benchmark.zig");
+const options = @import("options.zig");
+const Terminal = terminalpkg.Terminal;
+const global = @import("../global.zig");
+
+const log = std.log.scoped(.@"terminal-snapshot-bench");
+
+alloc: Allocator,
+opts: Options,
+terminal: ?Terminal = null,
+
+/// Reused across encode steps so buffer growth is a one-time setup cost.
+encoded: std.Io.Writer.Allocating,
+
+pub const Options = struct {
+ /// Set by the shared CLI parser for string option ownership.
+ _arena: ?std.heap.ArenaAllocator = null,
+
+ /// Select the operation performed inside the timed benchmark step.
+ mode: Mode = .encode,
+
+ /// Number of codec operations per benchmark step. Increase this when
+ /// the state is too small for stable `hyperfine` measurements.
+ loops: u32 = 1,
+
+ /// The size of the terminal. This affects wrapping, page sizes, and
+ /// the amount of state per encoded page.
+ @"terminal-rows": u16 = 24,
+ @"terminal-cols": u16 = 80,
+
+ /// Pre-generated VT stream fed to the terminal during setup. `-` reads
+ /// stdin, although a regular file is recommended so identical state can
+ /// be reused across runs. When unset, the terminal is empty.
+ data: ?[]const u8 = null,
+
+ pub fn deinit(self: *Options) void {
+ if (self._arena) |arena| arena.deinit();
+ self.* = undefined;
+ }
+};
+
+pub const Mode = enum {
+ /// Establish the benchmark loop overhead.
+ noop,
+
+ /// Encode one complete snapshot per loop.
+ encode,
+
+ /// Restore one complete snapshot per loop, including its teardown.
+ decode,
+
+ /// Restore one renderable READY prefix per loop, including teardown.
+ @"decode-ready",
+
+ /// Print encoded sizes by record tag. Not a timing benchmark.
+ report,
+};
+
+pub fn create(
+ alloc: Allocator,
+ opts: Options,
+) !*TerminalSnapshot {
+ const ptr = try alloc.create(TerminalSnapshot);
+ errdefer alloc.destroy(ptr);
+ ptr.* = .{
+ .alloc = alloc,
+ .opts = opts,
+ .encoded = .init(alloc),
+ };
+ return ptr;
+}
+
+pub fn destroy(self: *TerminalSnapshot, alloc: Allocator) void {
+ if (self.terminal) |*t| t.deinit(self.alloc);
+ self.encoded.deinit();
+ alloc.destroy(self);
+}
+
+pub fn benchmark(self: *TerminalSnapshot) Benchmark {
+ return .init(self, .{
+ .stepFn = switch (self.opts.mode) {
+ .noop => stepNoop,
+ .encode => stepEncode,
+ .decode => stepDecode,
+ .@"decode-ready" => stepDecodeReady,
+ .report => stepReport,
+ },
+ .setupFn = setup,
+ .teardownFn = teardown,
+ });
+}
+
+/// Build the terminal state every mode shares and, for decode mode, the
+/// encoded snapshot it restores. All of this is outside the timed region.
+fn setup(ptr: *anyopaque) Benchmark.Error!void {
+ const self: *TerminalSnapshot = @ptrCast(@alignCast(ptr));
+ self.setupImpl() catch |err| {
+ log.warn("failed to prepare snapshot benchmark err={}", .{err});
+ return error.BenchmarkFailed;
+ };
+}
+
+fn setupImpl(self: *TerminalSnapshot) !void {
+ if (self.terminal) |*t| t.deinit(self.alloc);
+ self.terminal = null;
+ self.terminal = try Terminal.init(global.io(), self.alloc, .{
+ .cols = self.opts.@"terminal-cols",
+ .rows = self.opts.@"terminal-rows",
+ .max_scrollback_bytes = null,
+ .max_scrollback_lines = null,
+ });
+ const terminal = &self.terminal.?;
+
+ // Feed the input corpus through the standard VT stream.
+ if (try options.dataFile(self.opts.data)) |data_f| {
+ defer data_f.close(global.io());
+
+ var stream = terminal.vtStream();
+ defer stream.deinit();
+
+ var read_buf: [4096]u8 align(std.atomic.cache_line) = undefined;
+ var f_reader = data_f.reader(global.io(), &read_buf);
+ const r = &f_reader.interface;
+
+ var buf: [4096]u8 = undefined;
+ while (true) {
+ const n = try r.readSliceShort(&buf);
+ if (n == 0) break; // EOF reached
+ stream.nextSlice(buf[0..n]);
+ }
+ }
+
+ // Decode restores the same bytes every step. Encode mode also uses
+ // this buffer, in which case this is its one-time growth.
+ self.encoded.shrinkRetainingCapacity(0);
+ try snapshot.encode(
+ self.alloc,
+ &self.encoded.writer,
+ terminal,
+ .{ .continuation = .ground },
+ );
+}
+
+fn teardown(ptr: *anyopaque) void {
+ const self: *TerminalSnapshot = @ptrCast(@alignCast(ptr));
+ if (self.terminal) |*t| t.deinit(self.alloc);
+ self.terminal = null;
+ self.encoded.shrinkRetainingCapacity(0);
+}
+
+fn stepNoop(ptr: *anyopaque) Benchmark.Error!void {
+ const self: *TerminalSnapshot = @ptrCast(@alignCast(ptr));
+ for (0..self.opts.loops) |_| {
+ std.mem.doNotOptimizeAway(self.encoded.written());
+ }
+}
+
+fn stepEncode(ptr: *anyopaque) Benchmark.Error!void {
+ const self: *TerminalSnapshot = @ptrCast(@alignCast(ptr));
+ const terminal = &self.terminal.?;
+ for (0..self.opts.loops) |_| {
+ self.encoded.shrinkRetainingCapacity(0);
+ snapshot.encode(
+ self.alloc,
+ &self.encoded.writer,
+ terminal,
+ .{ .continuation = .ground },
+ ) catch |err| {
+ log.warn("snapshot encoding failed err={}", .{err});
+ return error.BenchmarkFailed;
+ };
+ std.mem.doNotOptimizeAway(self.encoded.written());
+ }
+}
+
+fn stepDecode(ptr: *anyopaque) Benchmark.Error!void {
+ const self: *TerminalSnapshot = @ptrCast(@alignCast(ptr));
+ const bytes = self.encoded.written();
+ for (0..self.opts.loops) |_| {
+ var reader: std.Io.Reader = .fixed(bytes);
+ var decoded = snapshot.decode(
+ self.alloc,
+ global.io(),
+ &reader,
+ .{ .max_continuation_bytes = 1024 * 1024 },
+ ) catch |err| {
+ log.warn("snapshot decoding failed err={}", .{err});
+ return error.BenchmarkFailed;
+ };
+ std.mem.doNotOptimizeAway(&decoded);
+ decoded.deinit(self.alloc);
+ }
+}
+
+/// The renderable-prefix half of decode: everything through READY, leaving
+/// all history unread. The gap between this and `stepDecode` is the latency
+/// class the incremental decoder removes from time-to-interactive.
+fn stepDecodeReady(ptr: *anyopaque) Benchmark.Error!void {
+ const self: *TerminalSnapshot = @ptrCast(@alignCast(ptr));
+ const bytes = self.encoded.written();
+ for (0..self.opts.loops) |_| {
+ var reader: std.Io.Reader = .fixed(bytes);
+ var decoder: snapshot.Decoder = .init(&reader);
+ var decoded = decoder.ready(
+ self.alloc,
+ global.io(),
+ .{ .max_continuation_bytes = 1024 * 1024 },
+ ) catch |err| {
+ log.warn("snapshot READY decoding failed err={}", .{err});
+ return error.BenchmarkFailed;
+ };
+ std.mem.doNotOptimizeAway(&decoded);
+ decoded.deinit(self.alloc);
+ }
+}
+
+/// Print the encoded size grouped by record tag. This shares the encoder
+/// with encode mode but deliberately makes no timing claims.
+fn stepReport(ptr: *anyopaque) Benchmark.Error!void {
+ const self: *TerminalSnapshot = @ptrCast(@alignCast(ptr));
+ const bytes = self.encoded.written();
+
+ var payload_totals = std.enums.EnumArray(
+ snapshot.record.Tag,
+ u64,
+ ).initFill(0);
+ var record_counts = std.enums.EnumArray(
+ snapshot.record.Tag,
+ u64,
+ ).initFill(0);
+ var record_total: u64 = 0;
+
+ var offset: usize = snapshot.envelope.encoded_len;
+ while (offset + snapshot.record.Header.len <= bytes.len) {
+ const tag_raw = std.mem.readInt(u16, bytes[offset..][0..2], .little);
+ const payload_len = std.mem.readInt(
+ u32,
+ bytes[offset + 2 ..][0..4],
+ .little,
+ );
+ const tag = std.enums.fromInt(snapshot.record.Tag, tag_raw) orelse {
+ log.warn("unknown record tag {}", .{tag_raw});
+ return error.BenchmarkFailed;
+ };
+ payload_totals.getPtr(tag).* += payload_len;
+ record_counts.getPtr(tag).* += 1;
+ record_total += 1;
+ offset += snapshot.record.Header.len + payload_len;
+ }
+
+ var it = payload_totals.iterator();
+ while (it.next()) |entry| {
+ std.debug.print("terminal-snapshot tag={s} records={d} payload={d}\n", .{
+ @tagName(entry.key),
+ record_counts.get(entry.key),
+ entry.value.*,
+ });
+ }
+ std.debug.print(
+ "terminal-snapshot total records={d} framing={d} encoded={d}\n",
+ .{
+ record_total,
+ record_total * snapshot.record.Header.len +
+ snapshot.envelope.encoded_len,
+ bytes.len,
+ },
+ );
+}
+
+test TerminalSnapshot {
+ const testing = std.testing;
+ const impl: *TerminalSnapshot = try .create(testing.allocator, .{});
+ defer impl.destroy(testing.allocator);
+
+ const bench = impl.benchmark();
+ _ = try bench.run(.once);
+}
+
+test "TerminalSnapshot decode round trip" {
+ const testing = std.testing;
+ const impl: *TerminalSnapshot = try .create(testing.allocator, .{
+ .mode = .decode,
+ .@"terminal-rows" = 4,
+ .@"terminal-cols" = 8,
+ });
+ defer impl.destroy(testing.allocator);
+
+ const bench = impl.benchmark();
+ _ = try bench.run(.once);
+}
+
+test "TerminalSnapshot decode READY prefix" {
+ const testing = std.testing;
+ const impl: *TerminalSnapshot = try .create(testing.allocator, .{
+ .mode = .@"decode-ready",
+ .@"terminal-rows" = 4,
+ .@"terminal-cols" = 8,
+ });
+ defer impl.destroy(testing.allocator);
+
+ 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
index a5302005..a16e36e0 100644
--- a/vendor/libghostty-vt/src/benchmark/TerminalStream.zig
+++ b/vendor/libghostty-vt/src/benchmark/TerminalStream.zig
@@ -19,6 +19,7 @@ const Benchmark = @import("Benchmark.zig");
const options = @import("options.zig");
const Terminal = terminalpkg.Terminal;
const Stream = terminalpkg.TerminalStream;
+const global = @import("../global.zig");
const log = std.log.scoped(.@"terminal-stream-bench");
@@ -27,7 +28,7 @@ terminal: Terminal,
stream: Stream,
/// The file, opened in the setup function.
-data_f: ?std.fs.File = null,
+data_f: ?std.Io.File = null,
pub const Options = struct {
/// The size of the terminal. This affects benchmarking when
@@ -36,7 +37,13 @@ pub const Options = struct {
@"terminal-rows": u16 = 80,
@"terminal-cols": u16 = 120,
- /// The data to read as a filepath. If this is "-" then
+ /// Enable opt-in continuation tracking on the stream.
+ @"continuation-enabled": bool = false,
+
+ /// Maximum continuation suffix retained when tracking is enabled.
+ @"continuation-max-bytes": usize = 1024 * 1024,
+
+ /// Pre-generated data from ghostty-gen. 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
@@ -54,13 +61,21 @@ pub fn create(
ptr.* = .{
.opts = opts,
- .terminal = try .init(alloc, .{
+ .terminal = try .init(global.io(), alloc, .{
.rows = opts.@"terminal-rows",
.cols = opts.@"terminal-cols",
}),
.stream = undefined,
};
- ptr.stream = .initAlloc(alloc, .init(&ptr.terminal));
+ errdefer ptr.terminal.deinit(alloc);
+ ptr.stream = .init(.{
+ .allocator = alloc,
+ .handler = .init(&ptr.terminal),
+ .continuation_max_bytes = if (opts.@"continuation-enabled")
+ opts.@"continuation-max-bytes"
+ else
+ null,
+ });
return ptr;
}
@@ -85,8 +100,8 @@ fn setup(ptr: *anyopaque) Benchmark.Error!void {
// Always reset our terminal state
self.terminal.fullReset();
- // Open our data file to prepare for reading. We can do more
- // validation here eventually.
+ // 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});
@@ -97,7 +112,7 @@ fn setup(ptr: *anyopaque) Benchmark.Error!void {
fn teardown(ptr: *anyopaque) void {
const self: *TerminalStream = @ptrCast(@alignCast(ptr));
if (self.data_f) |f| {
- f.close();
+ f.close(global.io());
self.data_f = null;
}
}
@@ -112,8 +127,10 @@ fn step(ptr: *anyopaque) Benchmark.Error!void {
// aren't currently IO bound.
const f = self.data_f orelse return;
- var read_buf: [64 * 1024]u8 align(std.atomic.cache_line) = undefined;
- var f_reader = f.reader(&read_buf);
+ // Unbuffered: readSliceShort below reads directly into `buf`,
+ // avoiding a per-chunk memcpy through an intermediate reader
+ // buffer that would pollute the measurement.
+ var f_reader = f.reader(global.io(), &.{});
const r = &f_reader.interface;
// This buffer size matches the read buffer size used by the
@@ -139,4 +156,12 @@ test TerminalStream {
const bench = impl.benchmark();
_ = try bench.run(.once);
+
+ const tracked: *TerminalStream = try .create(alloc, .{
+ .@"continuation-enabled" = true,
+ });
+ defer tracked.destroy(alloc);
+
+ const tracked_bench = tracked.benchmark();
+ _ = try tracked_bench.run(.once);
}
diff --git a/vendor/libghostty-vt/src/benchmark/cli.zig b/vendor/libghostty-vt/src/benchmark/cli.zig
index dc4d7b8f..f76dfc4b 100644
--- a/vendor/libghostty-vt/src/benchmark/cli.zig
+++ b/vendor/libghostty-vt/src/benchmark/cli.zig
@@ -1,6 +1,7 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const cli = @import("../cli.zig");
+const global = @import("../global.zig");
/// The available actions for the CLI. This is the list of available
/// benchmarks. View docs for each individual one in the predictably
@@ -13,7 +14,10 @@ pub const Action = enum {
@"page-compression",
@"scrollback-compression",
@"screen-clone",
+ @"terminal-formatter",
@"terminal-parser",
+ @"terminal-resize",
+ @"terminal-snapshot",
@"terminal-stream",
@"is-symbol",
@"osc-parser",
@@ -36,7 +40,10 @@ pub const Action = enum {
.@"terminal-stream" => @import("TerminalStream.zig"),
.@"codepoint-width" => @import("CodepointWidth.zig"),
.@"grapheme-break" => @import("GraphemeBreak.zig"),
+ .@"terminal-formatter" => @import("TerminalFormatter.zig"),
.@"terminal-parser" => @import("TerminalParser.zig"),
+ .@"terminal-resize" => @import("TerminalResize.zig"),
+ .@"terminal-snapshot" => @import("TerminalSnapshot.zig"),
.@"is-symbol" => @import("IsSymbol.zig"),
.@"osc-parser" => @import("OscParser.zig"),
};
@@ -44,19 +51,20 @@ pub const Action = enum {
};
/// An entrypoint for the benchmark CLI.
-pub fn main() !void {
+pub fn main(minimal: std.process.Init.Minimal) !void {
+ try global.init(.{ .tool = minimal });
const alloc = std.heap.c_allocator;
- const action_ = try cli.action.detectArgs(Action, alloc);
+ const action_ = try cli.action.detectArgs(Action, alloc, minimal.args);
const action = action_ orelse return error.NoAction;
- try mainAction(alloc, action, .cli);
+ try mainAction(alloc, action, .{ .cli = minimal.args });
}
/// Arguments that can be passed to the benchmark.
pub const Args = union(enum) {
/// The arguments passed to the CLI via argc/argv.
- cli,
+ cli: std.process.Args,
- /// Simple string arguments, parsed via std.process.ArgIteratorGeneral.
+ /// Simple string arguments, parsed via ArgIteratorGeneral.
string: []const u8,
};
@@ -83,13 +91,13 @@ fn mainActionImpl(
var opts: Options = .{};
defer if (@hasDecl(Options, "deinit")) opts.deinit();
switch (args) {
- .cli => {
- var iter = try cli.args.argsIterator(alloc);
+ .cli => |process_args| {
+ var iter = try cli.args.argsIterator(alloc, process_args);
defer iter.deinit();
try cli.args.parse(Options, alloc, &opts, &iter);
},
.string => |str| {
- var iter = try std.process.ArgIteratorGeneral(.{}).init(
+ var iter = try std.process.Args.IteratorGeneral(.{}).init(
alloc,
str,
);
diff --git a/vendor/libghostty-vt/src/benchmark/main.zig b/vendor/libghostty-vt/src/benchmark/main.zig
index f22891c7..2c1f31f5 100644
--- a/vendor/libghostty-vt/src/benchmark/main.zig
+++ b/vendor/libghostty-vt/src/benchmark/main.zig
@@ -6,7 +6,10 @@ pub const CodepointWidth = @import("CodepointWidth.zig");
pub const GraphemeBreak = @import("GraphemeBreak.zig");
pub const HyperlinkMap = @import("HyperlinkMap.zig");
pub const ScreenClone = @import("ScreenClone.zig");
+pub const TerminalFormatter = @import("TerminalFormatter.zig");
pub const TerminalParser = @import("TerminalParser.zig");
+pub const TerminalResize = @import("TerminalResize.zig");
+pub const TerminalSnapshot = @import("TerminalSnapshot.zig");
pub const IsSymbol = @import("IsSymbol.zig");
pub const PageCompression = @import("PageCompression.zig");
pub const ScrollbackCompression = @import("ScrollbackCompression.zig");
diff --git a/vendor/libghostty-vt/src/benchmark/options.zig b/vendor/libghostty-vt/src/benchmark/options.zig
index 049e80f4..9322818d 100644
--- a/vendor/libghostty-vt/src/benchmark/options.zig
+++ b/vendor/libghostty-vt/src/benchmark/options.zig
@@ -1,20 +1,21 @@
//! This file contains helpers for CLI options.
const std = @import("std");
+const global = @import("../global.zig");
/// 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 {
+pub fn dataFile(path_: ?[]const u8) !?std.Io.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();
+ const file = try std.Io.Dir.cwd().openFile(global.io(), path, .{});
+ errdefer file.close(global.io());
return file;
}