terminal/snapshot: borrow fully buffered record payloads

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 |
This commit is contained in:
Mitchell Hashimoto
2026-08-15 08:54:25 -07:00
parent 36d8e3f777
commit c1a61fddda
2 changed files with 61 additions and 1 deletions

View File

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

View File

@@ -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)
{