From c1a61fddda00e907c7e66bd3609d6c558cccd26d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 15 Aug 2026 08:54:25 -0700 Subject: [PATCH] terminal/snapshot: borrow fully buffered record payloads MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the record source already has the complete payload buffered — always the case for in-memory snapshots such as ghostty_snapshot_decoder_new_buf — the record reader now borrows the payload straight out of the source buffer instead of streaming it through the limited and hashing reader adapters. Payload decoders parse a fixed reader over the borrowed bytes, `finish` validates the CRC with a single bulk update, and the source advances only after validation. The page decoder takes a matching fast path: a fully buffered payload is parsed in place, skipping the staging allocation and copy it previously made per PAGE record. Streaming sources are unchanged. This is a modest win on its own; it is also the foundation for later commits whose buffered fast paths rely on the payload being contiguous. Benchmarks (see the first commit in this series for methodology; "prev" is the parent commit): | wasm | encode prev | encode | decode prev | decode | |-----------|------------:|---------:|------------:|---------:| | ascii | 3.43 ms | 3.50 ms | 2.81 ms | 2.68 ms | | styled | 3.14 ms | 3.15 ms | 7.12 ms | 7.04 ms | | truecolor | 5.33 ms | 5.27 ms | 12.11 ms | 12.04 ms | | cjk | 5.95 ms | 5.92 ms | 12.04 ms | 11.89 ms | | grapheme | 14.18 ms | 14.33 ms | 13.16 ms | 13.14 ms | | native | mode | prev | this | |--------|--------|--------:|--------:| | ascii | encode | 41.7 ms | 41.8 ms | | ascii | decode | 53.2 ms | 52.4 ms | | utf8 | encode | 47.2 ms | 47.4 ms | | utf8 | decode | 61.6 ms | 61.2 ms | --- src/terminal/snapshot/page.zig | 12 +++++++- src/terminal/snapshot/record.zig | 50 ++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/src/terminal/snapshot/page.zig b/src/terminal/snapshot/page.zig index 0d166e389..bd64e483e 100644 --- a/src/terminal/snapshot/page.zig +++ b/src/terminal/snapshot/page.zig @@ -233,7 +233,17 @@ pub const Decoder = struct { // payload, so this is the byte count of the tables and grid. const remaining = self.record_reader.header.payload_len - Header.len; - if (remaining <= max_staged_payload) { + if (self.record_reader.payloadReader().bufferedLen() >= remaining) { + // The complete payload is already buffered, e.g. borrowed from + // an in-memory snapshot. Parse it in place with no staging + // copy; `finish` still enforces the CRC and exact exhaustion. + try decodePayloadBody( + self.record_reader.payloadReader(), + alloc, + destination, + self.header, + ); + } else if (remaining <= max_staged_payload) { // Stage the payload with one bulk read. The whole payload passes // through the checksum hasher as one update and the payload // decoders then parse a flat buffer, which keeps per-row work free diff --git a/src/terminal/snapshot/record.zig b/src/terminal/snapshot/record.zig index e8dada911..397d00cbe 100644 --- a/src/terminal/snapshot/record.zig +++ b/src/terminal/snapshot/record.zig @@ -257,6 +257,18 @@ pub const Reader = struct { limited: std.Io.Reader.Limited, hashing: std.Io.Reader.Hashed(Crc32c), + /// When the source already has the complete payload buffered, for + /// example an in-memory snapshot, the payload is borrowed straight + /// from the source buffer instead of streaming through the limited + /// and hashing adapters. The checksum is then verified with one bulk + /// update in `finish`, and the source is not advanced until `finish`. + borrowed: ?Borrowed, + + const Borrowed = struct { + source: *std.Io.Reader, + payload: std.Io.Reader, + }; + pub const InitError = Header.DecodeError; /// Errors detected after a payload decoder returns. @@ -278,6 +290,21 @@ pub const Reader = struct { ) InitError!void { self.* = undefined; self.header = try Header.decode(source); + + // The complete payload is already sitting in the source buffer: + // borrow it in place. Payload decoders read from a fixed reader + // over the borrowed bytes and `finish` checksums them in one pass. + if (source.bufferedLen() >= self.header.payload_len) { + self.borrowed = .{ + .source = source, + .payload = .fixed( + source.buffered()[0..self.header.payload_len], + ), + }; + return; + } + + self.borrowed = null; self.limited = .init( source, .limited(self.header.payload_len), @@ -297,11 +324,34 @@ pub const Reader = struct { /// Return the length-limited, checksum-updating payload reader. pub fn payloadReader(self: *Reader) *std.Io.Reader { + if (self.borrowed) |*borrowed| return &borrowed.payload; return &self.hashing.reader; } /// Require exact payload exhaustion and validate its CRC32C. pub fn finish(self: *Reader) FinishError!void { + if (self.borrowed) |*borrowed| { + if (borrowed.payload.bufferedLen() != 0) { + return error.PayloadNotExhausted; + } + + var checksum: Checksum = .init( + self.header.tag, + self.header.payload_len, + ); + checksum.writer().writeAll( + borrowed.payload.buffer[0..borrowed.payload.end], + ) catch unreachable; + if (checksum.final() != self.header.crc32c) { + return error.InvalidChecksum; + } + + // The borrowed bytes validated, so consume them from the + // source only now, leaving it positioned at the next record. + borrowed.source.toss(self.header.payload_len); + return; + } + if (self.hashing.reader.bufferedLen() != 0 or self.limited.remaining != .nothing) {