From 47a5182621e324f3351683ca2cc422562b3ff792 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 15 Aug 2026 09:14:31 -0700 Subject: [PATCH] terminal/snapshot: skip remap tables for pages without styles Decoding a page allocated and zeroed two full remap tables (a 128 KB entries array plus an 8 KB seen bitmap each for styles and hyperlinks) even when the page declared no table entries at all, which is every page of plain scrollback. Empty tables now use a shared `.empty` remap that allocates nothing; `get` reads it as all-unmapped through a length check. Pages that do declare entries are unchanged. (Leaving the entries array unzeroed behind a seen-bitmap-gated `get` was also tried and measured no better than the plain memset, so the table keeps its simple zero-means-unmapped representation.) Benchmarks ("prev" is the parent commit): | wasm | encode prev | encode | decode prev | decode | |-----------|------------:|---------:|------------:|---------:| | ascii | 2.06 ms | 2.13 ms | 2.70 ms | 2.54 ms | | styled | 2.41 ms | 2.29 ms | 6.56 ms | 6.75 ms | | truecolor | 5.02 ms | 4.95 ms | 11.86 ms | 12.05 ms | | cjk | 6.03 ms | 6.23 ms | 11.60 ms | 11.39 ms | | grapheme | 8.36 ms | 8.72 ms | 11.39 ms | 11.27 ms | | native | mode | prev | this | |--------|--------|--------:|--------:| | ascii | encode | 25.6 ms | 24.4 ms | | ascii | decode | 48.1 ms | 45.1 ms | | utf8 | encode | 41.7 ms | 41.9 ms | | utf8 | decode | 58.5 ms | 58.6 ms | --- src/terminal/snapshot/grid.zig | 17 +++++++++++++---- src/terminal/snapshot/page.zig | 15 +++++++++++---- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/src/terminal/snapshot/grid.zig b/src/terminal/snapshot/grid.zig index 1fc946e31..3a890913b 100644 --- a/src/terminal/snapshot/grid.zig +++ b/src/terminal/snapshot/grid.zig @@ -1515,6 +1515,11 @@ fn Remap(comptime Id: type) type { /// semantics. seen: std.DynamicBitSetUnmanaged, + /// A remap with no entries at all: every lookup is unmapped. Use + /// this instead of `init` when the encoded table is empty so pages + /// without styles or hyperlinks allocate nothing. + pub const empty: Self = .{ .entries = &.{}, .seen = .{} }; + pub fn init(alloc: Allocator) Allocator.Error!Self { const entries = try alloc.alloc(Id, capacity); errdefer alloc.free(entries); @@ -1527,25 +1532,29 @@ fn Remap(comptime Id: type) type { } pub fn deinit(self: *Self, alloc: Allocator) void { - alloc.free(self.entries); - self.seen.deinit(alloc); + if (self.entries.len != 0) { + alloc.free(self.entries); + self.seen.deinit(alloc); + } self.* = undefined; } - /// Record one encoded-to-native mapping. + /// Record one encoded-to-native mapping. Illegal on `empty`. pub fn put(self: *Self, encoded: Id, native: Id) void { assert(!self.seen.isSet(encoded)); self.entries[encoded] = native; self.seen.set(encoded); } - /// Whether the encoded ID already has an entry, even a default one. + /// Whether the encoded ID already has an entry, even a default + /// one. Illegal on `empty`. pub fn contains(self: *const Self, encoded: Id) bool { return self.seen.isSet(encoded); } /// The native ID for an encoded ID, or zero when unmapped. pub inline fn get(self: *const Self, encoded: Id) Id { + if (self.entries.len == 0) return 0; return self.entries[encoded]; } }; diff --git a/src/terminal/snapshot/page.zig b/src/terminal/snapshot/page.zig index bd64e483e..0acd51718 100644 --- a/src/terminal/snapshot/page.zig +++ b/src/terminal/snapshot/page.zig @@ -367,12 +367,19 @@ fn decodePayloadBody( page.pauseIntegrityChecks(true); defer page.pauseIntegrityChecks(false); - var style_remap = grid.StyleRemap.init(alloc) catch - return error.OutOfMemory; + // Pages without styles or hyperlinks, the common case for plain + // scrollback, skip the remap tables entirely: every encoded cell ID + // resolves to the default through the empty remap. + var style_remap: grid.StyleRemap = if (header.style_count > 0) + grid.StyleRemap.init(alloc) catch return error.OutOfMemory + else + .empty; defer style_remap.deinit(alloc); - var hyperlink_remap = grid.HyperlinkRemap.init(alloc) catch - return error.OutOfMemory; + var hyperlink_remap: grid.HyperlinkRemap = if (header.hyperlink_count > 0) + grid.HyperlinkRemap.init(alloc) catch return error.OutOfMemory + else + .empty; defer hyperlink_remap.deinit(alloc); // Styles