diff --git a/src/benchmark/TerminalStream.zig b/src/benchmark/TerminalStream.zig index 8580695f2..6300ba04c 100644 --- a/src/benchmark/TerminalStream.zig +++ b/src/benchmark/TerminalStream.zig @@ -14,36 +14,49 @@ 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 Terminal = terminalpkg.Terminal; const Stream = terminalpkg.Stream(*Handler); +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, - args: Options, + opts: Options, ) !*TerminalStream { const ptr = try alloc.create(TerminalStream); errdefer alloc.destroy(ptr); ptr.* = .{ + .opts = opts, .terminal = try .init(alloc, .{ - .rows = args.@"terminal-rows", - .cols = args.@"terminal-cols", + .rows = opts.@"terminal-rows", + .cols = opts.@"terminal-cols", }), .handler = .{ .t = &ptr.terminal }, .stream = .{ .handler = &ptr.handler }, @@ -61,17 +74,52 @@ 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); + 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 { 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. diff --git a/src/benchmark/cli.zig b/src/benchmark/cli.zig new file mode 100644 index 000000000..781eafd24 --- /dev/null +++ b/src/benchmark/cli.zig @@ -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); +} diff --git a/src/benchmark/main.zig b/src/benchmark/main.zig index 802519e29..93e489578 100644 --- a/src/benchmark/main.zig +++ b/src/benchmark/main.zig @@ -1,3 +1,4 @@ +pub const cli = @import("cli.zig"); pub const Benchmark = @import("Benchmark.zig"); pub const TerminalStream = @import("TerminalStream.zig"); diff --git a/src/build/GhosttyBench.zig b/src/build/GhosttyBench.zig index 9e93a3b85..0dc18aa4d 100644 --- a/src/build/GhosttyBench.zig +++ b/src/build/GhosttyBench.zig @@ -14,6 +14,22 @@ pub fn init( var steps = std.ArrayList(*std.Build.Step.Compile).init(b.allocator); 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 const c_dir_path = b.pathFromRoot("src/bench"); var c_dir = try std.fs.cwd().openDir(c_dir_path, .{ .iterate = true }); diff --git a/src/main_bench.zig b/src/main_bench.zig new file mode 100644 index 000000000..9e4af1fc7 --- /dev/null +++ b/src/main_bench.zig @@ -0,0 +1,3 @@ +const std = @import("std"); +const benchmark = @import("benchmark/main.zig"); +pub const main = benchmark.cli.main;