mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-07-26 18:52:01 +00:00
This commit adds a few new mode flags to the `bench-stream` program to generator synthetic OSC sequences. The new modes are `gen-osc`, `gen-osc-valid`, and `gen-osc-invalid`. The `gen-osc` mode generates equal parts valid and invalid OSC sequences, while the suffixed variants are for generating only valid or invalid sequences, respectively. This commit also fixes our build system to actually be able to build the benchmarks. It turns out we were just rebuilding the main Ghostty binary for `-Demit-bench`. And, our benchmarks didn't run under Zig 0.14, which is now fixed. An important new design I'm working towards in this commit is to split out synthetic data generation to a dedicated package in `src/bench/synth` although I'm tempted to move it to `src/synth` since it may be useful outside of benchmarks. The synth package is a work-in-progress, but it contains a hint of what's to come. I ultimately want to able to generate all kinds of synthetic data with a lot of knobs to control dimensionality (e.g. in the case of OSC sequences: valid/invalid, length, operation types, etc.).
205 lines
5.7 KiB
Zig
205 lines
5.7 KiB
Zig
//! This benchmark tests the throughput of codepoint width calculation.
|
|
//! This is a common operation in terminal character printing and the
|
|
//! motivating factor to write this benchmark was discovering that our
|
|
//! codepoint width function was 30% of the runtime of every character
|
|
//! print.
|
|
//!
|
|
//! This will consume all of the available stdin, so you should run it
|
|
//! with `head` in a pipe to restrict. For example, to test ASCII input:
|
|
//!
|
|
//! bench-stream --mode=gen-ascii | head -c 50M | bench-codepoint-width --mode=ziglyph
|
|
//!
|
|
|
|
const std = @import("std");
|
|
const assert = std.debug.assert;
|
|
const Allocator = std.mem.Allocator;
|
|
const ArenaAllocator = std.heap.ArenaAllocator;
|
|
const ziglyph = @import("ziglyph");
|
|
const cli = @import("../cli.zig");
|
|
const simd = @import("../simd/main.zig");
|
|
const table = @import("../unicode/main.zig").table;
|
|
const UTF8Decoder = @import("../terminal/UTF8Decoder.zig");
|
|
|
|
const Args = struct {
|
|
mode: Mode = .noop,
|
|
|
|
/// The size for read buffers. Doesn't usually need to be changed. The
|
|
/// main point is to make this runtime known so we can avoid compiler
|
|
/// optimizations.
|
|
@"buffer-size": usize = 4096,
|
|
|
|
/// This is set by the CLI parser for deinit.
|
|
_arena: ?ArenaAllocator = null,
|
|
|
|
pub fn deinit(self: *Args) void {
|
|
if (self._arena) |arena| arena.deinit();
|
|
self.* = undefined;
|
|
}
|
|
};
|
|
|
|
const Mode = enum {
|
|
/// The baseline mode copies the data from the fd into a buffer. This
|
|
/// is used to show the minimal overhead of reading the fd into memory
|
|
/// and establishes a baseline for the other modes.
|
|
noop,
|
|
|
|
/// libc wcwidth
|
|
wcwidth,
|
|
|
|
/// Use ziglyph library to calculate the display width of each codepoint.
|
|
ziglyph,
|
|
|
|
/// Our SIMD implementation.
|
|
simd,
|
|
|
|
/// Test our lookup table implementation.
|
|
table,
|
|
};
|
|
|
|
pub const std_options: std.Options = .{
|
|
.log_level = .debug,
|
|
};
|
|
|
|
pub fn main() !void {
|
|
// We want to use the c allocator because it is much faster than GPA.
|
|
const alloc = std.heap.c_allocator;
|
|
|
|
// Parse our args
|
|
var args: Args = .{};
|
|
defer args.deinit();
|
|
{
|
|
var iter = try cli.args.argsIterator(alloc);
|
|
defer iter.deinit();
|
|
try cli.args.parse(Args, alloc, &args, &iter);
|
|
}
|
|
|
|
const reader = std.io.getStdIn().reader();
|
|
const buf = try alloc.alloc(u8, args.@"buffer-size");
|
|
|
|
// Handle the modes that do not depend on terminal state first.
|
|
switch (args.mode) {
|
|
.noop => try benchNoop(reader, buf),
|
|
.wcwidth => try benchWcwidth(reader, buf),
|
|
.ziglyph => try benchZiglyph(reader, buf),
|
|
.simd => try benchSimd(reader, buf),
|
|
.table => try benchTable(reader, buf),
|
|
}
|
|
}
|
|
|
|
noinline fn benchNoop(
|
|
reader: anytype,
|
|
buf: []u8,
|
|
) !void {
|
|
var d: UTF8Decoder = .{};
|
|
while (true) {
|
|
const n = try reader.read(buf);
|
|
if (n == 0) break;
|
|
|
|
// Using stream.next directly with a for loop applies a naive
|
|
// scalar approach.
|
|
for (buf[0..n]) |c| {
|
|
_ = d.next(c);
|
|
}
|
|
}
|
|
}
|
|
|
|
extern "c" fn wcwidth(c: u32) c_int;
|
|
|
|
noinline fn benchWcwidth(
|
|
reader: anytype,
|
|
buf: []u8,
|
|
) !void {
|
|
var d: UTF8Decoder = .{};
|
|
while (true) {
|
|
const n = try reader.read(buf);
|
|
if (n == 0) break;
|
|
|
|
// Using stream.next directly with a for loop applies a naive
|
|
// scalar approach.
|
|
for (buf[0..n]) |c| {
|
|
const cp_, const consumed = d.next(c);
|
|
assert(consumed);
|
|
if (cp_) |cp| {
|
|
const width = wcwidth(cp);
|
|
|
|
// Write the width to the buffer to avoid it being compiled away
|
|
buf[0] = @intCast(width);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
noinline fn benchTable(
|
|
reader: anytype,
|
|
buf: []u8,
|
|
) !void {
|
|
var d: UTF8Decoder = .{};
|
|
while (true) {
|
|
const n = try reader.read(buf);
|
|
if (n == 0) break;
|
|
|
|
// Using stream.next directly with a for loop applies a naive
|
|
// scalar approach.
|
|
for (buf[0..n]) |c| {
|
|
const cp_, const consumed = d.next(c);
|
|
assert(consumed);
|
|
if (cp_) |cp| {
|
|
// This is the same trick we do in terminal.zig so we
|
|
// keep it here.
|
|
const width = if (cp <= 0xFF) 1 else table.get(@intCast(cp)).width;
|
|
|
|
// Write the width to the buffer to avoid it being compiled away
|
|
buf[0] = @intCast(width);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
noinline fn benchZiglyph(
|
|
reader: anytype,
|
|
buf: []u8,
|
|
) !void {
|
|
var d: UTF8Decoder = .{};
|
|
while (true) {
|
|
const n = try reader.read(buf);
|
|
if (n == 0) break;
|
|
|
|
// Using stream.next directly with a for loop applies a naive
|
|
// scalar approach.
|
|
for (buf[0..n]) |c| {
|
|
const cp_, const consumed = d.next(c);
|
|
assert(consumed);
|
|
if (cp_) |cp| {
|
|
const width = ziglyph.display_width.codePointWidth(cp, .half);
|
|
|
|
// Write the width to the buffer to avoid it being compiled away
|
|
buf[0] = @intCast(width);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
noinline fn benchSimd(
|
|
reader: anytype,
|
|
buf: []u8,
|
|
) !void {
|
|
var d: UTF8Decoder = .{};
|
|
while (true) {
|
|
const n = try reader.read(buf);
|
|
if (n == 0) break;
|
|
|
|
// Using stream.next directly with a for loop applies a naive
|
|
// scalar approach.
|
|
for (buf[0..n]) |c| {
|
|
const cp_, const consumed = d.next(c);
|
|
assert(consumed);
|
|
if (cp_) |cp| {
|
|
const width = simd.codepointWidth(cp);
|
|
|
|
// Write the width to the buffer to avoid it being compiled away
|
|
buf[0] = @intCast(width);
|
|
}
|
|
}
|
|
}
|
|
}
|