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 |
This commit is contained in:
Mitchell Hashimoto
2026-08-15 09:14:31 -07:00
parent 7c1014ef66
commit 47a5182621
2 changed files with 24 additions and 8 deletions

View File

@@ -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];
}
};

View File

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