From 62cc056ae31f346d6060f16041d0539448235eb6 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 11 Jul 2026 19:55:57 -0700 Subject: [PATCH] terminal: reset dead set entries in bulk Reset the RefCountedSet table in one pass when every stored item is dead. The previous add path deleted trailing entries individually, repeatedly backshifting probe sequences even though no live entry needed to survive. Deletion callbacks still run exactly once before storage is cleared, so page-owned hyperlink values retain their existing ownership semantics. ReleaseFast timings are seven-run median user CPU times. | Workload | Before | After | Change | | --- | ---: | ---: | ---: | | 128 styles, dead churn | 0.72s | 0.60s | -16.7% | | 4,096 styles, dead churn | 0.77s | 0.66s | -14.3% | | Clear/redraw SGR stream | 0.65s | 0.61s | -6.2% | --- src/terminal/ref_counted_set.zig | 86 ++++++++++++++++++++++++++++++-- 1 file changed, 82 insertions(+), 4 deletions(-) diff --git a/src/terminal/ref_counted_set.zig b/src/terminal/ref_counted_set.zig index c32723d01..fbf407b4e 100644 --- a/src/terminal/ref_counted_set.zig +++ b/src/terminal/ref_counted_set.zig @@ -278,10 +278,17 @@ pub fn RefCountedSet( const items = self.items.ptr(base); - // Trim dead items from the end of the list. - while (self.next_id > 1 and items[self.next_id - 1].meta.ref == 0) { - self.next_id -= 1; - self.deleteItem(base, self.next_id, ctx); + // If every item is dead, reset the table in bulk. Deleting each + // item individually would repeatedly backshift probe sequences + // even though no entries need to survive. + if (self.living == 0 and self.next_id > 1) { + self.clearDead(base, ctx); + } else { + // Trim dead items from the end of the list. + while (self.next_id > 1 and items[self.next_id - 1].meta.ref == 0) { + self.next_id -= 1; + self.deleteItem(base, self.next_id, ctx); + } } const hash: u64 = ctx.hash(value); @@ -486,6 +493,29 @@ pub fn RefCountedSet( return self.living; } + /// Reset the table after every stored item has become dead. + fn clearDead(self: *Self, base: anytype, ctx: Context) void { + assert(self.living == 0); + + const table = self.table.ptr(base)[0..self.layout.table_cap]; + const items = self.items.ptr(base)[0..self.next_id]; + + if (comptime @hasDecl(Context, "deleted")) { + for (items[1..]) |item| { + if (item.meta.psl != std.math.maxInt(Id)) { + assert(item.meta.ref == 0); + ctx.deleted(item.value); + } + } + } + + @memset(table, 0); + @memset(items[1..], .{}); + self.next_id = 1; + self.max_psl = 0; + self.psl_stats = @splat(0); + } + /// Delete an item, removing any references from /// the table, and freeing its ID to be reused. fn deleteItem(self: *Self, base: anytype, id: Id, ctx: Context) void { @@ -855,6 +885,54 @@ test "RefCountedSet random operations against live-value oracle" { } } +test "RefCountedSet bulk reset calls deleted exactly once" { + const testing = std.testing; + const Context = struct { + deleted_count: *usize, + + pub fn hash(_: *const @This(), value: u8) u64 { + return std.hash.int(@as(u64, value)); + } + + pub fn eql(_: *const @This(), a: u8, b: u8) bool { + return a == b; + } + + pub fn deleted(self: *const @This(), _: u8) void { + self.deleted_count.* += 1; + } + }; + const Set = RefCountedSet(u8, u8, u16, Context); + + const layout: Set.Layout = .init(16); + const buf = try testing.allocator.alignedAlloc( + u8, + Set.base_align, + layout.total_size, + ); + defer testing.allocator.free(buf); + + var deleted_count: usize = 0; + var set = Set.init(.init(buf), layout, .{ + .deleted_count = &deleted_count, + }); + + const first = try set.add(buf, 1); + const second = try set.add(buf, 2); + const third = try set.add(buf, 3); + set.release(buf, first); + set.release(buf, second); + set.release(buf, third); + + // The next add resets all three dead entries before inserting. The new + // value starts from the lowest ID again and remains owned by the set. + const replacement = try set.add(buf, 4); + try testing.expectEqual(@as(usize, 3), deleted_count); + try testing.expectEqual(@as(u8, 1), replacement); + try testing.expectEqual(@as(usize, 1), set.count()); + try testing.expectEqual(replacement, set.lookup(buf, 4).?); +} + test "RefCountedSet zero capacity bypasses context" { const testing = std.testing; const Context = struct {