synthetic cli (ghostty-gen)

This commit is contained in:
Mitchell Hashimoto
2025-07-09 14:42:19 -07:00
parent b5ff0442d4
commit a28b7e9205
7 changed files with 177 additions and 4 deletions

View File

@ -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:

View File

@ -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());
}

View File

@ -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",

5
src/main_gen.zig Normal file
View File

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

95
src/synthetic/cli.zig Normal file
View File

@ -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);
}

View File

@ -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);
}

View File

@ -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");