Merge pull request #1880 from qwerasd205/fix-set

Fix a few RefCountedSet problems
This commit is contained in:
Mitchell Hashimoto
2024-06-24 21:01:52 -07:00
committed by GitHub
6 changed files with 210 additions and 108 deletions

View File

@ -37,7 +37,7 @@ pub fn render(page: *const terminal.Page) void {
} }
{ {
_ = cimgui.c.igTableSetColumnIndex(1); _ = cimgui.c.igTableSetColumnIndex(1);
cimgui.c.igText("%d", page.styles.count(page.memory)); cimgui.c.igText("%d", page.styles.count());
} }
} }
{ {

View File

@ -490,6 +490,12 @@ pub fn clone(
src.* = old_dst; src.* = old_dst;
dirty.setValue(i, dirty.isSet(i + chunk.start)); dirty.setValue(i, dirty.isSet(i + chunk.start));
} }
// We need to clear the rows we're about to truncate.
for (len..page.data.size.rows) |i| {
page.data.clearCells(&rows[i], 0, page.data.size.cols);
}
page.data.size.rows = @intCast(len); page.data.size.rows = @intCast(len);
total_rows += len; total_rows += len;
@ -1839,7 +1845,7 @@ pub const AdjustCapacity = struct {
/// Adjust the number of styles in the page. This may be /// Adjust the number of styles in the page. This may be
/// rounded up if necessary to fit alignment requirements, /// rounded up if necessary to fit alignment requirements,
/// but it will never be rounded down. /// but it will never be rounded down.
styles: ?u16 = null, styles: ?usize = null,
/// Adjust the number of available grapheme bytes in the page. /// Adjust the number of available grapheme bytes in the page.
grapheme_bytes: ?usize = null, grapheme_bytes: ?usize = null,
@ -1871,7 +1877,7 @@ pub fn adjustCapacity(
var cap = page.data.capacity; var cap = page.data.capacity;
if (adjustment.styles) |v| { if (adjustment.styles) |v| {
const aligned = try std.math.ceilPowerOfTwo(u16, v); const aligned = try std.math.ceilPowerOfTwo(usize, v);
cap.styles = @max(cap.styles, aligned); cap.styles = @max(cap.styles, aligned);
} }
if (adjustment.grapheme_bytes) |v| { if (adjustment.grapheme_bytes) |v| {
@ -4759,6 +4765,56 @@ test "PageList clone partial trimmed left" {
try testing.expectEqual(@as(usize, 40), s2.totalRows()); try testing.expectEqual(@as(usize, 40), s2.totalRows());
} }
test "PageList clone partial trimmed left reclaims styles" {
const testing = std.testing;
const alloc = testing.allocator;
var s = try init(alloc, 80, 20, null);
defer s.deinit();
try testing.expectEqual(@as(usize, s.rows), s.totalRows());
try s.growRows(30);
// Style the rows we're trimming
{
try testing.expect(s.pages.first == s.pages.last);
const page = &s.pages.first.?.data;
const style: stylepkg.Style = .{ .flags = .{ .bold = true } };
const style_id = try page.styles.add(page.memory, style);
var it = s.rowIterator(.left_up, .{ .screen = .{} }, .{ .screen = .{ .y = 9 } });
while (it.next()) |p| {
const rac = p.rowAndCell();
rac.row.styled = true;
rac.cell.* = .{
.content_tag = .codepoint,
.content = .{ .codepoint = 'A' },
.style_id = style_id,
};
page.styles.use(page.memory, style_id);
}
// We're over-counted by 1 because `add` implies `use`.
page.styles.release(page.memory, style_id);
// Expect to have one style
try testing.expectEqual(1, page.styles.count());
}
var s2 = try s.clone(.{
.top = .{ .screen = .{ .y = 10 } },
.memory = .{ .alloc = alloc },
});
defer s2.deinit();
try testing.expectEqual(@as(usize, 40), s2.totalRows());
{
try testing.expect(s2.pages.first == s2.pages.last);
const page = &s2.pages.first.?.data;
try testing.expectEqual(0, page.styles.count());
}
}
test "PageList clone partial trimmed both" { test "PageList clone partial trimmed both" {
const testing = std.testing; const testing = std.testing;
const alloc = testing.allocator; const alloc = testing.allocator;

View File

@ -1175,7 +1175,7 @@ pub fn setAttribute(self: *Screen, attr: sgr.Attribute) !void {
pub fn manualStyleUpdate(self: *Screen) !void { pub fn manualStyleUpdate(self: *Screen) !void {
var page = &self.cursor.page_pin.page.data; var page = &self.cursor.page_pin.page.data;
// std.log.warn("active styles={}", .{page.styles.count(page.memory)}); // std.log.warn("active styles={}", .{page.styles.count()});
// Release our previous style if it was not default. // Release our previous style if it was not default.
if (self.cursor.style_id != style.default_id) { if (self.cursor.style_id != style.default_id) {
@ -1201,12 +1201,17 @@ pub fn manualStyleUpdate(self: *Screen) !void {
const id = page.styles.add( const id = page.styles.add(
page.memory, page.memory,
self.cursor.style, self.cursor.style,
) catch id: { ) catch |err| id: {
// Our style map is full. Let's allocate a new // Our style map is full or needs to be rehashed,
// page by doubling the size and then try again. // so we allocate a new page, which will rehash,
// and double the style capacity for it if it was
// full.
const node = try self.pages.adjustCapacity( const node = try self.pages.adjustCapacity(
self.cursor.page_pin.page, self.cursor.page_pin.page,
.{ .styles = page.capacity.styles * 2 }, switch (err) {
error.OutOfMemory => .{ .styles = page.capacity.styles * 2 },
error.NeedsRehash => .{},
},
); );
page = &node.data; page = &node.data;
@ -2388,17 +2393,17 @@ test "Screen cursorCopy style deref" {
var s2 = try Screen.init(alloc, 10, 10, 0); var s2 = try Screen.init(alloc, 10, 10, 0);
defer s2.deinit(); defer s2.deinit();
const page = s2.cursor.page_pin.page.data; const page = &s2.cursor.page_pin.page.data;
// Bold should create our style // Bold should create our style
try s2.setAttribute(.{ .bold = {} }); try s2.setAttribute(.{ .bold = {} });
try testing.expectEqual(@as(usize, 1), page.styles.count(page.memory)); try testing.expectEqual(@as(usize, 1), page.styles.count());
try testing.expect(s2.cursor.style.flags.bold); try testing.expect(s2.cursor.style.flags.bold);
// Copy default style, should release our style // Copy default style, should release our style
try s2.cursorCopy(s.cursor); try s2.cursorCopy(s.cursor);
try testing.expect(!s2.cursor.style.flags.bold); try testing.expect(!s2.cursor.style.flags.bold);
try testing.expectEqual(@as(usize, 0), page.styles.count(page.memory)); try testing.expectEqual(@as(usize, 0), page.styles.count());
} }
test "Screen cursorCopy style copy" { test "Screen cursorCopy style copy" {
@ -2411,10 +2416,10 @@ test "Screen cursorCopy style copy" {
var s2 = try Screen.init(alloc, 10, 10, 0); var s2 = try Screen.init(alloc, 10, 10, 0);
defer s2.deinit(); defer s2.deinit();
const page = s2.cursor.page_pin.page.data; const page = &s2.cursor.page_pin.page.data;
try s2.cursorCopy(s.cursor); try s2.cursorCopy(s.cursor);
try testing.expect(s2.cursor.style.flags.bold); try testing.expect(s2.cursor.style.flags.bold);
try testing.expectEqual(@as(usize, 1), page.styles.count(page.memory)); try testing.expectEqual(@as(usize, 1), page.styles.count());
} }
test "Screen style basics" { test "Screen style basics" {
@ -2423,19 +2428,19 @@ test "Screen style basics" {
var s = try Screen.init(alloc, 80, 24, 1000); var s = try Screen.init(alloc, 80, 24, 1000);
defer s.deinit(); defer s.deinit();
const page = s.cursor.page_pin.page.data; const page = &s.cursor.page_pin.page.data;
try testing.expectEqual(@as(usize, 0), page.styles.count(page.memory)); try testing.expectEqual(@as(usize, 0), page.styles.count());
// Set a new style // Set a new style
try s.setAttribute(.{ .bold = {} }); try s.setAttribute(.{ .bold = {} });
try testing.expect(s.cursor.style_id != 0); try testing.expect(s.cursor.style_id != 0);
try testing.expectEqual(@as(usize, 1), page.styles.count(page.memory)); try testing.expectEqual(@as(usize, 1), page.styles.count());
try testing.expect(s.cursor.style.flags.bold); try testing.expect(s.cursor.style.flags.bold);
// Set another style, we should still only have one since it was unused // Set another style, we should still only have one since it was unused
try s.setAttribute(.{ .italic = {} }); try s.setAttribute(.{ .italic = {} });
try testing.expect(s.cursor.style_id != 0); try testing.expect(s.cursor.style_id != 0);
try testing.expectEqual(@as(usize, 1), page.styles.count(page.memory)); try testing.expectEqual(@as(usize, 1), page.styles.count());
try testing.expect(s.cursor.style.flags.italic); try testing.expect(s.cursor.style.flags.italic);
} }
@ -2445,18 +2450,18 @@ test "Screen style reset to default" {
var s = try Screen.init(alloc, 80, 24, 1000); var s = try Screen.init(alloc, 80, 24, 1000);
defer s.deinit(); defer s.deinit();
const page = s.cursor.page_pin.page.data; const page = &s.cursor.page_pin.page.data;
try testing.expectEqual(@as(usize, 0), page.styles.count(page.memory)); try testing.expectEqual(@as(usize, 0), page.styles.count());
// Set a new style // Set a new style
try s.setAttribute(.{ .bold = {} }); try s.setAttribute(.{ .bold = {} });
try testing.expect(s.cursor.style_id != 0); try testing.expect(s.cursor.style_id != 0);
try testing.expectEqual(@as(usize, 1), page.styles.count(page.memory)); try testing.expectEqual(@as(usize, 1), page.styles.count());
// Reset to default // Reset to default
try s.setAttribute(.{ .reset_bold = {} }); try s.setAttribute(.{ .reset_bold = {} });
try testing.expect(s.cursor.style_id == 0); try testing.expect(s.cursor.style_id == 0);
try testing.expectEqual(@as(usize, 0), page.styles.count(page.memory)); try testing.expectEqual(@as(usize, 0), page.styles.count());
} }
test "Screen style reset with unset" { test "Screen style reset with unset" {
@ -2465,18 +2470,18 @@ test "Screen style reset with unset" {
var s = try Screen.init(alloc, 80, 24, 1000); var s = try Screen.init(alloc, 80, 24, 1000);
defer s.deinit(); defer s.deinit();
const page = s.cursor.page_pin.page.data; const page = &s.cursor.page_pin.page.data;
try testing.expectEqual(@as(usize, 0), page.styles.count(page.memory)); try testing.expectEqual(@as(usize, 0), page.styles.count());
// Set a new style // Set a new style
try s.setAttribute(.{ .bold = {} }); try s.setAttribute(.{ .bold = {} });
try testing.expect(s.cursor.style_id != 0); try testing.expect(s.cursor.style_id != 0);
try testing.expectEqual(@as(usize, 1), page.styles.count(page.memory)); try testing.expectEqual(@as(usize, 1), page.styles.count());
// Reset to default // Reset to default
try s.setAttribute(.{ .unset = {} }); try s.setAttribute(.{ .unset = {} });
try testing.expect(s.cursor.style_id == 0); try testing.expect(s.cursor.style_id == 0);
try testing.expectEqual(@as(usize, 0), page.styles.count(page.memory)); try testing.expectEqual(@as(usize, 0), page.styles.count());
} }
test "Screen clearRows active one line" { test "Screen clearRows active one line" {
@ -2522,13 +2527,13 @@ test "Screen clearRows active styled line" {
try s.setAttribute(.{ .unset = {} }); try s.setAttribute(.{ .unset = {} });
// We should have one style // We should have one style
const page = s.cursor.page_pin.page.data; const page = &s.cursor.page_pin.page.data;
try testing.expectEqual(@as(usize, 1), page.styles.count(page.memory)); try testing.expectEqual(@as(usize, 1), page.styles.count());
s.clearRows(.{ .active = .{} }, null, false); s.clearRows(.{ .active = .{} }, null, false);
// We should have none because active cleared it // We should have none because active cleared it
try testing.expectEqual(@as(usize, 0), page.styles.count(page.memory)); try testing.expectEqual(@as(usize, 0), page.styles.count());
const str = try s.dumpStringAlloc(alloc, .{ .screen = .{} }); const str = try s.dumpStringAlloc(alloc, .{ .screen = .{} });
defer alloc.free(str); defer alloc.free(str);

View File

@ -2826,8 +2826,8 @@ test "Terminal: print over wide char with bold" {
try t.print(0x1F600); // Smiley face try t.print(0x1F600); // Smiley face
// verify we have styles in our style map // verify we have styles in our style map
{ {
const page = t.screen.cursor.page_pin.page.data; const page = &t.screen.cursor.page_pin.page.data;
try testing.expectEqual(@as(usize, 1), page.styles.count(page.memory)); try testing.expectEqual(@as(usize, 1), page.styles.count());
} }
// Go back and overwrite with no style // Go back and overwrite with no style
@ -2837,8 +2837,8 @@ test "Terminal: print over wide char with bold" {
// verify our style is gone // verify our style is gone
{ {
const page = t.screen.cursor.page_pin.page.data; const page = &t.screen.cursor.page_pin.page.data;
try testing.expectEqual(@as(usize, 0), page.styles.count(page.memory)); try testing.expectEqual(@as(usize, 0), page.styles.count());
} }
try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } })); try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } }));
@ -2856,8 +2856,8 @@ test "Terminal: print over wide char with bg color" {
try t.print(0x1F600); // Smiley face try t.print(0x1F600); // Smiley face
// verify we have styles in our style map // verify we have styles in our style map
{ {
const page = t.screen.cursor.page_pin.page.data; const page = &t.screen.cursor.page_pin.page.data;
try testing.expectEqual(@as(usize, 1), page.styles.count(page.memory)); try testing.expectEqual(@as(usize, 1), page.styles.count());
} }
// Go back and overwrite with no style // Go back and overwrite with no style
@ -2867,8 +2867,8 @@ test "Terminal: print over wide char with bg color" {
// verify our style is gone // verify our style is gone
{ {
const page = t.screen.cursor.page_pin.page.data; const page = &t.screen.cursor.page_pin.page.data;
try testing.expectEqual(@as(usize, 0), page.styles.count(page.memory)); try testing.expectEqual(@as(usize, 0), page.styles.count());
} }
try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } })); try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } }));
@ -3322,7 +3322,7 @@ test "Terminal: overwrite multicodepoint grapheme clears grapheme data" {
try testing.expectEqual(@as(usize, 2), t.screen.cursor.x); try testing.expectEqual(@as(usize, 2), t.screen.cursor.x);
// We should have one cell with graphemes // We should have one cell with graphemes
const page = t.screen.cursor.page_pin.page.data; const page = &t.screen.cursor.page_pin.page.data;
try testing.expectEqual(@as(usize, 1), page.graphemeCount()); try testing.expectEqual(@as(usize, 1), page.graphemeCount());
// Move back and overwrite wide // Move back and overwrite wide
@ -3362,7 +3362,7 @@ test "Terminal: overwrite multicodepoint grapheme tail clears grapheme data" {
try testing.expectEqual(@as(usize, 2), t.screen.cursor.x); try testing.expectEqual(@as(usize, 2), t.screen.cursor.x);
// We should have one cell with graphemes // We should have one cell with graphemes
const page = t.screen.cursor.page_pin.page.data; const page = &t.screen.cursor.page_pin.page.data;
try testing.expectEqual(@as(usize, 1), page.graphemeCount()); try testing.expectEqual(@as(usize, 1), page.graphemeCount());
// Move back and overwrite wide // Move back and overwrite wide
@ -4534,8 +4534,8 @@ test "Terminal: insertLines handles style refs" {
try t.setAttribute(.{ .unset = {} }); try t.setAttribute(.{ .unset = {} });
// verify we have styles in our style map // verify we have styles in our style map
const page = t.screen.cursor.page_pin.page.data; const page = &t.screen.cursor.page_pin.page.data;
try testing.expectEqual(@as(usize, 1), page.styles.count(page.memory)); try testing.expectEqual(@as(usize, 1), page.styles.count());
t.setCursorPos(2, 2); t.setCursorPos(2, 2);
t.insertLines(1); t.insertLines(1);
@ -4547,7 +4547,7 @@ test "Terminal: insertLines handles style refs" {
} }
// verify we have no styles in our style map // verify we have no styles in our style map
try testing.expectEqual(@as(usize, 0), page.styles.count(page.memory)); try testing.expectEqual(@as(usize, 0), page.styles.count());
} }
test "Terminal: insertLines outside of scroll region" { test "Terminal: insertLines outside of scroll region" {
@ -5336,14 +5336,14 @@ test "Terminal: eraseChars handles refcounted styles" {
try t.print('C'); try t.print('C');
// verify we have styles in our style map // verify we have styles in our style map
const page = t.screen.cursor.page_pin.page.data; const page = &t.screen.cursor.page_pin.page.data;
try testing.expectEqual(@as(usize, 1), page.styles.count(page.memory)); try testing.expectEqual(@as(usize, 1), page.styles.count());
t.setCursorPos(1, 1); t.setCursorPos(1, 1);
t.eraseChars(2); t.eraseChars(2);
// verify we have no styles in our style map // verify we have no styles in our style map
try testing.expectEqual(@as(usize, 0), page.styles.count(page.memory)); try testing.expectEqual(@as(usize, 0), page.styles.count());
} }
test "Terminal: eraseChars protected attributes respected with iso" { test "Terminal: eraseChars protected attributes respected with iso" {
@ -7043,7 +7043,7 @@ test "Terminal: bold style" {
const cell = list_cell.cell; const cell = list_cell.cell;
try testing.expectEqual(@as(u21, 'A'), cell.content.codepoint); try testing.expectEqual(@as(u21, 'A'), cell.content.codepoint);
try testing.expect(cell.style_id != 0); try testing.expect(cell.style_id != 0);
const page = t.screen.cursor.page_pin.page.data; const page = &t.screen.cursor.page_pin.page.data;
try testing.expect(page.styles.refCount(page.memory, t.screen.cursor.style_id) > 1); try testing.expect(page.styles.refCount(page.memory, t.screen.cursor.style_id) > 1);
} }
} }
@ -7067,8 +7067,8 @@ test "Terminal: garbage collect overwritten" {
} }
// verify we have no styles in our style map // verify we have no styles in our style map
const page = t.screen.cursor.page_pin.page.data; const page = &t.screen.cursor.page_pin.page.data;
try testing.expectEqual(@as(usize, 0), page.styles.count(page.memory)); try testing.expectEqual(@as(usize, 0), page.styles.count());
} }
test "Terminal: do not garbage collect old styles in use" { test "Terminal: do not garbage collect old styles in use" {
@ -7089,8 +7089,8 @@ test "Terminal: do not garbage collect old styles in use" {
} }
// verify we have no styles in our style map // verify we have no styles in our style map
const page = t.screen.cursor.page_pin.page.data; const page = &t.screen.cursor.page_pin.page.data;
try testing.expectEqual(@as(usize, 1), page.styles.count(page.memory)); try testing.expectEqual(@as(usize, 1), page.styles.count());
} }
test "Terminal: print with style marks the row as styled" { test "Terminal: print with style marks the row as styled" {
@ -7425,7 +7425,7 @@ test "Terminal: insertBlanks deleting graphemes" {
try t.print(0x1F467); try t.print(0x1F467);
// We should have one cell with graphemes // We should have one cell with graphemes
const page = t.screen.cursor.page_pin.page.data; const page = &t.screen.cursor.page_pin.page.data;
try testing.expectEqual(@as(usize, 1), page.graphemeCount()); try testing.expectEqual(@as(usize, 1), page.graphemeCount());
t.setCursorPos(1, 1); t.setCursorPos(1, 1);
@ -7461,7 +7461,7 @@ test "Terminal: insertBlanks shift graphemes" {
try t.print(0x1F467); try t.print(0x1F467);
// We should have one cell with graphemes // We should have one cell with graphemes
const page = t.screen.cursor.page_pin.page.data; const page = &t.screen.cursor.page_pin.page.data;
try testing.expectEqual(@as(usize, 1), page.graphemeCount()); try testing.expectEqual(@as(usize, 1), page.graphemeCount());
t.setCursorPos(1, 1); t.setCursorPos(1, 1);

View File

@ -237,6 +237,7 @@ pub const Page = struct {
MissingStyle, MissingStyle,
UnmarkedStyleRow, UnmarkedStyleRow,
MismatchedStyleRef, MismatchedStyleRef,
ZombieStyles,
InvalidStyleCount, InvalidStyleCount,
InvalidSpacerTailLocation, InvalidSpacerTailLocation,
InvalidSpacerHeadLocation, InvalidSpacerHeadLocation,
@ -429,6 +430,37 @@ pub const Page = struct {
} }
} }
} }
// Verify there are no zombie styles, that is, styles in the
// set with ref counts > 0, which are not present in the page.
{
const styles_table = self.styles.table.ptr(self.memory)[0..self.styles.layout.table_cap];
const styles_items = self.styles.items.ptr(self.memory)[0..self.styles.layout.cap];
var zombies: usize = 0;
for (styles_table) |id| {
if (id == 0) continue;
const item = styles_items[id];
if (item.meta.ref == 0) continue;
const expected = styles_seen.get(id) orelse 0;
if (expected > 0) continue;
if (item.meta.ref > expected) {
zombies += 1;
}
}
// Just 1 zombie style might be the cursor style, so ignore it.
if (zombies > 1) {
log.warn(
"page integrity violation zombie styles count={}",
.{zombies},
);
return IntegrityError.ZombieStyles;
}
}
} }
/// Clone the contents of this page. This will allocate new memory /// Clone the contents of this page. This will allocate new memory
@ -468,7 +500,7 @@ pub const Page = struct {
return result; return result;
} }
pub const CloneFromError = Allocator.Error || error{OutOfMemory}; pub const CloneFromError = Allocator.Error || style.Set.AddError;
/// Clone the contents of another page into this page. The capacities /// Clone the contents of another page into this page. The capacities
/// can be different, but the size of the other page must fit into /// can be different, but the size of the other page must fit into
@ -1027,7 +1059,7 @@ pub const Capacity = struct {
rows: size.CellCountInt, rows: size.CellCountInt,
/// Number of unique styles that can be used on this page. /// Number of unique styles that can be used on this page.
styles: u16 = 16, styles: usize = 16,
/// Number of bytes to allocate for grapheme data. /// Number of bytes to allocate for grapheme data.
grapheme_bytes: usize = grapheme_bytes_default, grapheme_bytes: usize = grapheme_bytes_default,

View File

@ -13,9 +13,9 @@ const fastmem = @import("../fastmem.zig");
/// the exact memory requirement of a given capacity by calling `layout` /// the exact memory requirement of a given capacity by calling `layout`
/// and checking the total size. /// and checking the total size.
/// ///
/// When the set exceeds capacity, `error.OutOfMemory` is returned from /// When the set exceeds capacity, an `OutOfMemory` or `NeedsRehash` error
/// any memory-using methods. The caller is responsible for determining /// is returned from any memory-using methods. The caller is responsible
/// a path forward. /// for determining a path forward.
/// ///
/// This set is reference counted. Each item in the set has an associated /// This set is reference counted. Each item in the set has an associated
/// reference count. The caller is responsible for calling release for an /// reference count. The caller is responsible for calling release for an
@ -108,6 +108,9 @@ pub fn RefCountedSet(
/// The backing store of items /// The backing store of items
items: Offset(Item), items: Offset(Item),
/// The number of living items currently stored in the set.
living: Id = 0,
/// The next index to store an item at. /// The next index to store an item at.
/// Id 0 is reserved for unused items. /// Id 0 is reserved for unused items.
next_id: Id = 1, next_id: Id = 1,
@ -132,21 +135,22 @@ pub fn RefCountedSet(
/// ///
/// The returned layout `cap` property will be 1 more than the number /// The returned layout `cap` property will be 1 more than the number
/// of items that the set can actually store, since ID 0 is reserved. /// of items that the set can actually store, since ID 0 is reserved.
pub fn layout(cap: Id) Layout { pub fn layout(cap: usize) Layout {
// Experimentally, this load factor works quite well. // Experimentally, this load factor works quite well.
const load_factor = 0.8125; const load_factor = 0.8125;
const table_cap: Id = std.math.ceilPowerOfTwoAssert(Id, cap); assert(cap <= @as(usize, @intCast(std.math.maxInt(Id))) + 1);
const table_mask: Id = (@as(Id, 1) << std.math.log2_int(Id, table_cap)) - 1;
const items_cap: Id = @intFromFloat(load_factor * @as(f64, @floatFromInt(table_cap))); const table_cap: usize = std.math.ceilPowerOfTwoAssert(usize, cap);
const items_cap: usize = @intFromFloat(load_factor * @as(f64, @floatFromInt(table_cap)));
const table_mask: Id = @intCast((@as(usize, 1) << std.math.log2_int(usize, table_cap)) - 1);
const table_start = 0; const table_start = 0;
const table_cap_usize: usize = @intCast(table_cap); const table_end = table_start + table_cap * @sizeOf(Id);
const table_end = table_start + table_cap_usize * @sizeOf(Id);
const items_start = std.mem.alignForward(usize, table_end, @alignOf(Item)); const items_start = std.mem.alignForward(usize, table_end, @alignOf(Item));
const items_cap_usize: usize = @intCast(items_cap); const items_end = items_start + items_cap * @sizeOf(Item);
const items_end = items_start + items_cap_usize * @sizeOf(Item);
const total_size = items_end; const total_size = items_end;
@ -161,8 +165,8 @@ pub fn RefCountedSet(
} }
pub const Layout = struct { pub const Layout = struct {
cap: Id, cap: usize,
table_cap: Id, table_cap: usize,
table_mask: Id, table_mask: Id,
table_start: usize, table_start: usize,
items_start: usize, items_start: usize,
@ -184,12 +188,23 @@ pub fn RefCountedSet(
}; };
} }
/// Possible errors for `add` and `addWithId`.
pub const AddError = error{
/// There is not enough memory to add a new item.
/// Remove items or grow and reinitialize.
OutOfMemory,
/// The set needs to be rehashed, as there are many dead
/// items with lower IDs which are inaccessible for re-use.
NeedsRehash,
};
/// Add an item to the set if not present and increment its ref count. /// Add an item to the set if not present and increment its ref count.
/// ///
/// Returns the item's ID. /// Returns the item's ID.
/// ///
/// If the set has no more room, then an OutOfMemory error is returned. /// If the set has no more room, then an OutOfMemory error is returned.
pub fn add(self: *Self, base: anytype, value: T) error{OutOfMemory}!Id { pub fn add(self: *Self, base: anytype, value: T) AddError!Id {
const items = self.items.ptr(base); const items = self.items.ptr(base);
// Trim dead items from the end of the list. // Trim dead items from the end of the list.
@ -198,14 +213,34 @@ pub fn RefCountedSet(
self.deleteItem(base, self.next_id); self.deleteItem(base, self.next_id);
} }
// If we still don't have an available ID, we're out of memory. // If we still don't have an available ID, we can't continue.
if (self.next_id >= self.layout.cap) return error.OutOfMemory; if (self.next_id >= self.layout.cap) {
// Arbitrarily chosen, threshold for rehashing.
// If less than 90% of currently allocated IDs
// correspond to living items, we should rehash.
// Otherwise, claim we're out of memory because
// we assume that we'll end up running out of
// memory or rehashing again very soon if we
// rehash with only a few IDs left.
const rehash_threshold = 0.9;
if (self.living < @as(Id, @intFromFloat(@as(f64, @floatFromInt(self.layout.cap)) * rehash_threshold))) {
return AddError.NeedsRehash;
}
// If we don't have at least 10% dead items then
// we claim we're out of memory.
return AddError.OutOfMemory;
}
const id = self.upsert(base, value, self.next_id); const id = self.upsert(base, value, self.next_id);
items[id].meta.ref += 1; items[id].meta.ref += 1;
if (id == self.next_id) self.next_id += 1; if (id == self.next_id) self.next_id += 1;
if (items[id].meta.ref == 1) {
self.living += 1;
}
return id; return id;
} }
@ -215,7 +250,7 @@ pub fn RefCountedSet(
/// Returns the item's ID, or null if the provided ID was used. /// Returns the item's ID, or null if the provided ID was used.
/// ///
/// If the set has no more room, then an OutOfMemory error is returned. /// If the set has no more room, then an OutOfMemory error is returned.
pub fn addWithId(self: *Self, base: anytype, value: T, id: Id) error{OutOfMemory}!?Id { pub fn addWithId(self: *Self, base: anytype, value: T, id: Id) AddError!?Id {
const items = self.items.ptr(base); const items = self.items.ptr(base);
if (id < self.next_id) { if (id < self.next_id) {
@ -226,6 +261,8 @@ pub fn RefCountedSet(
items[added_id].meta.ref += 1; items[added_id].meta.ref += 1;
self.living += 1;
return if (added_id == id) null else added_id; return if (added_id == id) null else added_id;
} else if (self.context.eql(value, items[id].value)) { } else if (self.context.eql(value, items[id].value)) {
items[id].meta.ref += 1; items[id].meta.ref += 1;
@ -302,6 +339,7 @@ pub fn RefCountedSet(
assert(item.meta.ref > 0); assert(item.meta.ref > 0);
item.meta.ref -= 1; item.meta.ref -= 1;
if (item.meta.ref == 0) self.living -= 1;
} }
/// Release a specified number of references to an item by its ID. /// Release a specified number of references to an item by its ID.
@ -316,6 +354,10 @@ pub fn RefCountedSet(
assert(item.meta.ref >= n); assert(item.meta.ref >= n);
item.meta.ref -= n; item.meta.ref -= n;
if (item.meta.ref == 0) {
self.living -= 1;
}
} }
/// Get the ref count for an item by its ID. /// Get the ref count for an item by its ID.
@ -329,42 +371,8 @@ pub fn RefCountedSet(
} }
/// Get the current number of non-dead items in the set. /// Get the current number of non-dead items in the set.
/// pub fn count(self: *const Self) usize {
/// NOT DESIGNED TO BE USED OUTSIDE OF TESTING, this is a very slow return self.living;
/// operation, since it traverses the entire structure to count.
///
/// Additionally, because this is a testing method, it does extra
/// work to verify the integrity of the structure when called.
pub fn count(self: *const Self, base: anytype) usize {
const table = self.table.ptr(base);
const items = self.items.ptr(base);
// The number of items accessible through the table.
var tb_ct: usize = 0;
for (table[0..self.layout.table_cap]) |id| {
if (id != 0) {
const item = items[id];
if (item.meta.ref > 0) {
tb_ct += 1;
}
}
}
// The number of items accessible through the backing store.
// The two counts should always match- it shouldn't be possible
// to have untracked items in the backing store.
var it_ct: usize = 0;
for (items[0..self.layout.cap]) |it| {
if (it.meta.ref > 0) {
it_ct += 1;
}
}
assert(tb_ct == it_ct);
return tb_ct;
} }
/// Delete an item, removing any references from /// Delete an item, removing any references from
@ -390,7 +398,7 @@ pub fn RefCountedSet(
items[id] = .{}; items[id] = .{};
var p: Id = item.meta.bucket; var p: Id = item.meta.bucket;
var n: Id = (p + 1) & self.layout.table_mask; var n: Id = (p +% 1) & self.layout.table_mask;
while (table[n] != 0 and items[table[n]].meta.psl > 0) { while (table[n] != 0 and items[table[n]].meta.psl > 0) {
items[table[n]].meta.bucket = p; items[table[n]].meta.bucket = p;
@ -399,7 +407,7 @@ pub fn RefCountedSet(
self.psl_stats[items[table[n]].meta.psl] += 1; self.psl_stats[items[table[n]].meta.psl] += 1;
table[p] = table[n]; table[p] = table[n];
p = n; p = n;
n = (n + 1) & self.layout.table_mask; n = (p +% 1) & self.layout.table_mask;
} }
while (self.max_psl > 0 and self.psl_stats[self.max_psl] == 0) { while (self.max_psl > 0 and self.psl_stats[self.max_psl] == 0) {
@ -508,6 +516,7 @@ pub fn RefCountedSet(
chosen_id = id; chosen_id = id;
held_item.meta.bucket = p; held_item.meta.bucket = p;
self.psl_stats[item.meta.psl] -= 1;
self.psl_stats[held_item.meta.psl] += 1; self.psl_stats[held_item.meta.psl] += 1;
self.max_psl = @max(self.max_psl, held_item.meta.psl); self.max_psl = @max(self.max_psl, held_item.meta.psl);