From a28b7e9205b8d002119926e3251169167be595ac Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 9 Jul 2025 14:42:19 -0700 Subject: [PATCH] synthetic cli (ghostty-gen) --- src/benchmark/cli.zig | 5 +- src/benchmark/main.zig | 2 +- src/build/GhosttyBench.zig | 19 +++++++- src/main_gen.zig | 5 ++ src/synthetic/cli.zig | 95 +++++++++++++++++++++++++++++++++++++ src/synthetic/cli/Ascii.zig | 53 +++++++++++++++++++++ src/synthetic/main.zig | 2 + 7 files changed, 177 insertions(+), 4 deletions(-) create mode 100644 src/main_gen.zig create mode 100644 src/synthetic/cli.zig create mode 100644 src/synthetic/cli/Ascii.zig diff --git a/src/benchmark/cli.zig b/src/benchmark/cli.zig index 3f59b4a72..97bb9c683 100644 --- a/src/benchmark/cli.zig +++ b/src/benchmark/cli.zig @@ -3,12 +3,13 @@ const Allocator = std.mem.Allocator; const cli = @import("../cli.zig"); /// The available actions for the CLI. This is the list of available -/// benchmarks. +/// benchmarks. View docs for each individual one in the predictably +/// named files. pub const Action = enum { - @"terminal-stream", @"codepoint-width", @"grapheme-break", @"terminal-parser", + @"terminal-stream", /// Returns the struct associated with the action. The struct /// should have a few decls: diff --git a/src/benchmark/main.zig b/src/benchmark/main.zig index 56c515c9d..49bb17289 100644 --- a/src/benchmark/main.zig +++ b/src/benchmark/main.zig @@ -7,5 +7,5 @@ pub const GraphemeBreak = @import("GraphemeBreak.zig"); pub const TerminalParser = @import("TerminalParser.zig"); test { - _ = @import("std").testing.refAllDecls(@This()); + @import("std").testing.refAllDecls(@This()); } diff --git a/src/build/GhosttyBench.zig b/src/build/GhosttyBench.zig index 0dc18aa4d..0588bba3c 100644 --- a/src/build/GhosttyBench.zig +++ b/src/build/GhosttyBench.zig @@ -14,7 +14,24 @@ pub fn init( var steps = std.ArrayList(*std.Build.Step.Compile).init(b.allocator); errdefer steps.deinit(); - // Our new benchmarking application. + // Our synthetic data generator + { + const exe = b.addExecutable(.{ + .name = "ghostty-gen", + .root_module = b.createModule(.{ + .root_source_file = b.path("src/main_gen.zig"), + .target = deps.config.target, + // We always want our datagen to be fast because it + // takes awhile to run. + .optimize = .ReleaseFast, + }), + }); + exe.linkLibC(); + _ = try deps.add(exe); + try steps.append(exe); + } + + // Our benchmarking application. { const exe = b.addExecutable(.{ .name = "ghostty-bench", diff --git a/src/main_gen.zig b/src/main_gen.zig new file mode 100644 index 000000000..b988819f8 --- /dev/null +++ b/src/main_gen.zig @@ -0,0 +1,5 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const synthetic = @import("synthetic/main.zig"); + +pub const main = synthetic.cli.main; diff --git a/src/synthetic/cli.zig b/src/synthetic/cli.zig new file mode 100644 index 000000000..7cb2e68d2 --- /dev/null +++ b/src/synthetic/cli.zig @@ -0,0 +1,95 @@ +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 +/// synthetic generators. View docs for each individual one in the +/// predictably named files under `cli/`. +pub const Action = enum { + ascii, + + /// 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 destroy`: Destroy the instance of the action. + /// + /// See TerminalStream for an example. + pub fn Struct(comptime action: Action) type { + return switch (action) { + .ascii => @import("cli/Ascii.zig"), + }; + } +}; + +/// An entrypoint for the synthetic generator 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); +} + +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 Impl = Action.Struct(comptime_action); + try mainActionImpl(Impl, alloc, args); + }, + } +} + +fn mainActionImpl( + comptime Impl: type, + alloc: Allocator, + args: Args, +) !void { + // First, parse our CLI options. + const Options = Impl.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); + }, + } + + // TODO: Make this a command line option. + const seed: u64 = @truncate(@as( + u128, + @bitCast(std.time.nanoTimestamp()), + )); + var prng = std.Random.DefaultPrng.init(seed); + const rand = prng.random(); + + // Our output always goes to stdout. + const writer = std.io.getStdOut().writer(); + + // Create our implementation + const impl = try Impl.create(alloc, opts); + defer impl.destroy(alloc); + try impl.run(writer, rand); +} diff --git a/src/synthetic/cli/Ascii.zig b/src/synthetic/cli/Ascii.zig new file mode 100644 index 000000000..f294be2e0 --- /dev/null +++ b/src/synthetic/cli/Ascii.zig @@ -0,0 +1,53 @@ +//! 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 Ascii = @This(); + +const std = @import("std"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; +const synthetic = @import("../main.zig"); + +const log = std.log.scoped(.@"terminal-stream-bench"); + +pub const Options = struct {}; + +/// Create a new terminal stream handler for the given arguments. +pub fn create( + alloc: Allocator, + _: Options, +) !*Ascii { + const ptr = try alloc.create(Ascii); + errdefer alloc.destroy(ptr); + return ptr; +} + +pub fn destroy(self: *Ascii, alloc: Allocator) void { + alloc.destroy(self); +} + +pub fn run(self: *Ascii, writer: anytype, rand: std.Random) !void { + _ = self; + + var gen: synthetic.Bytes = .{ + .rand = rand, + .alphabet = synthetic.Bytes.Alphabet.ascii, + }; + + var buf: [1024]u8 = undefined; + while (true) { + const data = try gen.next(&buf); + writer.writeAll(data) catch |err| switch (err) { + error.BrokenPipe => return, // stdout closed + else => return err, + }; + } +} + +test Ascii { + const testing = std.testing; + const alloc = testing.allocator; + + const impl: *Ascii = try .create(alloc, .{}); + defer impl.destroy(alloc); +} diff --git a/src/synthetic/main.zig b/src/synthetic/main.zig index 67cd47054..85f9f7d35 100644 --- a/src/synthetic/main.zig +++ b/src/synthetic/main.zig @@ -13,6 +13,8 @@ //! is not limited to that and we may want to extract this to a //! standalone package one day. +pub const cli = @import("cli.zig"); + pub const Generator = @import("Generator.zig"); pub const Bytes = @import("Bytes.zig"); pub const Utf8 = @import("Utf8.zig");