initial ghostty-bench program

This commit is contained in:
Mitchell Hashimoto
2025-07-08 09:12:03 -07:00
parent 1739418f6f
commit b8f5cf9d52
5 changed files with 133 additions and 4 deletions

View File

@ -14,36 +14,49 @@
const TerminalStream = @This(); const TerminalStream = @This();
const std = @import("std"); const std = @import("std");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const terminalpkg = @import("../terminal/main.zig"); const terminalpkg = @import("../terminal/main.zig");
const Benchmark = @import("Benchmark.zig"); const Benchmark = @import("Benchmark.zig");
const Terminal = terminalpkg.Terminal; const Terminal = terminalpkg.Terminal;
const Stream = terminalpkg.Stream(*Handler); const Stream = terminalpkg.Stream(*Handler);
opts: Options,
terminal: Terminal, terminal: Terminal,
handler: Handler, handler: Handler,
stream: Stream, stream: Stream,
/// The file, opened in the setup function.
data_f: ?std.fs.File = null,
pub const Options = struct { pub const Options = struct {
/// The size of the terminal. This affects benchmarking when /// The size of the terminal. This affects benchmarking when
/// dealing with soft line wrapping and the memory impact /// dealing with soft line wrapping and the memory impact
/// of page sizes. /// of page sizes.
@"terminal-rows": u16 = 80, @"terminal-rows": u16 = 80,
@"terminal-cols": u16 = 120, @"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. /// Create a new terminal stream handler for the given arguments.
pub fn create( pub fn create(
alloc: Allocator, alloc: Allocator,
args: Options, opts: Options,
) !*TerminalStream { ) !*TerminalStream {
const ptr = try alloc.create(TerminalStream); const ptr = try alloc.create(TerminalStream);
errdefer alloc.destroy(ptr); errdefer alloc.destroy(ptr);
ptr.* = .{ ptr.* = .{
.opts = opts,
.terminal = try .init(alloc, .{ .terminal = try .init(alloc, .{
.rows = args.@"terminal-rows", .rows = opts.@"terminal-rows",
.cols = args.@"terminal-cols", .cols = opts.@"terminal-cols",
}), }),
.handler = .{ .t = &ptr.terminal }, .handler = .{ .t = &ptr.terminal },
.stream = .{ .handler = &ptr.handler }, .stream = .{ .handler = &ptr.handler },
@ -61,17 +74,52 @@ pub fn benchmark(self: *TerminalStream) Benchmark {
return .init(self, .{ return .init(self, .{
.stepFn = step, .stepFn = step,
.setupFn = setup, .setupFn = setup,
.teardownFn = teardown,
}); });
} }
fn setup(ptr: *anyopaque) Benchmark.Error!void { fn setup(ptr: *anyopaque) Benchmark.Error!void {
const self: *TerminalStream = @ptrCast(@alignCast(ptr)); const self: *TerminalStream = @ptrCast(@alignCast(ptr));
// Always reset our terminal state
self.terminal.fullReset(); self.terminal.fullReset();
// Open our data file to prepare for reading. We can do more
// validation here eventually.
assert(self.data_f == null);
if (self.opts.data) |path| {
self.data_f = std.fs.cwd().openFile(path, .{}) catch
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 { fn step(ptr: *anyopaque) Benchmark.Error!void {
const self: *TerminalStream = @ptrCast(@alignCast(ptr)); const self: *TerminalStream = @ptrCast(@alignCast(ptr));
_ = self;
// 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 r = std.io.bufferedReader(f.reader());
var buf: [4096]u8 = undefined;
while (true) {
const n = r.read(&buf) catch return error.BenchmarkFailed;
if (n == 0) break; // EOF reached
const chunk = buf[0..n];
self.stream.nextSlice(chunk) catch
return error.BenchmarkFailed;
}
} }
/// Implements the handler interface for the terminal.Stream. /// Implements the handler interface for the terminal.Stream.

61
src/benchmark/cli.zig Normal file
View File

@ -0,0 +1,61 @@
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.
const Action = enum {
@"terminal-stream",
/// 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) {
.@"terminal-stream" => @import("TerminalStream.zig"),
};
}
};
/// An entrypoint for the benchmark CLI.
pub fn main() !void {
// TODO: Better terminal output throughout this, use libvaxis.
const alloc = std.heap.c_allocator;
const action_ = try cli.action.detectArgs(Action, alloc);
const action = action_ orelse return error.NoAction;
// We need a comptime action to get the struct type and do the
// rest.
return switch (action) {
inline else => |comptime_action| {
const BenchmarkImpl = Action.Struct(comptime_action);
try mainAction(BenchmarkImpl, alloc);
},
};
}
fn mainAction(comptime BenchmarkImpl: type, alloc: Allocator) !void {
// First, parse our CLI options.
const Options = BenchmarkImpl.Options;
var opts: Options = .{};
defer if (@hasDecl(Options, "deinit")) opts.deinit();
{
var iter = try cli.args.argsIterator(alloc);
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);
}

View File

@ -1,3 +1,4 @@
pub const cli = @import("cli.zig");
pub const Benchmark = @import("Benchmark.zig"); pub const Benchmark = @import("Benchmark.zig");
pub const TerminalStream = @import("TerminalStream.zig"); pub const TerminalStream = @import("TerminalStream.zig");

View File

@ -14,6 +14,22 @@ pub fn init(
var steps = std.ArrayList(*std.Build.Step.Compile).init(b.allocator); var steps = std.ArrayList(*std.Build.Step.Compile).init(b.allocator);
errdefer steps.deinit(); errdefer steps.deinit();
// Our new benchmarking application.
{
const exe = b.addExecutable(.{
.name = "ghostty-bench",
.root_module = b.createModule(.{
.root_source_file = b.path("src/main_bench.zig"),
.target = deps.config.target,
// We always want our benchmarks to be in release mode.
.optimize = .ReleaseFast,
}),
});
exe.linkLibC();
_ = try deps.add(exe);
try steps.append(exe);
}
// Open the directory ./src/bench // Open the directory ./src/bench
const c_dir_path = b.pathFromRoot("src/bench"); const c_dir_path = b.pathFromRoot("src/bench");
var c_dir = try std.fs.cwd().openDir(c_dir_path, .{ .iterate = true }); var c_dir = try std.fs.cwd().openDir(c_dir_path, .{ .iterate = true });

3
src/main_bench.zig Normal file
View File

@ -0,0 +1,3 @@
const std = @import("std");
const benchmark = @import("benchmark/main.zig");
pub const main = benchmark.cli.main;