mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-07-31 04:39:01 +00:00
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% |
This commit is contained in:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user