diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-07-16 23:28:05 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-07-16 23:29:28 +0300 |
| commit | 4a3302d19a2baa3a33e910e0088c24e780905b90 (patch) | |
| tree | dcaf76fae1c82102d933c9225b17ca7ac5d3a847 /vendor/libghostty-vt/src/benchmark | |
| parent | 550e9f8cc085f428429f5cf2da0bb2dba897e8b8 (diff) | |
chore: update vendored libghostty-vt
refs #276
Diffstat (limited to 'vendor/libghostty-vt/src/benchmark')
| -rw-r--r-- | vendor/libghostty-vt/src/benchmark/ApcParser.zig | 143 | ||||
| -rw-r--r-- | vendor/libghostty-vt/src/benchmark/HyperlinkMap.zig | 155 | ||||
| -rw-r--r-- | vendor/libghostty-vt/src/benchmark/PageCompression.zig | 519 | ||||
| -rw-r--r-- | vendor/libghostty-vt/src/benchmark/ScreenClone.zig | 113 | ||||
| -rw-r--r-- | vendor/libghostty-vt/src/benchmark/ScrollbackCompression.zig | 343 | ||||
| -rw-r--r-- | vendor/libghostty-vt/src/benchmark/TerminalStream.zig | 49 | ||||
| -rw-r--r-- | vendor/libghostty-vt/src/benchmark/cli.zig | 8 | ||||
| -rw-r--r-- | vendor/libghostty-vt/src/benchmark/main.zig | 3 |
8 files changed, 1297 insertions, 36 deletions
diff --git a/vendor/libghostty-vt/src/benchmark/ApcParser.zig b/vendor/libghostty-vt/src/benchmark/ApcParser.zig new file mode 100644 index 00000000..73b7c57a --- /dev/null +++ b/vendor/libghostty-vt/src/benchmark/ApcParser.zig @@ -0,0 +1,143 @@ +//! This benchmark tests the throughput of APC sequence parsing +//! through the terminal stream: VT state machine dispatch, APC +//! protocol identification, and the protocol command parsers +//! (e.g. Kitty graphics). Completed commands are parsed and then +//! discarded; command execution (image decoding, storage) is not +//! included so this isolates the parsing path. +const ApcParser = @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 options = @import("options.zig"); + +const log = std.log.scoped(.@"apc-parser-bench"); + +opts: Options, +stream: Stream, + +/// The file, opened in the setup function. +data_f: ?std.fs.File = null, + +pub const Options = struct { + /// 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, +}; + +const Stream = terminalpkg.Stream(Handler); + +/// A stream handler that only processes APC actions, parsing and +/// immediately discarding completed commands. +const Handler = struct { + alloc: Allocator, + apc: terminalpkg.apc.Handler = .{}, + + pub fn deinit(self: *Handler) void { + self.apc.deinit(); + } + + pub fn vt( + self: *Handler, + comptime action: Stream.Action.Tag, + value: Stream.Action.Value(action), + ) void { + switch (action) { + .apc_start => self.apc.start(), + .apc_put => self.apc.feed(self.alloc, value), + .apc_put_slice => self.apc.feedSlice(self.alloc, value.bytes), + .apc_end => if (self.apc.end()) |cmd| { + var c = cmd; + std.mem.doNotOptimizeAway(&c); + c.deinit(self.alloc); + }, + else => {}, + } + } +}; + +/// Create a new APC parser benchmark for the given arguments. +pub fn create( + alloc: Allocator, + opts: Options, +) !*ApcParser { + const ptr = try alloc.create(ApcParser); + errdefer alloc.destroy(ptr); + ptr.* = .{ + .opts = opts, + .stream = .init(.{ .alloc = alloc }), + }; + return ptr; +} + +pub fn destroy(self: *ApcParser, alloc: Allocator) void { + self.stream.deinit(); + alloc.destroy(self); +} + +pub fn benchmark(self: *ApcParser) Benchmark { + return .init(self, .{ + .stepFn = step, + .setupFn = setup, + .teardownFn = teardown, + }); +} + +fn setup(ptr: *anyopaque) Benchmark.Error!void { + const self: *ApcParser = @ptrCast(@alignCast(ptr)); + + // Open our data file to prepare for reading. We can do more + // validation here eventually. + assert(self.data_f == null); + self.data_f = options.dataFile(self.opts.data) catch |err| { + log.warn("error opening data file err={}", .{err}); + return error.BenchmarkFailed; + }; +} + +fn teardown(ptr: *anyopaque) void { + const self: *ApcParser = @ptrCast(@alignCast(ptr)); + if (self.data_f) |f| { + f.close(); + self.data_f = null; + } +} + +fn step(ptr: *anyopaque) Benchmark.Error!void { + const self: *ApcParser = @ptrCast(@alignCast(ptr)); + + const f = self.data_f orelse return; + + var read_buf: [64 * 1024]u8 align(std.atomic.cache_line) = undefined; + var f_reader = f.reader(&read_buf); + const r = &f_reader.interface; + + // This buffer size matches the read buffer size used by the + // real IO thread (see termio Exec.zig buffer_capacity) so that + // the benchmark exercises the stream with realistic chunk sizes. + var buf: [64 * 1024]u8 = undefined; + while (true) { + const n = r.readSliceShort(&buf) catch { + log.warn("error reading data file err={?}", .{f_reader.err}); + return error.BenchmarkFailed; + }; + if (n == 0) break; // EOF reached + self.stream.nextSlice(buf[0..n]); + } +} + +test ApcParser { + const testing = std.testing; + const alloc = testing.allocator; + + const impl: *ApcParser = try .create(alloc, .{}); + defer impl.destroy(alloc); + + const bench = impl.benchmark(); + _ = try bench.run(.once); +} diff --git a/vendor/libghostty-vt/src/benchmark/HyperlinkMap.zig b/vendor/libghostty-vt/src/benchmark/HyperlinkMap.zig new file mode 100644 index 00000000..166c3d4f --- /dev/null +++ b/vendor/libghostty-vt/src/benchmark/HyperlinkMap.zig @@ -0,0 +1,155 @@ +//! Benchmark hyperlink cell-map lookups and remove/insert churn. +//! +//! Hyperlink cells are stored in a fixed-capacity, open-addressed hash map. +//! The `churn` mode models terminal output that repeatedly replaces cells in +//! a page whose hyperlink map is already close to full. This is particularly +//! useful for catching probe-length cliffs at high load factors. +const HyperlinkMap = @This(); + +const std = @import("std"); +const Allocator = std.mem.Allocator; +const terminal = @import("../terminal/main.zig"); +const hyperlink = @import("../terminal/hyperlink.zig"); +const Benchmark = @import("Benchmark.zig"); + +const log = std.log.scoped(.@"hyperlink-map-bench"); + +opts: Options, +page: terminal.Page, +link_id: hyperlink.Id, +entry_count: usize, + +pub const Options = struct { + /// Requested hyperlink working-set size. Must be a power of two and at + /// least 16. The map may reserve additional probe headroom. + entries: u16 = 4096, + + /// Percentage of the map populated before the timed operation. + /// Values above 100 are treated as 100. + @"load-percent": u8 = 100, + + /// Number of complete passes over the populated cells per step. + loops: u16 = 1, + + /// Operation to perform in the timed region. + mode: Mode = .churn, +}; + +pub const Mode = enum { + /// Look up every populated hyperlink cell. + lookup, + + /// Remove and reinsert every populated hyperlink cell. + churn, +}; + +pub fn create(alloc: Allocator, opts: Options) !*HyperlinkMap { + if (opts.entries < 16 or !std.math.isPowerOfTwo(opts.entries)) { + log.err("entries must be a power of two greater than or equal to 16", .{}); + return error.InvalidEntries; + } + + const ptr = try alloc.create(HyperlinkMap); + errdefer alloc.destroy(ptr); + + // The page requests one map slot per `hyperlink_cell_multiplier` set + // entries. Keep this relationship explicit so `entries` is the working + // set size under test regardless of the map's reserved probe headroom. + const set_entries = opts.entries / 16; + var page = try terminal.Page.init(.{ + .cols = opts.entries, + .rows = 1, + .hyperlink_bytes = @intCast( + @as(usize, set_entries) * @sizeOf(hyperlink.Set.Item), + ), + }); + errdefer page.deinit(); + + if (page.hyperlinkCapacity() < opts.entries) { + log.err("insufficient map capacity expected_at_least={} actual={}", .{ + opts.entries, + page.hyperlinkCapacity(), + }); + return error.UnexpectedCapacity; + } + + const link_id = try page.insertHyperlink(.{ + .id = .{ .implicit = 1 }, + .uri = "https://example.com/benchmark", + }); + + const load = @min(opts.@"load-percent", 100); + const entry_count = @max( + 1, + @divFloor(@as(usize, opts.entries) * load, 100), + ); + for (0..entry_count) |x| { + const rac = page.getRowAndCell(x, 0); + page.hyperlink_set.use(page.memory, link_id); + try page.setHyperlink(rac.row, rac.cell, link_id); + } + + ptr.* = .{ + .opts = opts, + .page = page, + .link_id = link_id, + .entry_count = entry_count, + }; + return ptr; +} + +pub fn destroy(self: *HyperlinkMap, alloc: Allocator) void { + self.page.deinit(); + alloc.destroy(self); +} + +pub fn benchmark(self: *HyperlinkMap) Benchmark { + return .init(self, .{ + .stepFn = switch (self.opts.mode) { + .lookup => stepLookup, + .churn => stepChurn, + }, + }); +} + +fn stepLookup(ptr: *anyopaque) Benchmark.Error!void { + const self: *HyperlinkMap = @ptrCast(@alignCast(ptr)); + + for (0..self.opts.loops) |_| { + for (0..self.entry_count) |x| { + const cell = self.page.getRowAndCell(x, 0).cell; + const id = self.page.lookupHyperlink(cell) orelse + return error.BenchmarkFailed; + std.mem.doNotOptimizeAway(id); + } + } +} + +fn stepChurn(ptr: *anyopaque) Benchmark.Error!void { + const self: *HyperlinkMap = @ptrCast(@alignCast(ptr)); + + for (0..self.opts.loops) |_| { + for (0..self.entry_count) |x| { + const rac = self.page.getRowAndCell(x, 0); + self.page.clearHyperlink(rac.cell); + self.page.hyperlink_set.use(self.page.memory, self.link_id); + self.page.setHyperlink(rac.row, rac.cell, self.link_id) catch + return error.BenchmarkFailed; + } + } +} + +test HyperlinkMap { + const alloc = std.testing.allocator; + + inline for (.{ Mode.lookup, Mode.churn }) |mode| { + const impl = try HyperlinkMap.create(alloc, .{ + .entries = 64, + .mode = mode, + }); + defer impl.destroy(alloc); + + const bench = impl.benchmark(); + _ = try bench.run(.once); + } +} diff --git a/vendor/libghostty-vt/src/benchmark/PageCompression.zig b/vendor/libghostty-vt/src/benchmark/PageCompression.zig new file mode 100644 index 00000000..21153659 --- /dev/null +++ b/vendor/libghostty-vt/src/benchmark/PageCompression.zig @@ -0,0 +1,519 @@ +//! Benchmarks raw LZ4 compression and decompression on page-sized byte +//! buffers. +//! +//! This benchmark is intentionally independent of terminal page ownership and +//! lifecycle. It treats its input as opaque bytes and calls only the standalone +//! LZ4 block codec. In particular, it does not compress pages owned by a live +//! terminal or measure the scheduling and state transitions used by automatic +//! scrollback compression. +//! +//! ## Input +//! +//! `--data` names a pre-generated raw byte corpus. The corpus is divided into +//! `--page-size` byte chunks, with a final short chunk retained when the file +//! size is not an exact multiple. The default page size is 400 KiB, matching a +//! standard terminal page in ReleaseFast builds on the current target. +//! +//! A raw dump of actual page backing memory is the most representative input: +//! it includes cells, rows, styles, graphemes, hyperlinks, allocator metadata, +//! and unused capacity exactly as the codec would see them. Keep such corpora +//! outside the repository and reuse the same file when comparing branches. +//! Arbitrary files are accepted too, but their compression ratios should not be +//! interpreted as terminal scrollback ratios. +//! +//! ## Modes +//! +//! * `noop` walks the input chunks without invoking the codec. This measures the +//! benchmark loop's minimum overhead. +//! * `compress` compresses every input chunk into a reusable output buffer. +//! * `store` additionally copies each useful result into an exact-sized +//! allocation. It retains the most recent `--retained-pages` blocks so that +//! allocation, eviction, and allocator reuse resemble bounded scrollback. +//! * `decompress` prepares compressed blocks during setup, then decompresses +//! every block into a reusable output buffer. +//! * `report` compresses each chunk once and prints raw and encoded sizes. It is +//! for inspecting ratios, not timing comparisons. +//! +//! Dataset loading, output allocation, and preparation of blocks for +//! decompression happen in `setup` and are outside `Benchmark`'s timed region. +//! The `compress` and `decompress` steps perform no allocation. `store` +//! deliberately performs encoded-block allocations and frees inside the timed +//! step; only its reusable workspace and ring metadata are prepared in setup. +//! `hyperfine` still measures full process lifetime, so use `--loops` to +//! amortize setup and teardown when comparing small corpora. +//! +//! ## Examples +//! +//! Build benchmarks in ReleaseFast mode: +//! +//! zig build -Demit-bench -Doptimize=ReleaseFast -Demit-macos-app=false +//! +//! Inspect the compression ratio of a page corpus: +//! +//! ghostty-bench +page-compression --mode=report --data=/tmp/pages.raw +//! +//! Measure the cost of exact encoded storage relative to the codec alone: +//! +//! hyperfine --warmup 3 \ +//! 'ghostty-bench +page-compression --mode=compress --loops=100 --data=/tmp/pages.raw' \ +//! 'ghostty-bench +page-compression --mode=store --loops=100 --data=/tmp/pages.raw' +const PageCompression = @This(); + +const std = @import("std"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; +const Benchmark = @import("Benchmark.zig"); +const options = @import("options.zig"); +const compress = @import("../terminal/compress.zig"); +const CompressedPage = compress.Page; +const lz4 = compress.lz4; + +const log = std.log.scoped(.@"page-compression-bench"); + +/// Prevent a malformed or accidentally enormous corpus from consuming +/// unbounded memory during benchmark setup. +const max_data_size = 64 * 1024 * 1024; + +alloc: Allocator, +opts: Options, + +/// Complete contents of the input corpus. Individual pages are slices into +/// this allocation, so it remains alive until teardown. +data: []u8 = &.{}, + +/// Compressed blocks prepared during setup for `decompress` mode. +encoded: std.ArrayList(Encoded) = .empty, + +/// Exact encoded allocations retained by `store` mode. Null entries have not +/// been reached yet; once full, `stored_next` identifies the oldest block. +stored: []?[]u8 = &.{}, +stored_next: usize = 0, + +/// Reused by compression and report modes. Its length is the compression +/// bound of the largest input chunk. +compression_output: []u8 = &.{}, + +/// Reused by decompression mode. Its length is at least one input chunk. +decompression_output: []u8 = &.{}, + +/// Fixed 16 KiB scratch table required by the compressor. +table: lz4.HashTable = undefined, + +pub const Options = struct { + /// Set by the shared CLI parser for string option ownership. + _arena: ?std.heap.ArenaAllocator = null, + + /// Select the operation performed inside the timed benchmark step. + mode: Mode = .compress, + + /// Repeat the complete corpus this many times per benchmark step. Increase + /// this when the corpus is too small for stable `hyperfine` measurements. + loops: u32 = 1, + + /// Number of bytes treated as one independent LZ4 block. Real page dumps + /// should use the exact backing-memory size of the pages being measured. + @"page-size": usize = 400 * 1024, + + /// Number of exact encoded allocations retained by `store` mode before + /// the oldest allocation is freed and replaced. The default approximates + /// a 10 MB scrollback composed of standard 400 KiB terminal pages. + @"retained-pages": usize = 25, + + /// Pre-generated input corpus. `-` reads stdin, although a regular file is + /// recommended so identical bytes can be reused across benchmark runs. + /// When unset, all modes are no-ops. + data: ?[]const u8 = null, + + pub fn deinit(self: *Options) void { + if (self._arena) |arena| arena.deinit(); + self.* = undefined; + } +}; + +pub const Mode = enum { + /// Walk page boundaries and establish the benchmark loop overhead. + noop, + + /// Compress each raw page into a reusable compression-bound buffer. + compress, + + /// Compress and retain exact-sized encoded blocks in a bounded ring. + store, + + /// Decompress blocks prepared before the timed region. + decompress, + + /// Print per-page and aggregate encoded sizes. Not a timing benchmark. + report, +}; + +const Encoded = struct { + /// Exact compressed block bytes. + bytes: []u8, + + /// Exact output length expected by the raw block decoder. + raw_len: usize, +}; + +/// Allocate benchmark state. Input data is intentionally loaded later by +/// `setup` so construction is cheap and follows the other benchmarks. +pub fn create( + alloc: Allocator, + opts: Options, +) !*PageCompression { + const ptr = try alloc.create(PageCompression); + ptr.* = .{ + .alloc = alloc, + .opts = opts, + }; + return ptr; +} + +/// Release allocations retained across benchmark steps. +pub fn destroy(self: *PageCompression, alloc: Allocator) void { + self.clearPreparedData(); + alloc.destroy(self); +} + +/// Select one operation for the benchmark harness to time. +pub fn benchmark(self: *PageCompression) Benchmark { + return .init(self, .{ + .stepFn = switch (self.opts.mode) { + .noop => stepNoop, + .compress => stepCompress, + .store => stepStore, + .decompress => stepDecompress, + .report => stepReport, + }, + .setupFn = setup, + .teardownFn = teardown, + }); +} + +/// Load and partition the input corpus. For decompression mode this also +/// creates the encoded blocks, keeping compression outside the timed region. +fn setup(ptr: *anyopaque) Benchmark.Error!void { + const self: *PageCompression = @ptrCast(@alignCast(ptr)); + assert(self.data.len == 0); + assert(self.encoded.items.len == 0); + + self.setupData() catch |err| { + log.warn("failed to prepare page compression benchmark err={}", .{err}); + return error.BenchmarkFailed; + }; +} + +fn setupData(self: *PageCompression) !void { + if (self.opts.loops == 0) return error.InvalidLoops; + if (self.opts.@"page-size" == 0) return error.InvalidPageSize; + if (self.opts.mode == .store and self.opts.@"retained-pages" == 0) + return error.InvalidRetainedPages; + + const data_file = try options.dataFile(self.opts.data) orelse return; + defer data_file.close(); + + self.data = try data_file.readToEndAlloc(self.alloc, max_data_size); + errdefer { + self.alloc.free(self.data); + self.data = &.{}; + } + if (self.data.len == 0) return; + if (self.opts.mode == .noop) return; + + const largest_page = @min(self.opts.@"page-size", self.data.len); + self.compression_output = try self.alloc.alloc( + u8, + try lz4.compressBound(largest_page), + ); + errdefer { + self.alloc.free(self.compression_output); + self.compression_output = &.{}; + } + + if (self.opts.mode == .store) try self.prepareStored(); + if (self.opts.mode == .decompress) try self.prepareEncoded(); +} + +/// Allocate only the ring metadata for `store` mode. The encoded blocks which +/// populate it are intentionally allocated by the timed benchmark step. +fn prepareStored(self: *PageCompression) !void { + self.stored = try self.alloc.alloc(?[]u8, self.opts.@"retained-pages"); + @memset(self.stored, null); + self.stored_next = 0; +} + +/// Precompress every input page and verify one decode before benchmarking. +/// This catches corpus or codec problems before the timer starts. +fn prepareEncoded(self: *PageCompression) !void { + self.decompression_output = try self.alloc.alloc( + u8, + @min(self.opts.@"page-size", self.data.len), + ); + errdefer { + self.alloc.free(self.decompression_output); + self.decompression_output = &.{}; + } + + var it = self.pages(); + while (it.next()) |page| { + const encoded_len = try lz4.compress( + page, + self.compression_output, + &self.table, + ); + const encoded = try self.alloc.dupe( + u8, + self.compression_output[0..encoded_len], + ); + self.encoded.append(self.alloc, .{ + .bytes = encoded, + .raw_len = page.len, + }) catch |err| { + self.alloc.free(encoded); + return err; + }; + } + + var page_it = self.pages(); + for (self.encoded.items) |block| { + const page = page_it.next().?; + const output = self.decompression_output[0..block.raw_len]; + _ = try lz4.decompress(block.bytes, output); + if (!std.mem.eql(u8, page, output)) return error.RoundTripMismatch; + } +} + +/// Release everything created by setup. This is shared by teardown and +/// destroy so errors and direct unit-test use remain leak-free. +fn clearPreparedData(self: *PageCompression) void { + for (self.encoded.items) |block| self.alloc.free(block.bytes); + self.encoded.deinit(self.alloc); + self.encoded = .empty; + + for (self.stored) |block| if (block) |bytes| self.alloc.free(bytes); + if (self.stored.len > 0) self.alloc.free(self.stored); + self.stored = &.{}; + self.stored_next = 0; + + if (self.compression_output.len > 0) + self.alloc.free(self.compression_output); + self.compression_output = &.{}; + + if (self.decompression_output.len > 0) + self.alloc.free(self.decompression_output); + self.decompression_output = &.{}; + + if (self.data.len > 0) self.alloc.free(self.data); + self.data = &.{}; +} + +fn teardown(ptr: *anyopaque) void { + const self: *PageCompression = @ptrCast(@alignCast(ptr)); + self.clearPreparedData(); +} + +/// Baseline mode: traverse exactly the same page boundaries as compression +/// without invoking the codec. +fn stepNoop(ptr: *anyopaque) Benchmark.Error!void { + const self: *PageCompression = @ptrCast(@alignCast(ptr)); + for (0..self.opts.loops) |_| { + var it = self.pages(); + while (it.next()) |page| std.mem.doNotOptimizeAway(page); + } +} + +/// Compress all pages into one reusable output buffer. Only the returned +/// encoded length is consumed because retaining output pages would measure +/// allocation and ownership rather than codec throughput. +fn stepCompress(ptr: *anyopaque) Benchmark.Error!void { + const self: *PageCompression = @ptrCast(@alignCast(ptr)); + for (0..self.opts.loops) |_| { + var it = self.pages(); + while (it.next()) |page| { + const encoded_len = lz4.compress( + page, + self.compression_output, + &self.table, + ) catch |err| { + log.warn("page compression failed err={}", .{err}); + return error.BenchmarkFailed; + }; + std.mem.doNotOptimizeAway(encoded_len); + } + } +} + +/// Compress pages and retain their exact encoded allocations in a bounded +/// ring. This models the part of `compress.Page.init` which differs from the +/// allocation-free codec benchmark: copying the result, allocating its +/// persistent storage, and freeing an old block when scrollback is full. +fn stepStore(ptr: *anyopaque) Benchmark.Error!void { + const self: *PageCompression = @ptrCast(@alignCast(ptr)); + if (self.data.len == 0) return; + assert(self.stored.len > 0); + + for (0..self.opts.loops) |_| { + var it = self.pages(); + while (it.next()) |page| { + const output_len = CompressedPage.requiredScratch(page.len) catch |err| { + log.warn("failed to size stored page output err={}", .{err}); + return error.BenchmarkFailed; + }; + if (output_len == 0) continue; + + const encoded_len = lz4.compress( + page, + self.compression_output[0..output_len], + &self.table, + ) catch |err| switch (err) { + // This is the same profitability outcome as + // compress.Page.init: a block which exceeds the useful output + // limit remains resident and consumes no encoded allocation. + error.OutputTooSmall => continue, + error.InputTooLarge => { + log.warn("stored page compression failed err={}", .{err}); + return error.BenchmarkFailed; + }, + }; + + const encoded = self.alloc.dupe( + u8, + self.compression_output[0..encoded_len], + ) catch |err| { + log.warn("stored page allocation failed err={}", .{err}); + return error.BenchmarkFailed; + }; + + const slot = &self.stored[self.stored_next]; + if (slot.*) |previous| self.alloc.free(previous); + slot.* = encoded; + self.stored_next = (self.stored_next + 1) % self.stored.len; + std.mem.doNotOptimizeAway(encoded); + } + } +} + +/// Decompress blocks prepared by setup. The output allocation is reused so +/// this measures only decoding and the required memory writes. +fn stepDecompress(ptr: *anyopaque) Benchmark.Error!void { + const self: *PageCompression = @ptrCast(@alignCast(ptr)); + for (0..self.opts.loops) |_| { + for (self.encoded.items) |block| { + const output = self.decompression_output[0..block.raw_len]; + _ = lz4.decompress(block.bytes, output) catch |err| { + log.warn("page decompression failed err={}", .{err}); + return error.BenchmarkFailed; + }; + std.mem.doNotOptimizeAway(output); + } + } +} + +/// Print size information for evaluating compression ratio. This shares the +/// input and codec paths with compression mode but deliberately makes no +/// timing claims. +fn stepReport(ptr: *anyopaque) Benchmark.Error!void { + const self: *PageCompression = @ptrCast(@alignCast(ptr)); + if (self.data.len == 0) return; + + var page_index: usize = 0; + var raw_total: usize = 0; + var encoded_total: usize = 0; + var it = self.pages(); + while (it.next()) |page| : (page_index += 1) { + const encoded_len = lz4.compress( + page, + self.compression_output, + &self.table, + ) catch |err| { + log.warn("page compression report failed err={}", .{err}); + return error.BenchmarkFailed; + }; + raw_total += page.len; + encoded_total += encoded_len; + std.debug.print( + "page-compression page={d} raw={d} encoded={d} ratio={d:.2}%\n", + .{ page_index, page.len, encoded_len, percentage(encoded_len, page.len) }, + ); + } + + std.debug.print( + "page-compression total pages={d} raw={d} encoded={d} ratio={d:.2}% " ++ + "workspace={d} output_bound={d}\n", + .{ + page_index, + raw_total, + encoded_total, + percentage(encoded_total, raw_total), + @sizeOf(lz4.HashTable), + self.compression_output.len, + }, + ); +} + +/// Iterate fixed-size page chunks without allocating an index table. +fn pages(self: *const PageCompression) PageIterator { + return .{ + .data = self.data, + .page_size = self.opts.@"page-size", + }; +} + +const PageIterator = struct { + data: []const u8, + page_size: usize, + offset: usize = 0, + + fn next(self: *PageIterator) ?[]const u8 { + if (self.offset >= self.data.len) return null; + const len = @min(self.page_size, self.data.len - self.offset); + const end = self.offset + len; + defer self.offset = end; + return self.data[self.offset..end]; + } +}; + +fn percentage(part: usize, whole: usize) f64 { + if (whole == 0) return 0; + return @as(f64, @floatFromInt(part)) * 100 / + @as(f64, @floatFromInt(whole)); +} + +test PageCompression { + const testing = std.testing; + const impl: *PageCompression = try .create(testing.allocator, .{}); + defer impl.destroy(testing.allocator); + + const bench = impl.benchmark(); + _ = try bench.run(.once); +} + +test "PageCompression store retains exact encoded allocations" { + const testing = std.testing; + const page_size = 1024; + const impl: *PageCompression = try .create(testing.allocator, .{ + .mode = .store, + .@"page-size" = page_size, + .@"retained-pages" = 2, + }); + defer impl.destroy(testing.allocator); + + impl.data = try testing.allocator.alloc(u8, 3 * page_size); + @memset(impl.data, 0); + impl.compression_output = try testing.allocator.alloc( + u8, + try lz4.compressBound(page_size), + ); + try impl.prepareStored(); + + try stepStore(impl); + try testing.expectEqual(@as(usize, 1), impl.stored_next); + for (impl.stored) |block| { + const encoded = block.?; + try testing.expect(encoded.len < page_size); + + var decoded: [page_size]u8 = undefined; + _ = try lz4.decompress(encoded, &decoded); + try testing.expect(std.mem.allEqual(u8, &decoded, 0)); + } +} diff --git a/vendor/libghostty-vt/src/benchmark/ScreenClone.zig b/vendor/libghostty-vt/src/benchmark/ScreenClone.zig index 108eaa0c..5176ce26 100644 --- a/vendor/libghostty-vt/src/benchmark/ScreenClone.zig +++ b/vendor/libghostty-vt/src/benchmark/ScreenClone.zig @@ -21,6 +21,10 @@ pub const Options = struct { /// The type of codepoint width calculation to use. mode: Mode = .clone, + /// Multiplier on the number of iterations each step runs. This is + /// useful to make a benchmark run long enough for profiling. + loops: u32 = 1, + /// The size of the terminal. This affects benchmarking when /// dealing with soft line wrapping and the memory impact /// of page sizes. @@ -48,6 +52,22 @@ pub const Mode = enum { /// RenderState rather than a screen clone. render, + + /// Like render, but only the portion of the render state update + /// that requires holding a terminal lock (beginUpdate). The + /// deferred work (endUpdate) is excluded since it happens outside + /// of any locks. + @"render-locked", + + /// RenderState update with no changes to the terminal. This is + /// the common case for a renderer that is redrawing frames (e.g. + /// cursor blink, mouse movement) without terminal changes. + @"render-clean", + + /// RenderState update where a single row is dirty. This models the + /// common case of a shell prompt or TUI updating a small portion + /// of the screen between frames. + @"render-partial", }; pub fn create( @@ -79,6 +99,9 @@ pub fn benchmark(self: *ScreenClone) Benchmark { .noop => stepNoop, .clone => stepClone, .render => stepRender, + .@"render-locked" => stepRenderLocked, + .@"render-clean" => stepRenderClean, + .@"render-partial" => stepRenderPartial, }, .setupFn = setup, .teardownFn = teardown, @@ -178,16 +201,102 @@ fn stepRender(ptr: *anyopaque) Benchmark.Error!void { // We loop because its so fast that a single benchmark run doesn't // properly capture our speeds. - for (0..1000) |_| { + for (0..50_000 * @as(u64, self.opts.loops)) |_| { // Forces a full rebuild because it thinks our screen changed state.screen = .alternate; state.update(alloc, &self.terminal) catch |err| { log.warn("error cloning screen err={}", .{err}); return error.BenchmarkFailed; }; - std.mem.doNotOptimizeAway(state); + std.mem.doNotOptimizeAway(&state); + + // Note: we purposely do not free memory because we don't want + // to benchmark that. We'll free when the benchmark exits. + } +} + +fn stepRenderLocked(ptr: *anyopaque) Benchmark.Error!void { + const self: *ScreenClone = @ptrCast(@alignCast(ptr)); + + // We do this once out of the loop because a significant slowdown + // on the first run is allocation. After that first run, even with + // a full rebuild, it is much faster. Let's ignore that first run + // slowdown. + const alloc = self.terminal.screens.active.alloc; + var state: terminalpkg.RenderState = .empty; + state.update(alloc, &self.terminal) catch |err| { + log.warn("error cloning screen err={}", .{err}); + return error.BenchmarkFailed; + }; + + // We loop because its so fast that a single benchmark run doesn't + // properly capture our speeds. + for (0..50_000 * @as(u64, self.opts.loops)) |_| { + // Forces a full rebuild because it thinks our screen changed + state.screen = .alternate; + state.beginUpdate(alloc, &self.terminal) catch |err| { + log.warn("error cloning screen err={}", .{err}); + return error.BenchmarkFailed; + }; + std.mem.doNotOptimizeAway(&state); // Note: we purposely do not free memory because we don't want // to benchmark that. We'll free when the benchmark exits. } } + +fn stepRenderClean(ptr: *anyopaque) Benchmark.Error!void { + const self: *ScreenClone = @ptrCast(@alignCast(ptr)); + + // Initial update so that subsequent updates are clean (nothing + // dirty, no rebuilds). + const alloc = self.terminal.screens.active.alloc; + var state: terminalpkg.RenderState = .empty; + state.update(alloc, &self.terminal) catch |err| { + log.warn("error cloning screen err={}", .{err}); + return error.BenchmarkFailed; + }; + + // We loop because its so fast that a single benchmark run doesn't + // properly capture our speeds. + for (0..3_000_000 * @as(u64, self.opts.loops)) |_| { + state.update(alloc, &self.terminal) catch |err| { + log.warn("error cloning screen err={}", .{err}); + return error.BenchmarkFailed; + }; + std.mem.doNotOptimizeAway(&state); + } +} + +fn stepRenderPartial(ptr: *anyopaque) Benchmark.Error!void { + const self: *ScreenClone = @ptrCast(@alignCast(ptr)); + + // Initial update so that subsequent updates are incremental. + const alloc = self.terminal.screens.active.alloc; + var state: terminalpkg.RenderState = .empty; + state.update(alloc, &self.terminal) catch |err| { + log.warn("error cloning screen err={}", .{err}); + return error.BenchmarkFailed; + }; + + // Grab a pin roughly in the middle of the active area that we + // dirty on every iteration to simulate a small screen update. + const pages = &self.terminal.screens.active.pages; + const pin = pages.pin(.{ .active = .{ + .x = 0, + .y = self.terminal.rows / 2, + } }).?; + + // We loop because its so fast that a single benchmark run doesn't + // properly capture our speeds. + for (0..2_000_000 * @as(u64, self.opts.loops)) |_| { + // Mark a single row dirty. `update` clears this so each + // iteration rebuilds exactly one row. + pin.markDirty(); + state.update(alloc, &self.terminal) catch |err| { + log.warn("error cloning screen err={}", .{err}); + return error.BenchmarkFailed; + }; + std.mem.doNotOptimizeAway(&state); + } +} diff --git a/vendor/libghostty-vt/src/benchmark/ScrollbackCompression.zig b/vendor/libghostty-vt/src/benchmark/ScrollbackCompression.zig new file mode 100644 index 00000000..40ec20f4 --- /dev/null +++ b/vendor/libghostty-vt/src/benchmark/ScrollbackCompression.zig @@ -0,0 +1,343 @@ +//! Benchmarks cold-history compression and restoration on pages owned by a +//! live terminal. +//! +//! Unlike `page-compression`, which measures the standalone codec against raw +//! byte chunks, this benchmark first parses a VT corpus into a real `Terminal`. +//! Its timed operations therefore include the PageList state transition, +//! exact-sized encoded allocation, retained-mapping reclamation, and transparent +//! restoration through `PageList.Node.page`. +//! +//! ## Input +//! +//! `--data` names a pre-generated VT byte stream. Parsing happens during setup +//! and is not part of the benchmark's timed region. Use the same saved corpus, +//! terminal dimensions, and scrollback limit when comparing revisions. In +//! particular, do not pipe a generator into this benchmark: generation and +//! pipe scheduling add noise which can overwhelm the operation being measured. +//! +//! The benchmark always operates on the primary screen's PageList. This keeps +//! the result focused on scrollback even if malformed or incomplete input +//! leaves the terminal in its alternate screen at end of file. `--max-scrollback` +//! is expressed in bytes and defaults to 10 MB. +//! +//! ## Modes +//! +//! * `noop` parses the corpus but performs no timed PageList operation. This is +//! the common process and setup baseline for the other modes. +//! * `compress` times one complete `compress` invocation. +//! * `incremental` reaches the same final representation through +//! `compress(.drain)`. It drains the candidate-bounded steps and final +//! no-work verification pass in the timed region, making cursor and repeated +//! traversal overhead directly comparable with `compress`. +//! * `restore` compresses cold history during setup, outside the timed region, +//! then visits every fully historical node through `Node.page`. That public +//! content-access boundary transparently restores compressed nodes. +//! * `report` performs one compression pass and prints compressed page count, +//! encoded ratio, and estimated resident-byte savings. It is intended for +//! inspecting a corpus rather than timing comparisons. +//! +//! A fully historical page is a node strictly before the node containing the +//! top of the active area. The boundary node is deliberately excluded because +//! it can contain both history and active rows. Pages intersecting the current +//! viewport are also excluded so visible contents remain resident. Normal +//! terminal execution compresses eligible pages incrementally after activity +//! becomes idle. The benchmark invokes PageList operations directly so its +//! timed regions exclude the production scheduler's idle delay and +//! renderer-thread coordination. +//! +//! ## Examples +//! +//! Build the benchmark in ReleaseFast mode: +//! +//! zig build -Demit-bench -Doptimize=ReleaseFast -Demit-macos-app=false +//! +//! Inspect the memory reduction for a saved VT corpus: +//! +//! ghostty-bench +scrollback-compression --mode=report \ +//! --data=/tmp/scrollback.vt --terminal-cols=120 --terminal-rows=80 +//! +//! Compare PageList compression and restoration cost. Setup still contributes +//! to full process time, so use a sufficiently large corpus and compare against +//! `noop` with identical arguments: +//! +//! hyperfine --warmup 3 \ +//! 'ghostty-bench +scrollback-compression --mode=noop --data=/tmp/scrollback.vt' \ +//! 'ghostty-bench +scrollback-compression --mode=compress --data=/tmp/scrollback.vt' \ +//! 'ghostty-bench +scrollback-compression --mode=incremental --data=/tmp/scrollback.vt' \ +//! 'ghostty-bench +scrollback-compression --mode=restore --data=/tmp/scrollback.vt' +const ScrollbackCompression = @This(); + +const std = @import("std"); +const Allocator = std.mem.Allocator; +const terminalpkg = @import("../terminal/main.zig"); +const Benchmark = @import("Benchmark.zig"); +const options = @import("options.zig"); +const PageList = terminalpkg.PageList; +const Terminal = terminalpkg.Terminal; + +const log = std.log.scoped(.@"scrollback-compression-bench"); + +opts: Options, +terminal: Terminal, + +pub const Options = struct { + /// Set by the shared CLI parser so the `data` string remains valid for the + /// lifetime of the benchmark implementation. + _arena: ?std.heap.ArenaAllocator = null, + + /// Select the PageList operation performed inside the timed benchmark. + mode: Mode = .compress, + + /// Dimensions used to construct the terminal which consumes the corpus. + /// These affect wrapping and therefore the number and contents of pages. + @"terminal-rows": u16 = 80, + @"terminal-cols": u16 = 120, + + /// Maximum primary-screen scrollback allocation in bytes. PageList rounds + /// this as required by its page allocation policy. + @"max-scrollback": usize = 10_000_000, + + /// Pre-generated VT corpus. `-` reads stdin, although a regular file is + /// strongly recommended so comparisons can reuse identical input bytes. + /// When unset, every mode operates on the initial empty terminal. + data: ?[]const u8 = null, + + pub fn deinit(self: *Options) void { + if (self._arena) |arena| arena.deinit(); + self.* = undefined; + } +}; + +pub const Mode = enum { + /// Establish process and setup overhead without a PageList operation. + noop, + + /// Compress every eligible fully historical page once. + compress, + + /// Compress every eligible page through bounded resumable steps. + incremental, + + /// Restore pages compressed outside the timed region. + restore, + + /// Compress once and print aggregate memory statistics. + report, +}; + +pub fn create( + alloc: Allocator, + opts: Options, +) !*ScrollbackCompression { + const ptr = try alloc.create(ScrollbackCompression); + errdefer alloc.destroy(ptr); + + ptr.* = .{ + .opts = opts, + .terminal = try .init(alloc, .{ + .rows = opts.@"terminal-rows", + .cols = opts.@"terminal-cols", + .max_scrollback = opts.@"max-scrollback", + }), + }; + return ptr; +} + +pub fn destroy(self: *ScrollbackCompression, alloc: Allocator) void { + self.terminal.deinit(alloc); + alloc.destroy(self); +} + +pub fn benchmark(self: *ScrollbackCompression) Benchmark { + return .init(self, .{ + .stepFn = switch (self.opts.mode) { + .noop => stepNoop, + .compress => stepCompress, + .incremental => stepIncremental, + .restore => stepRestore, + .report => stepReport, + }, + .setupFn = setup, + }); +} + +/// Reset the terminal and consume the complete VT corpus before timing starts. +/// Restore mode also prepares its compressed representation here so its timed +/// step contains decoding and mapping writes, but not encoding. +fn setup(ptr: *anyopaque) Benchmark.Error!void { + const self: *ScrollbackCompression = @ptrCast(@alignCast(ptr)); + self.terminal.fullReset(); + + self.loadCorpus() catch |err| { + log.warn("failed to prepare scrollback compression benchmark err={}", .{err}); + return error.BenchmarkFailed; + }; + + if (self.opts.mode == .restore) { + _ = self.pages().compress(.full); + } +} + +/// Feed the corpus in the same 64 KiB chunks used by the real IO thread and +/// terminal-stream benchmark. Parser and file IO costs remain in setup. +fn loadCorpus(self: *ScrollbackCompression) !void { + const data_file = try options.dataFile(self.opts.data) orelse return; + defer data_file.close(); + + var stream = self.terminal.vtStream(); + defer stream.deinit(); + + var read_buf: [64 * 1024]u8 align(std.atomic.cache_line) = undefined; + var file_reader = data_file.reader(&read_buf); + const reader = &file_reader.interface; + + var buf: [64 * 1024]u8 = undefined; + while (true) { + const n = reader.readSliceShort(&buf) catch + return file_reader.err orelse error.ReadFailed; + if (n == 0) return; + stream.nextSlice(buf[0..n]); + } +} + +/// Return the primary screen because it is the terminal screen which owns +/// scrollback. A corpus ending in the alternate screen must not turn this into +/// an alternate-screen allocation benchmark. +fn pages(self: *ScrollbackCompression) *PageList { + return &self.terminal.screens.get(.primary).?.pages; +} + +fn stepNoop(ptr: *anyopaque) Benchmark.Error!void { + const self: *ScrollbackCompression = @ptrCast(@alignCast(ptr)); + std.mem.doNotOptimizeAway(&self.terminal); +} + +fn stepCompress(ptr: *anyopaque) Benchmark.Error!void { + const self: *ScrollbackCompression = @ptrCast(@alignCast(ptr)); + _ = self.pages().compress(.full); + std.mem.doNotOptimizeAway(&self.terminal); +} + +fn stepIncremental(ptr: *anyopaque) Benchmark.Error!void { + const self: *ScrollbackCompression = @ptrCast(@alignCast(ptr)); + _ = self.pages().compress(.drain); + std.mem.doNotOptimizeAway(&self.terminal); +} + +fn stepRestore(ptr: *anyopaque) Benchmark.Error!void { + const self: *ScrollbackCompression = @ptrCast(@alignCast(ptr)); + std.mem.doNotOptimizeAway(self.visitColdPages()); +} + +/// Visit all nodes which were eligible for the setup compression pass. +/// +/// `page` is intentionally used instead of inspecting the union payload so the +/// benchmark follows the same transparent restoration boundary as consumers. +/// Compressible nodes restore here; resident candidates which failed the +/// opportunistic pass are harmlessly visited through the same boundary. +fn visitColdPages(self: *ScrollbackCompression) usize { + const page_list = self.pages(); + const active_node = page_list.getTopLeft(.active).node; + var visited: usize = 0; + var node_ = page_list.pages.first; + while (node_) |node| : (node_ = node.next) { + if (node == active_node) break; + std.mem.doNotOptimizeAway(node.page()); + visited += 1; + } + return visited; +} + +fn stepReport(ptr: *anyopaque) Benchmark.Error!void { + const self: *ScrollbackCompression = @ptrCast(@alignCast(ptr)); + _ = self.pages().compress(.full); + const memory = self.pages().memoryStats(); + + std.debug.print( + "scrollback-compression compressed={d} raw={d} " ++ + "encoded={d} ratio={d:.2}% savings={d}\n", + .{ + memory.compressed_pages, + memory.decommitted_raw_bytes, + memory.encoded_bytes, + percentage( + memory.encoded_bytes, + memory.decommitted_raw_bytes, + ), + memory.estimatedSavings(), + }, + ); +} + +fn percentage(part: usize, whole: usize) f64 { + if (whole == 0) return 0; + return @as(f64, @floatFromInt(part)) * 100 / + @as(f64, @floatFromInt(whole)); +} + +test ScrollbackCompression { + const testing = std.testing; + const impl: *ScrollbackCompression = try .create(testing.allocator, .{}); + defer impl.destroy(testing.allocator); + + const bench = impl.benchmark(); + _ = try bench.run(.once); +} + +test "ScrollbackCompression restores cold terminal pages" { + const testing = std.testing; + const impl: *ScrollbackCompression = try .create(testing.allocator, .{ + .mode = .restore, + .@"terminal-rows" = 4, + // Standard-width pages hold 215 rows. Keeping that row capacity here + // makes this test corpus small while still producing cold history. + .@"terminal-cols" = 215, + .@"max-scrollback" = 1_000_000, + }); + defer impl.destroy(testing.allocator); + + var stream = impl.terminal.vtStream(); + defer stream.deinit(); + for (0..256) |_| stream.nextSlice("aaaa\r\n"); + + _ = impl.pages().compress(.full); + const compressed = impl.pages().memoryStats(); + try testing.expect(compressed.compressed_pages > 0); + try testing.expect(impl.visitColdPages() >= compressed.compressed_pages); + + // Restored historical pages are resident and therefore eligible for a + // later explicit pass. This also verifies that the benchmark traversal + // went through Node.page rather than merely inspecting page metadata. + _ = impl.pages().compress(.full); + const recompressed = impl.pages().memoryStats(); + try testing.expectEqual( + compressed.compressed_pages, + recompressed.compressed_pages, + ); +} + +test "ScrollbackCompression drains incremental compression steps" { + const testing = std.testing; + const impl: *ScrollbackCompression = try .create(testing.allocator, .{ + .mode = .incremental, + .@"terminal-rows" = 4, + .@"terminal-cols" = 215, + .@"max-scrollback" = 1_000_000, + }); + defer impl.destroy(testing.allocator); + + var stream = impl.terminal.vtStream(); + defer stream.deinit(); + for (0..256) |_| stream.nextSlice("aaaa\r\n"); + + _ = impl.pages().compress(.drain); + const incremental = impl.pages().memoryStats(); + try testing.expect(incremental.compressed_pages > 0); + + // Restore the same pages and compare against the monolithic operation. + // Both paths should produce the same final storage representation. + _ = impl.visitColdPages(); + _ = impl.pages().compress(.full); + const monolithic = impl.pages().memoryStats(); + try testing.expectEqual(monolithic, incremental); +} diff --git a/vendor/libghostty-vt/src/benchmark/TerminalStream.zig b/vendor/libghostty-vt/src/benchmark/TerminalStream.zig index 1cac656e..a5302005 100644 --- a/vendor/libghostty-vt/src/benchmark/TerminalStream.zig +++ b/vendor/libghostty-vt/src/benchmark/TerminalStream.zig @@ -2,15 +2,13 @@ //! handler from input to terminal state update. This is useful to //! test general throughput of VT parsing and handling. //! -//! Note that the handler used for this benchmark isn't the full -//! terminal handler, since that requires a significant amount of -//! state. This is a simplified version that only handles specific -//! terminal operations like printing characters. We should expand -//! this to include more operations to improve the accuracy of the -//! benchmark. +//! This uses the full readonly terminal stream handler +//! (terminal.TerminalStream) so every escape sequence updates real +//! terminal state (styles, cursor movement, erases, modes, etc.). +//! This closely mirrors the work done by the real IO thread. //! -//! It is a fairly broad benchmark that can be used to determine -//! if we need to optimize something more specific (e.g. the parser). +//! For more isolated measurements see the terminal-parser and +//! osc-parser benchmarks. const TerminalStream = @This(); const std = @import("std"); @@ -20,13 +18,12 @@ const terminalpkg = @import("../terminal/main.zig"); const Benchmark = @import("Benchmark.zig"); const options = @import("options.zig"); const Terminal = terminalpkg.Terminal; -const Stream = terminalpkg.Stream(*Handler); +const Stream = terminalpkg.TerminalStream; const log = std.log.scoped(.@"terminal-stream-bench"); opts: Options, terminal: Terminal, -handler: Handler, stream: Stream, /// The file, opened in the setup function. @@ -61,14 +58,15 @@ pub fn create( .rows = opts.@"terminal-rows", .cols = opts.@"terminal-cols", }), - .handler = .{ .t = &ptr.terminal }, - .stream = .init(&ptr.handler), + .stream = undefined, }; + ptr.stream = .initAlloc(alloc, .init(&ptr.terminal)); return ptr; } pub fn destroy(self: *TerminalStream, alloc: Allocator) void { + self.stream.deinit(); self.terminal.deinit(alloc); alloc.destroy(self); } @@ -114,11 +112,14 @@ fn step(ptr: *anyopaque) Benchmark.Error!void { // aren't currently IO bound. const f = self.data_f orelse return; - var read_buf: [4096]u8 align(std.atomic.cache_line) = undefined; + var read_buf: [64 * 1024]u8 align(std.atomic.cache_line) = undefined; var f_reader = f.reader(&read_buf); const r = &f_reader.interface; - var buf: [4096]u8 = undefined; + // This buffer size matches the read buffer size used by the + // real IO thread (see termio Exec.zig buffer_capacity) so that + // the benchmark exercises the stream with realistic chunk sizes. + var buf: [64 * 1024]u8 = undefined; while (true) { const n = r.readSliceShort(&buf) catch { log.warn("error reading data file err={?}", .{f_reader.err}); @@ -129,26 +130,6 @@ fn step(ptr: *anyopaque) Benchmark.Error!void { } } -/// Implements the handler interface for the terminal.Stream. -/// We should expand this to include more operations to make -/// our benchmark more realistic. -const Handler = struct { - t: *Terminal, - - pub fn vt( - self: *Handler, - comptime action: Stream.Action.Tag, - value: Stream.Action.Value(action), - ) void { - switch (action) { - .print => self.t.print(value.cp) catch |err| { - log.warn("error processing benchmark print err={}", .{err}); - }, - else => {}, - } - } -}; - test TerminalStream { const testing = std.testing; const alloc = testing.allocator; diff --git a/vendor/libghostty-vt/src/benchmark/cli.zig b/vendor/libghostty-vt/src/benchmark/cli.zig index 13f07077..dc4d7b8f 100644 --- a/vendor/libghostty-vt/src/benchmark/cli.zig +++ b/vendor/libghostty-vt/src/benchmark/cli.zig @@ -6,8 +6,12 @@ const cli = @import("../cli.zig"); /// benchmarks. View docs for each individual one in the predictably /// named files. pub const Action = enum { + @"apc-parser", @"codepoint-width", @"grapheme-break", + @"hyperlink-map", + @"page-compression", + @"scrollback-compression", @"screen-clone", @"terminal-parser", @"terminal-stream", @@ -24,7 +28,11 @@ pub const Action = enum { /// See TerminalStream for an example. pub fn Struct(comptime action: Action) type { return switch (action) { + .@"apc-parser" => @import("ApcParser.zig"), + .@"hyperlink-map" => @import("HyperlinkMap.zig"), .@"screen-clone" => @import("ScreenClone.zig"), + .@"page-compression" => @import("PageCompression.zig"), + .@"scrollback-compression" => @import("ScrollbackCompression.zig"), .@"terminal-stream" => @import("TerminalStream.zig"), .@"codepoint-width" => @import("CodepointWidth.zig"), .@"grapheme-break" => @import("GraphemeBreak.zig"), diff --git a/vendor/libghostty-vt/src/benchmark/main.zig b/vendor/libghostty-vt/src/benchmark/main.zig index 5673044f..f22891c7 100644 --- a/vendor/libghostty-vt/src/benchmark/main.zig +++ b/vendor/libghostty-vt/src/benchmark/main.zig @@ -4,9 +4,12 @@ pub const CApi = @import("CApi.zig"); pub const TerminalStream = @import("TerminalStream.zig"); pub const CodepointWidth = @import("CodepointWidth.zig"); pub const GraphemeBreak = @import("GraphemeBreak.zig"); +pub const HyperlinkMap = @import("HyperlinkMap.zig"); pub const ScreenClone = @import("ScreenClone.zig"); pub const TerminalParser = @import("TerminalParser.zig"); pub const IsSymbol = @import("IsSymbol.zig"); +pub const PageCompression = @import("PageCompression.zig"); +pub const ScrollbackCompression = @import("ScrollbackCompression.zig"); test { @import("std").testing.refAllDecls(@This()); |
