mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-09-14 18:01:58 +00:00
APC payloads such as Kitty graphics images can be megabytes of base64 data, but every byte was dispatched individually: through the VT state machine table, an apc_put action, the stream handler, the APC protocol handler, and finally a per-byte ArrayList append in the Kitty command parser. Five layers of dispatch per byte made large image transfers far slower than they needed to be. Add a bulk fast path alongside the existing CSI fast paths in consumeUntilGround: scan the longest run of apc_put bytes (stopping at any byte the parse table doesn't treat as APC payload: CAN, SUB, ESC, and most C1 bytes exit or abort the string state, and 0xA0-0xFF are ignored by it) and dispatch the run as a single new apc_put_slice action. The APC handler identifies the protocol from the first few bytes as before, then passes the remainder of each slice to the protocol parser in bulk; the Kitty parser appends payload data with a single appendSlice. Ignored/unknown APC sequences now drop each slice in O(1) instead of per-byte dispatch. The fast path is guarded the same way as the CSI fast paths: handlers with a vtRaw hook (the inspector) keep receiving per-byte apc_put actions, and the scalar next() path is unchanged. Also add benchmark support: a `ghostty-gen +kitty` synthetic generator emitting well-formed Kitty graphics transmit commands with 4 KiB random base64 payloads (not valid image data; the corpus exercises the parsing paths, not image decoding), and a `ghostty-bench +apc-parser` benchmark that measures the stream -> APC -> Kitty parse path without image decode/storage. Benchmarks on a 64 MiB corpus (hyperfine, ReleaseFast, x86_64 Linux, baseline is identical source with only the fast path disabled): apc-parser: 1.061 s -> 43 ms (~25x) terminal-stream (kitty): 1.163 s -> 72 ms (~16x) terminal-stream (ascii): no change The ascii case was verified with retired instruction counts (perf stat, pinned to one core) since wall time on the test machine has 4-7 ms of noise: 988,030,458 vs 988,045,833 instructions (+0.0016%), a fixed startup-size delta; the ground-state hot loop never reaches the new branch.
89 lines
2.7 KiB
Zig
89 lines
2.7 KiB
Zig
//! Generates random Kitty graphics protocol APC sequences.
|
|
//!
|
|
//! The payload is random base64, NOT a valid image: decoding it as
|
|
//! PNG will fail. This corpus is meant for benchmarking the stream,
|
|
//! APC, and Kitty command parsing paths, which never decode the
|
|
//! image data. It does not exercise successful image loading.
|
|
const Kitty = @This();
|
|
|
|
const std = @import("std");
|
|
const assert = std.debug.assert;
|
|
const Generator = @import("Generator.zig");
|
|
const Bytes = @import("Bytes.zig");
|
|
|
|
/// Random number generator.
|
|
rand: std.Random,
|
|
|
|
/// The base64 payload length for each generated command. This is
|
|
/// rounded down to a multiple of four so the payload is always valid
|
|
/// base64 without requiring padding. Kitty clients typically chunk
|
|
/// payloads at 4096 bytes.
|
|
data_len: usize = 4096,
|
|
|
|
fn checkBase64Alphabet(c: u8) bool {
|
|
return switch (c) {
|
|
'A'...'Z', 'a'...'z', '0'...'9', '+', '/' => true,
|
|
else => false,
|
|
};
|
|
}
|
|
|
|
/// The base64 alphabet, without the padding character.
|
|
pub const base64_alphabet = Bytes.generateAlphabet(checkBase64Alphabet);
|
|
|
|
pub fn generator(self: *Kitty) Generator {
|
|
return .init(self, next);
|
|
}
|
|
|
|
const prefix = "\x1b_G";
|
|
const st = "\x1b\\";
|
|
|
|
/// Get the next Kitty graphics APC sequence: a well-formed transmit
|
|
/// command with a random base64 payload (not a valid image; see the
|
|
/// module comment), including the APC prefix and the ST terminator.
|
|
pub fn next(
|
|
self: *Kitty,
|
|
writer: *std.Io.Writer,
|
|
max_len: usize,
|
|
) Generator.Error!void {
|
|
var control_buf: [64]u8 = undefined;
|
|
const control = std.fmt.bufPrint(
|
|
&control_buf,
|
|
"a=t,f=100,i={d};",
|
|
.{self.rand.intRangeAtMost(u32, 1, 1_000_000)},
|
|
) catch unreachable;
|
|
|
|
const overhead = prefix.len + control.len + st.len;
|
|
assert(max_len > overhead);
|
|
|
|
const avail = @min(self.data_len, max_len - overhead);
|
|
const payload_len = avail - (avail % 4);
|
|
|
|
try writer.writeAll(prefix);
|
|
try writer.writeAll(control);
|
|
if (payload_len > 0) {
|
|
const bytes: Bytes = .{
|
|
.rand = self.rand,
|
|
.alphabet = base64_alphabet,
|
|
.min_len = payload_len,
|
|
.max_len = payload_len,
|
|
};
|
|
_ = try bytes.write(writer);
|
|
}
|
|
try writer.writeAll(st);
|
|
}
|
|
|
|
test "kitty" {
|
|
const testing = std.testing;
|
|
var prng = std.Random.DefaultPrng.init(0);
|
|
|
|
var buf: [8192]u8 = undefined;
|
|
var writer: std.Io.Writer = .fixed(&buf);
|
|
var v: Kitty = .{ .rand = prng.random() };
|
|
const gen = v.generator();
|
|
try gen.next(&writer, buf.len);
|
|
|
|
const data = writer.buffered();
|
|
try testing.expect(std.mem.startsWith(u8, data, "\x1b_Ga=t,f=100,i="));
|
|
try testing.expect(std.mem.endsWith(u8, data, "\x1b\\"));
|
|
}
|