mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-15 16:26:08 +03:00
perf: introduce RefCountedSet structure, use it for Style.Set
This commit is contained in:
@ -1158,17 +1158,17 @@ fn reflowPage(
|
||||
|
||||
// If the source cell has a style, we need to copy it.
|
||||
if (src_cursor.page_cell.style_id != stylepkg.default_id) {
|
||||
const src_style = src_cursor.page.styles.lookupId(
|
||||
const src_style = src_cursor.page.styles.get(
|
||||
src_cursor.page.memory,
|
||||
src_cursor.page_cell.style_id,
|
||||
).?.*;
|
||||
|
||||
const dst_md = try dst_cursor.page.styles.upsert(
|
||||
).*;
|
||||
if (try dst_cursor.page.styles.addWithId(
|
||||
dst_cursor.page.memory,
|
||||
src_style,
|
||||
);
|
||||
dst_md.ref += 1;
|
||||
dst_cursor.page_cell.style_id = dst_md.id;
|
||||
src_cursor.page_cell.style_id,
|
||||
)) |id| {
|
||||
dst_cursor.page_cell.style_id = id;
|
||||
}
|
||||
dst_cursor.page_row.styled = true;
|
||||
}
|
||||
}
|
||||
@ -1905,18 +1905,6 @@ pub fn adjustCapacity(
|
||||
return new_page;
|
||||
}
|
||||
|
||||
/// Compact a page, reallocating to minimize the amount of memory
|
||||
/// required for the page. This is useful when we've overflowed ID
|
||||
/// spaces, are archiving a page, etc.
|
||||
///
|
||||
/// Note today: this doesn't minimize the memory usage, but it does
|
||||
/// fix style ID overflow. A future update can shrink the memory
|
||||
/// allocation.
|
||||
pub fn compact(self: *PageList, page: *List.Node) !*List.Node {
|
||||
// Adjusting capacity with no adjustments forces a reallocation.
|
||||
return try self.adjustCapacity(page, .{});
|
||||
}
|
||||
|
||||
/// Create a new page node. This does not add it to the list and this
|
||||
/// does not do any memory size accounting with max_size/page_size.
|
||||
fn createPage(
|
||||
@ -3039,10 +3027,10 @@ pub const Pin = struct {
|
||||
/// Returns the style for the given cell in this pin.
|
||||
pub fn style(self: Pin, cell: *pagepkg.Cell) stylepkg.Style {
|
||||
if (cell.style_id == stylepkg.default_id) return .{};
|
||||
return self.page.data.styles.lookupId(
|
||||
return self.page.data.styles.get(
|
||||
self.page.data.memory,
|
||||
cell.style_id,
|
||||
).?.*;
|
||||
).*;
|
||||
}
|
||||
|
||||
/// Check if this pin is dirty.
|
||||
@ -3302,10 +3290,10 @@ const Cell = struct {
|
||||
/// Not meant for non-test usage since this is inefficient.
|
||||
pub fn style(self: Cell) stylepkg.Style {
|
||||
if (self.cell.style_id == stylepkg.default_id) return .{};
|
||||
return self.page.data.styles.lookupId(
|
||||
return self.page.data.styles.get(
|
||||
self.page.data.memory,
|
||||
self.cell.style_id,
|
||||
).?.*;
|
||||
).*;
|
||||
}
|
||||
|
||||
/// Gets the screen point for the given cell.
|
||||
@ -7363,18 +7351,20 @@ test "PageList resize reflow less cols copy style" {
|
||||
|
||||
// Create a style
|
||||
const style: stylepkg.Style = .{ .flags = .{ .bold = true } };
|
||||
const style_md = try page.styles.upsert(page.memory, style);
|
||||
const style_id = try page.styles.add(page.memory, style);
|
||||
|
||||
for (0..s.cols - 1) |x| {
|
||||
const rac = page.getRowAndCell(x, 0);
|
||||
rac.cell.* = .{
|
||||
.content_tag = .codepoint,
|
||||
.content = .{ .codepoint = @intCast(x) },
|
||||
.style_id = style_md.id,
|
||||
.style_id = style_id,
|
||||
};
|
||||
|
||||
style_md.ref += 1;
|
||||
page.styles.use(page.memory, style_id);
|
||||
}
|
||||
|
||||
// We're over-counted by 1 because `add` implies `use`.
|
||||
page.styles.release(page.memory, style_id);
|
||||
}
|
||||
|
||||
// Resize
|
||||
@ -7391,10 +7381,10 @@ test "PageList resize reflow less cols copy style" {
|
||||
const style_id = rac.cell.style_id;
|
||||
try testing.expect(style_id != 0);
|
||||
|
||||
const style = offset.page.data.styles.lookupId(
|
||||
const style = offset.page.data.styles.get(
|
||||
offset.page.data.memory,
|
||||
style_id,
|
||||
).?;
|
||||
);
|
||||
try testing.expect(style.flags.bold);
|
||||
|
||||
const row = rac.row;
|
||||
|
@ -100,7 +100,6 @@ pub const Cursor = struct {
|
||||
/// we change pages we need to ensure that we update that page with
|
||||
/// our style when used.
|
||||
style_id: style.Id = style.default_id,
|
||||
style_ref: ?*size.CellCountInt = null,
|
||||
|
||||
/// The pointers into the page list where the cursor is currently
|
||||
/// located. This makes it faster to move the cursor.
|
||||
@ -202,31 +201,6 @@ pub fn assertIntegrity(self: *const Screen) void {
|
||||
) orelse unreachable;
|
||||
assert(self.cursor.x == pt.active.x);
|
||||
assert(self.cursor.y == pt.active.y);
|
||||
|
||||
if (self.cursor.style_id == style.default_id) {
|
||||
// If our style is default, we should have no refs.
|
||||
assert(self.cursor.style.default());
|
||||
assert(self.cursor.style_ref == null);
|
||||
} else {
|
||||
// If our style is not default, we should have a ref.
|
||||
assert(!self.cursor.style.default());
|
||||
assert(self.cursor.style_ref != null);
|
||||
|
||||
// Further, the ref should be valid within the current page.
|
||||
const page = &self.cursor.page_pin.page.data;
|
||||
const page_style = page.styles.lookupId(
|
||||
page.memory,
|
||||
self.cursor.style_id,
|
||||
).?.*;
|
||||
assert(self.cursor.style.eql(page_style));
|
||||
|
||||
// The metadata pointer should be equal to the ref.
|
||||
const md = page.styles.upsert(
|
||||
page.memory,
|
||||
page_style,
|
||||
) catch unreachable;
|
||||
assert(&md.ref == self.cursor.style_ref.?);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -524,14 +498,6 @@ pub fn cursorReload(self: *Screen) void {
|
||||
// If we have a style, we need to ensure it is in the page because this
|
||||
// method may also be called after a page change.
|
||||
if (self.cursor.style_id != style.default_id) {
|
||||
// We set our ref to null because manualStyleUpdate will refresh it.
|
||||
// If we had a valid ref and it was zero before, then manualStyleUpdate
|
||||
// will reload the same ref.
|
||||
//
|
||||
// We want to avoid the scenario this was non-null but the pointer
|
||||
// is now invalid because it pointed to a page that no longer exists.
|
||||
self.cursor.style_ref = null;
|
||||
|
||||
self.manualStyleUpdate() catch |err| {
|
||||
// This failure should not happen because manualStyleUpdate
|
||||
// handles page splitting, overflow, and more. This should only
|
||||
@ -540,7 +506,6 @@ pub fn cursorReload(self: *Screen) void {
|
||||
log.err("failed to update style on cursor reload err={}", .{err});
|
||||
self.cursor.style = .{};
|
||||
self.cursor.style_id = 0;
|
||||
self.cursor.style_ref = null;
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -595,7 +560,6 @@ pub fn cursorDownScroll(self: *Screen) !void {
|
||||
log.err("failed to update style on cursor scroll err={}", .{err});
|
||||
self.cursor.style = .{};
|
||||
self.cursor.style_id = 0;
|
||||
self.cursor.style_ref = null;
|
||||
};
|
||||
}
|
||||
} else {
|
||||
@ -683,7 +647,6 @@ pub fn cursorCopy(self: *Screen, other: Cursor) !void {
|
||||
// invalid.
|
||||
self.cursor.style = .{};
|
||||
self.cursor.style_id = 0;
|
||||
self.cursor.style_ref = null;
|
||||
|
||||
// We need to keep our old x/y because that is our cursorAbsolute
|
||||
// will fix up our pointers.
|
||||
@ -698,7 +661,6 @@ pub fn cursorCopy(self: *Screen, other: Cursor) !void {
|
||||
// We keep the old style ref so manualStyleUpdate can clean our old style up.
|
||||
self.cursor.style = other.style;
|
||||
self.cursor.style_id = old.style_id;
|
||||
self.cursor.style_ref = old.style_ref;
|
||||
try self.manualStyleUpdate();
|
||||
}
|
||||
|
||||
@ -744,7 +706,6 @@ fn cursorChangePin(self: *Screen, new: Pin) void {
|
||||
log.err("failed to update style on cursor change err={}", .{err});
|
||||
self.cursor.style = .{};
|
||||
self.cursor.style_id = 0;
|
||||
self.cursor.style_ref = null;
|
||||
};
|
||||
}
|
||||
|
||||
@ -889,33 +850,7 @@ pub fn clearCells(
|
||||
if (row.styled) {
|
||||
for (cells) |*cell| {
|
||||
if (cell.style_id == style.default_id) continue;
|
||||
|
||||
// Fast-path, the style ID matches, in this case we just update
|
||||
// our own ref and continue. We never delete because our style
|
||||
// is still active.
|
||||
if (page == &self.cursor.page_pin.page.data and
|
||||
cell.style_id == self.cursor.style_id)
|
||||
{
|
||||
self.cursor.style_ref.?.* -= 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Slow path: we need to lookup this style so we can decrement
|
||||
// the ref count. Since we've already loaded everything, we also
|
||||
// just go ahead and GC it if it reaches zero, too.
|
||||
if (page.styles.lookupId(
|
||||
page.memory,
|
||||
cell.style_id,
|
||||
)) |prev_style| {
|
||||
// Below upsert can't fail because it should already be present
|
||||
const md = page.styles.upsert(
|
||||
page.memory,
|
||||
prev_style.*,
|
||||
) catch unreachable;
|
||||
assert(md.ref > 0);
|
||||
md.ref -= 1;
|
||||
if (md.ref == 0) page.styles.remove(page.memory, cell.style_id);
|
||||
}
|
||||
page.styles.release(page.memory, cell.style_id);
|
||||
}
|
||||
|
||||
// If we have no left/right scroll region we can be sure that
|
||||
@ -1044,17 +979,37 @@ pub fn resizeWithoutReflow(
|
||||
}
|
||||
|
||||
/// Resize the screen.
|
||||
// TODO: replace resize and resizeWithoutReflow with this.
|
||||
fn resizeInternal(
|
||||
self: *Screen,
|
||||
cols: size.CellCountInt,
|
||||
rows: size.CellCountInt,
|
||||
reflow: bool,
|
||||
) !void {
|
||||
defer self.assertIntegrity();
|
||||
|
||||
// No matter what we mark our image state as dirty
|
||||
self.kitty_images.dirty = true;
|
||||
|
||||
// Perform the resize operation. This will update cursor by reference.
|
||||
// Release the cursor style while resizing just
|
||||
// in case the cursor ends up on a different page.
|
||||
const cursor_style = self.cursor.style;
|
||||
self.cursor.style = .{};
|
||||
self.manualStyleUpdate() catch unreachable;
|
||||
defer {
|
||||
// Restore the cursor style.
|
||||
self.cursor.style = cursor_style;
|
||||
self.manualStyleUpdate() catch |err| {
|
||||
// This failure should not happen because manualStyleUpdate
|
||||
// handles page splitting, overflow, and more. This should only
|
||||
// happen if we're out of RAM. In this case, we'll just degrade
|
||||
// gracefully back to the default style.
|
||||
log.err("failed to update style on cursor reload err={}", .{err});
|
||||
self.cursor.style = .{};
|
||||
self.cursor.style_id = 0;
|
||||
};
|
||||
}
|
||||
|
||||
// Perform the resize operation.
|
||||
try self.pages.resize(.{
|
||||
.rows = rows,
|
||||
.cols = cols,
|
||||
@ -1072,7 +1027,6 @@ fn resizeInternal(
|
||||
// If our cursor was updated, we do a full reload so all our cursor
|
||||
// state is correct.
|
||||
self.cursorReload();
|
||||
self.assertIntegrity();
|
||||
}
|
||||
|
||||
/// Set a style attribute for the current cursor.
|
||||
@ -1223,21 +1177,14 @@ pub fn manualStyleUpdate(self: *Screen) !void {
|
||||
|
||||
// std.log.warn("active styles={}", .{page.styles.count(page.memory)});
|
||||
|
||||
// Remove our previous style if is unused.
|
||||
if (self.cursor.style_ref) |ref| {
|
||||
if (ref.* == 0) {
|
||||
page.styles.remove(page.memory, self.cursor.style_id);
|
||||
}
|
||||
|
||||
// Reset our ID and ref to null since the ref is now invalid.
|
||||
self.cursor.style_id = 0;
|
||||
self.cursor.style_ref = null;
|
||||
// Release our previous style if it was not default.
|
||||
if (self.cursor.style_id != style.default_id) {
|
||||
page.styles.release(page.memory, self.cursor.style_id);
|
||||
}
|
||||
|
||||
// If our new style is the default, just reset to that
|
||||
if (self.cursor.style.default()) {
|
||||
self.cursor.style_id = 0;
|
||||
self.cursor.style_ref = null;
|
||||
return;
|
||||
}
|
||||
|
||||
@ -1251,42 +1198,29 @@ pub fn manualStyleUpdate(self: *Screen) !void {
|
||||
// if that makes a meaningful difference. Our priority is to keep print
|
||||
// fast because setting a ton of styles that do nothing is uncommon
|
||||
// and weird.
|
||||
const md = page.styles.upsert(
|
||||
const id = page.styles.add(
|
||||
page.memory,
|
||||
self.cursor.style,
|
||||
) catch |err| md: {
|
||||
switch (err) {
|
||||
// Our style map is full. Let's allocate a new page by doubling
|
||||
// the size and then try again.
|
||||
error.OutOfMemory => {
|
||||
const node = try self.pages.adjustCapacity(
|
||||
self.cursor.page_pin.page,
|
||||
.{ .styles = page.capacity.styles * 2 },
|
||||
);
|
||||
) catch id: {
|
||||
// Our style map is full. Let's allocate a new
|
||||
// page by doubling the size and then try again.
|
||||
const node = try self.pages.adjustCapacity(
|
||||
self.cursor.page_pin.page,
|
||||
.{ .styles = page.capacity.styles * 2 },
|
||||
);
|
||||
|
||||
page = &node.data;
|
||||
},
|
||||
|
||||
// We've run out of style IDs. This is fixed by doing a page
|
||||
// compaction.
|
||||
error.Overflow => {
|
||||
const node = try self.pages.compact(
|
||||
self.cursor.page_pin.page,
|
||||
);
|
||||
page = &node.data;
|
||||
},
|
||||
}
|
||||
page = &node.data;
|
||||
|
||||
// Since this modifies our cursor page, we need to reload
|
||||
cursor_reload = true;
|
||||
|
||||
break :md try page.styles.upsert(
|
||||
break :id try page.styles.add(
|
||||
page.memory,
|
||||
self.cursor.style,
|
||||
);
|
||||
};
|
||||
self.cursor.style_id = md.id;
|
||||
self.cursor.style_ref = &md.ref;
|
||||
self.cursor.style_id = id;
|
||||
|
||||
if (cursor_reload) self.cursorReload();
|
||||
self.assertIntegrity();
|
||||
}
|
||||
@ -2271,8 +2205,9 @@ pub fn testWriteString(self: *Screen, text: []const u8) !void {
|
||||
};
|
||||
|
||||
// If we have a ref-counted style, increase.
|
||||
if (self.cursor.style_ref) |ref| {
|
||||
ref.* += 1;
|
||||
if (self.cursor.style_id != style.default_id) {
|
||||
const page = self.cursor.page_pin.page.data;
|
||||
page.styles.use(page.memory, self.cursor.style_id);
|
||||
self.cursor.page_row.styled = true;
|
||||
}
|
||||
},
|
||||
@ -2310,6 +2245,14 @@ pub fn testWriteString(self: *Screen, text: []const u8) !void {
|
||||
.wide = .spacer_tail,
|
||||
.protected = self.cursor.protected,
|
||||
};
|
||||
|
||||
// If we have a ref-counted style, increase twice.
|
||||
if (self.cursor.style_id != style.default_id) {
|
||||
const page = self.cursor.page_pin.page.data;
|
||||
page.styles.use(page.memory, self.cursor.style_id);
|
||||
page.styles.use(page.memory, self.cursor.style_id);
|
||||
self.cursor.page_row.styled = true;
|
||||
}
|
||||
},
|
||||
|
||||
else => unreachable,
|
||||
@ -2792,10 +2735,10 @@ test "Screen: cursorDown across pages preserves style" {
|
||||
try s.setAttribute(.{ .bold = {} });
|
||||
{
|
||||
const page = &s.cursor.page_pin.page.data;
|
||||
const styleval = page.styles.lookupId(
|
||||
const styleval = page.styles.get(
|
||||
page.memory,
|
||||
s.cursor.style_id,
|
||||
).?;
|
||||
);
|
||||
try testing.expect(styleval.flags.bold);
|
||||
}
|
||||
|
||||
@ -2803,10 +2746,10 @@ test "Screen: cursorDown across pages preserves style" {
|
||||
s.cursorDown(1);
|
||||
{
|
||||
const page = &s.cursor.page_pin.page.data;
|
||||
const styleval = page.styles.lookupId(
|
||||
const styleval = page.styles.get(
|
||||
page.memory,
|
||||
s.cursor.style_id,
|
||||
).?;
|
||||
);
|
||||
try testing.expect(styleval.flags.bold);
|
||||
}
|
||||
}
|
||||
@ -2835,10 +2778,10 @@ test "Screen: cursorUp across pages preserves style" {
|
||||
try s.setAttribute(.{ .bold = {} });
|
||||
{
|
||||
const page = &s.cursor.page_pin.page.data;
|
||||
const styleval = page.styles.lookupId(
|
||||
const styleval = page.styles.get(
|
||||
page.memory,
|
||||
s.cursor.style_id,
|
||||
).?;
|
||||
);
|
||||
try testing.expect(styleval.flags.bold);
|
||||
}
|
||||
|
||||
@ -2848,10 +2791,10 @@ test "Screen: cursorUp across pages preserves style" {
|
||||
const page = &s.cursor.page_pin.page.data;
|
||||
try testing.expect(start_page == page);
|
||||
|
||||
const styleval = page.styles.lookupId(
|
||||
const styleval = page.styles.get(
|
||||
page.memory,
|
||||
s.cursor.style_id,
|
||||
).?;
|
||||
);
|
||||
try testing.expect(styleval.flags.bold);
|
||||
}
|
||||
}
|
||||
@ -2880,10 +2823,10 @@ test "Screen: cursorAbsolute across pages preserves style" {
|
||||
try s.setAttribute(.{ .bold = {} });
|
||||
{
|
||||
const page = &s.cursor.page_pin.page.data;
|
||||
const styleval = page.styles.lookupId(
|
||||
const styleval = page.styles.get(
|
||||
page.memory,
|
||||
s.cursor.style_id,
|
||||
).?;
|
||||
);
|
||||
try testing.expect(styleval.flags.bold);
|
||||
}
|
||||
|
||||
@ -2893,10 +2836,10 @@ test "Screen: cursorAbsolute across pages preserves style" {
|
||||
const page = &s.cursor.page_pin.page.data;
|
||||
try testing.expect(start_page == page);
|
||||
|
||||
const styleval = page.styles.lookupId(
|
||||
const styleval = page.styles.get(
|
||||
page.memory,
|
||||
s.cursor.style_id,
|
||||
).?;
|
||||
);
|
||||
try testing.expect(styleval.flags.bold);
|
||||
}
|
||||
}
|
||||
@ -3013,10 +2956,10 @@ test "Screen: scrolling across pages preserves style" {
|
||||
const page = &s.pages.pages.last.?.data;
|
||||
try testing.expect(start_page != page);
|
||||
|
||||
const styleval = page.styles.lookupId(
|
||||
const styleval = page.styles.get(
|
||||
page.memory,
|
||||
s.cursor.style_id,
|
||||
).?;
|
||||
);
|
||||
try testing.expect(styleval.flags.bold);
|
||||
}
|
||||
|
||||
|
@ -600,8 +600,19 @@ fn printCell(
|
||||
);
|
||||
}
|
||||
|
||||
// Keep track of the previous style so we can decrement the ref count
|
||||
const prev_style_id = cell.style_id;
|
||||
// We don't need to update the style refs unless the
|
||||
// cell's new style will be different after writing.
|
||||
const style_changed = cell.style_id != self.screen.cursor.style_id;
|
||||
|
||||
if (style_changed) {
|
||||
var page = &self.screen.cursor.page_pin.page.data;
|
||||
|
||||
// Release the old style.
|
||||
if (cell.style_id != style.default_id) {
|
||||
assert(self.screen.cursor.page_row.styled);
|
||||
page.styles.release(page.memory, cell.style_id);
|
||||
}
|
||||
}
|
||||
|
||||
// Write
|
||||
cell.* = .{
|
||||
@ -612,50 +623,12 @@ fn printCell(
|
||||
.protected = self.screen.cursor.protected,
|
||||
};
|
||||
|
||||
if (comptime std.debug.runtime_safety) {
|
||||
// We've had bugs around this, so let's add an assertion: every
|
||||
// style we use should be present in the style table.
|
||||
if (self.screen.cursor.style_id != style.default_id) {
|
||||
const page = &self.screen.cursor.page_pin.page.data;
|
||||
if (page.styles.lookupId(
|
||||
page.memory,
|
||||
self.screen.cursor.style_id,
|
||||
) == null) {
|
||||
log.err("can't find style page={X} id={}", .{
|
||||
@intFromPtr(&self.screen.cursor.page_pin.page.data),
|
||||
self.screen.cursor.style_id,
|
||||
});
|
||||
@panic("style not found");
|
||||
}
|
||||
}
|
||||
}
|
||||
if (style_changed) {
|
||||
var page = &self.screen.cursor.page_pin.page.data;
|
||||
|
||||
// Handle the style ref count handling
|
||||
style_ref: {
|
||||
if (prev_style_id != style.default_id) {
|
||||
const row = self.screen.cursor.page_row;
|
||||
assert(row.styled);
|
||||
|
||||
// If our previous cell had the same style ID as us currently,
|
||||
// then we don't bother with any ref counts because we're the same.
|
||||
if (prev_style_id == self.screen.cursor.style_id) break :style_ref;
|
||||
|
||||
// Slow path: we need to lookup this style so we can decrement
|
||||
// the ref count. Since we've already loaded everything, we also
|
||||
// just go ahead and GC it if it reaches zero, too.
|
||||
var page = &self.screen.cursor.page_pin.page.data;
|
||||
if (page.styles.lookupId(page.memory, prev_style_id)) |prev_style| {
|
||||
// Below upsert can't fail because it should already be present
|
||||
const md = page.styles.upsert(page.memory, prev_style.*) catch unreachable;
|
||||
assert(md.ref > 0);
|
||||
md.ref -= 1;
|
||||
if (md.ref == 0) page.styles.remove(page.memory, prev_style_id);
|
||||
}
|
||||
}
|
||||
|
||||
// If we have a ref-counted style, increase.
|
||||
if (self.screen.cursor.style_ref) |ref| {
|
||||
ref.* += 1;
|
||||
// Use the new style.
|
||||
if (cell.style_id != style.default_id) {
|
||||
page.styles.use(page.memory, cell.style_id);
|
||||
self.screen.cursor.page_row.styled = true;
|
||||
}
|
||||
}
|
||||
@ -2190,8 +2163,12 @@ pub fn decaln(self: *Terminal) !void {
|
||||
});
|
||||
|
||||
// If we have a ref-counted style, increase
|
||||
if (self.screen.cursor.style_ref) |ref| {
|
||||
ref.* += @intCast(cells.len);
|
||||
if (self.screen.cursor.style_id != style.default_id) {
|
||||
page.styles.useMultiple(
|
||||
page.memory,
|
||||
self.screen.cursor.style_id,
|
||||
@intCast(cells.len),
|
||||
);
|
||||
row.styled = true;
|
||||
}
|
||||
|
||||
@ -7066,7 +7043,8 @@ test "Terminal: bold style" {
|
||||
const cell = list_cell.cell;
|
||||
try testing.expectEqual(@as(u21, 'A'), cell.content.codepoint);
|
||||
try testing.expect(cell.style_id != 0);
|
||||
try testing.expect(t.screen.cursor.style_ref.?.* > 0);
|
||||
const page = t.screen.cursor.page_pin.page.data;
|
||||
try testing.expect(page.styles.refCount(page.memory, t.screen.cursor.style_id) > 1);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -197,6 +197,7 @@ pub const Page = struct {
|
||||
.styles = style.Set.init(
|
||||
buf.add(l.styles_start),
|
||||
l.styles_layout,
|
||||
style.StyleSetContext{},
|
||||
),
|
||||
.grapheme_alloc = GraphemeAlloc.init(
|
||||
buf.add(l.grapheme_alloc_start),
|
||||
@ -324,17 +325,11 @@ pub const Page = struct {
|
||||
|
||||
if (cell.style_id != style.default_id) {
|
||||
// If a cell has a style, it must be present in the styles
|
||||
// set.
|
||||
_ = self.styles.lookupId(
|
||||
// set. Accessing it with `get` asserts that.
|
||||
_ = self.styles.get(
|
||||
self.memory,
|
||||
cell.style_id,
|
||||
) orelse {
|
||||
log.warn(
|
||||
"page integrity violation y={} x={} style missing id={}",
|
||||
.{ y, x, cell.style_id },
|
||||
);
|
||||
return IntegrityError.MissingStyle;
|
||||
};
|
||||
);
|
||||
|
||||
if (!row.styled) {
|
||||
log.warn(
|
||||
@ -424,12 +419,11 @@ pub const Page = struct {
|
||||
{
|
||||
var it = styles_seen.iterator();
|
||||
while (it.next()) |entry| {
|
||||
const style_val = self.styles.lookupId(self.memory, entry.key_ptr.*).?.*;
|
||||
const md = self.styles.upsert(self.memory, style_val) catch unreachable;
|
||||
if (md.ref < entry.value_ptr.*) {
|
||||
const ref_count = self.styles.refCount(self.memory, entry.key_ptr.*);
|
||||
if (ref_count < entry.value_ptr.*) {
|
||||
log.warn(
|
||||
"page integrity violation style ref count mismatch id={} expected={} actual={}",
|
||||
.{ entry.key_ptr.*, entry.value_ptr.*, md.ref },
|
||||
.{ entry.key_ptr.*, entry.value_ptr.*, ref_count },
|
||||
);
|
||||
return IntegrityError.MismatchedStyleRef;
|
||||
}
|
||||
@ -474,7 +468,7 @@ pub const Page = struct {
|
||||
return result;
|
||||
}
|
||||
|
||||
pub const CloneFromError = Allocator.Error || style.Set.UpsertError;
|
||||
pub const CloneFromError = Allocator.Error || error{OutOfMemory};
|
||||
|
||||
/// Clone the contents of another page into this page. The capacities
|
||||
/// can be different, but the size of the other page must fit into
|
||||
@ -586,11 +580,22 @@ pub const Page = struct {
|
||||
for (cps) |cp| try self.appendGrapheme(dst_row, dst_cell, cp);
|
||||
}
|
||||
if (src_cell.style_id != style.default_id) {
|
||||
const other_style = other.styles.lookupId(other.memory, src_cell.style_id).?.*;
|
||||
const md = try self.styles.upsert(self.memory, other_style);
|
||||
md.ref += 1;
|
||||
dst_cell.style_id = md.id;
|
||||
dst_row.styled = true;
|
||||
|
||||
if (other == self) {
|
||||
// If it's the same page we don't have to worry about
|
||||
// copying the style, we can use the style ID directly.
|
||||
dst_cell.style_id = src_cell.style_id;
|
||||
self.styles.use(self.memory, dst_cell.style_id);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Slow path: Get the style from the other
|
||||
// page and add it to this page's style set.
|
||||
const other_style = other.styles.get(other.memory, src_cell.style_id).*;
|
||||
if (try self.styles.addWithId(self.memory, other_style, src_cell.style_id)) |id| {
|
||||
dst_cell.style_id = id;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -767,13 +772,7 @@ pub const Page = struct {
|
||||
for (cells) |*cell| {
|
||||
if (cell.style_id == style.default_id) continue;
|
||||
|
||||
if (self.styles.lookupId(self.memory, cell.style_id)) |prev_style| {
|
||||
// Below upsert can't fail because it should already be present
|
||||
const md = self.styles.upsert(self.memory, prev_style.*) catch unreachable;
|
||||
assert(md.ref > 0);
|
||||
md.ref -= 1;
|
||||
if (md.ref == 0) self.styles.remove(self.memory, cell.style_id);
|
||||
}
|
||||
self.styles.release(self.memory, cell.style_id);
|
||||
}
|
||||
|
||||
if (cells.len == self.size.cols) row.styled = false;
|
||||
@ -2112,7 +2111,7 @@ test "Page verifyIntegrity styles good" {
|
||||
defer page.deinit();
|
||||
|
||||
// Upsert a style we'll use
|
||||
const md = try page.styles.upsert(page.memory, .{ .flags = .{
|
||||
const id = try page.styles.add(page.memory, .{ .flags = .{
|
||||
.bold = true,
|
||||
} });
|
||||
|
||||
@ -2123,11 +2122,15 @@ test "Page verifyIntegrity styles good" {
|
||||
rac.cell.* = .{
|
||||
.content_tag = .codepoint,
|
||||
.content = .{ .codepoint = @intCast(x + 1) },
|
||||
.style_id = md.id,
|
||||
.style_id = id,
|
||||
};
|
||||
md.ref += 1;
|
||||
page.styles.use(page.memory, id);
|
||||
}
|
||||
|
||||
// The original style add would have incremented the
|
||||
// ref count too, so release it to balance that out.
|
||||
page.styles.release(page.memory, id);
|
||||
|
||||
try page.verifyIntegrity(testing.allocator);
|
||||
}
|
||||
|
||||
@ -2140,7 +2143,7 @@ test "Page verifyIntegrity styles ref count mismatch" {
|
||||
defer page.deinit();
|
||||
|
||||
// Upsert a style we'll use
|
||||
const md = try page.styles.upsert(page.memory, .{ .flags = .{
|
||||
const id = try page.styles.add(page.memory, .{ .flags = .{
|
||||
.bold = true,
|
||||
} });
|
||||
|
||||
@ -2151,13 +2154,17 @@ test "Page verifyIntegrity styles ref count mismatch" {
|
||||
rac.cell.* = .{
|
||||
.content_tag = .codepoint,
|
||||
.content = .{ .codepoint = @intCast(x + 1) },
|
||||
.style_id = md.id,
|
||||
.style_id = id,
|
||||
};
|
||||
md.ref += 1;
|
||||
page.styles.use(page.memory, id);
|
||||
}
|
||||
|
||||
// The original style add would have incremented the
|
||||
// ref count too, so release it to balance that out.
|
||||
page.styles.release(page.memory, id);
|
||||
|
||||
// Miss a ref
|
||||
md.ref -= 1;
|
||||
page.styles.release(page.memory, id);
|
||||
|
||||
try testing.expectError(
|
||||
Page.IntegrityError.MismatchedStyleRef,
|
||||
|
573
src/terminal/ref_counted_set.zig
Normal file
573
src/terminal/ref_counted_set.zig
Normal file
@ -0,0 +1,573 @@
|
||||
const size = @import("size.zig");
|
||||
const Offset = size.Offset;
|
||||
const OffsetBuf = size.OffsetBuf;
|
||||
|
||||
const fastmem = @import("../fastmem.zig");
|
||||
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
|
||||
/// A reference counted set.
|
||||
///
|
||||
/// This set is created with some capacity in mind. You can determine
|
||||
/// the exact memory requirement of a given capacity by calling `layout`
|
||||
/// and checking the total size.
|
||||
///
|
||||
/// When the set exceeds capacity, `error.OutOfMemory` is returned from
|
||||
/// any memory-using methods. The caller is responsible for determining
|
||||
/// a path forward.
|
||||
///
|
||||
/// This set is reference counted. Each item in the set has an associated
|
||||
/// reference count. The caller is responsible for calling release for an
|
||||
/// item when it is no longer being used. Items with 0 references will be
|
||||
/// kept until another item is written to their bucket. This allows items
|
||||
/// to be ressurected if they are re-added before they get overwritten.
|
||||
///
|
||||
/// The backing data structure of this set is an open addressed hash table
|
||||
/// with linear probing and Robin Hood hashing, and a flat array of items.
|
||||
///
|
||||
/// The table maps values to item IDs, which are indices in the item array
|
||||
/// which contain the item's value and its reference count. Item IDs can be
|
||||
/// used to efficiently access an item and update its reference count after
|
||||
/// it has been added to the table, to avoid having to use the hash map to
|
||||
/// look the value back up.
|
||||
///
|
||||
/// ID 0 is reserved and will never be assigned.
|
||||
///
|
||||
/// Parameters:
|
||||
///
|
||||
/// `Context`
|
||||
/// A type containing methods to define behaviors.
|
||||
/// - `fn hash(*Context, T) u64` - Return a hash for an item.
|
||||
/// - `fn eql(*Context, T, T) bool` - Check two items for equality.
|
||||
///
|
||||
/// - `fn deleted(*Context, T) void` - [OPTIONAL] Deletion callback.
|
||||
/// If present, called whenever an item is finally deleted.
|
||||
/// Useful if the item has memory that needs to be freed.
|
||||
///
|
||||
pub fn RefCountedSet(
|
||||
comptime T: type,
|
||||
comptime Id: type,
|
||||
comptime RefCountInt: type,
|
||||
comptime Context: type,
|
||||
) type {
|
||||
return struct {
|
||||
const Self = RefCountedSet(T, Id, RefCountInt, Context);
|
||||
|
||||
pub const base_align = @max(
|
||||
@alignOf(Context),
|
||||
@alignOf(Layout),
|
||||
@alignOf(Item),
|
||||
@alignOf(Id),
|
||||
);
|
||||
|
||||
/// Set item
|
||||
pub const Item = struct {
|
||||
/// The value this item represents.
|
||||
value: T = undefined,
|
||||
/// Metadata for this item.
|
||||
meta: Metadata = .{},
|
||||
|
||||
pub const Metadata = struct {
|
||||
/// The bucket in the hash table where this item
|
||||
/// is referenced.
|
||||
bucket: Id = std.math.maxInt(Id),
|
||||
|
||||
/// The length of the probe sequence between this
|
||||
/// item's starting bucket and the bucket it's in,
|
||||
/// used for Robin Hood hashing.
|
||||
psl: Id = 0,
|
||||
|
||||
/// The reference count for this item.
|
||||
ref: RefCountInt = 0,
|
||||
};
|
||||
};
|
||||
|
||||
/// A hash table of item indices
|
||||
table: Offset(Id),
|
||||
|
||||
/// By keeping track of the max probe sequence length
|
||||
/// we can bail out early when looking up values that
|
||||
/// aren't present.
|
||||
max_psl: Id = 0,
|
||||
|
||||
/// We keep track of how many items have a PSL of any
|
||||
/// given length, so that we can shrink max_psl when
|
||||
/// we delete items.
|
||||
///
|
||||
/// A probe sequence of length 32 or more is astronomically
|
||||
/// unlikely. Roughly a (1/table_cap)^32 -- with any normal
|
||||
/// table capacity that is so unlikely that it's not worth
|
||||
/// handling.
|
||||
psl_stats: [32]Id = [_]Id{0} ** 32,
|
||||
|
||||
/// The backing store of items
|
||||
items: Offset(Item),
|
||||
|
||||
/// The next index to store an item at.
|
||||
/// Id 0 is reserved for unused items.
|
||||
next_id: Id = 1,
|
||||
|
||||
layout: Layout,
|
||||
|
||||
/// An instance of the context structure.
|
||||
context: Context,
|
||||
|
||||
/// Returns the memory layout for the given base offset and
|
||||
/// desired capacity. The layout can be used by the caller to
|
||||
/// determine how much memory to allocate, and the layout must
|
||||
/// be used to initialize the set so that the set knows all
|
||||
/// the offsets for the various buffers.
|
||||
///
|
||||
/// The capacity passed for cap will be used for the hash table,
|
||||
/// which has a load factor of `0.8125` (13/16), so the number of
|
||||
/// items which can actually be stored in the set will be smaller.
|
||||
///
|
||||
/// The laid out capacity will be at least `cap`, but may be higher,
|
||||
/// since it is rounded up to the next power of 2 for efficiency.
|
||||
///
|
||||
/// 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.
|
||||
pub fn layout(cap: Id) Layout {
|
||||
// Experimentally, this load factor works quite well.
|
||||
const load_factor = 0.8125;
|
||||
|
||||
const table_cap: Id = std.math.ceilPowerOfTwoAssert(Id, cap);
|
||||
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_start = 0;
|
||||
const table_end = table_start + table_cap * @sizeOf(Id);
|
||||
|
||||
const items_start = std.mem.alignForward(usize, table_end, @alignOf(Item));
|
||||
const items_end = items_start + items_cap * @sizeOf(Item);
|
||||
|
||||
const total_size = items_end;
|
||||
|
||||
return .{
|
||||
.cap = items_cap,
|
||||
.table_cap = table_cap,
|
||||
.table_mask = table_mask,
|
||||
.table_start = table_start,
|
||||
.items_start = items_start,
|
||||
.total_size = total_size,
|
||||
};
|
||||
}
|
||||
|
||||
pub const Layout = struct {
|
||||
cap: Id,
|
||||
table_cap: Id,
|
||||
table_mask: Id,
|
||||
table_start: usize,
|
||||
items_start: usize,
|
||||
total_size: usize,
|
||||
};
|
||||
|
||||
pub fn init(base: OffsetBuf, l: Layout, context: Context) Self {
|
||||
const table = base.member(Id, l.table_start);
|
||||
const items = base.member(Item, l.items_start);
|
||||
|
||||
@memset(table.ptr(base)[0..l.table_cap], 0);
|
||||
@memset(items.ptr(base)[0..l.cap], .{});
|
||||
|
||||
return .{
|
||||
.table = table,
|
||||
.items = items,
|
||||
.layout = l,
|
||||
.context = context,
|
||||
};
|
||||
}
|
||||
|
||||
/// Add an item to the set if not present
|
||||
/// and increment its reference count.
|
||||
///
|
||||
/// Returns the item's ID.
|
||||
///
|
||||
/// If the set has no more room, then an
|
||||
/// OutOfMemory error is returned instead.
|
||||
pub fn add(self: *Self, base: anytype, value: T) error{OutOfMemory}!Id {
|
||||
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);
|
||||
}
|
||||
|
||||
// If we still don't have an available ID, we're out of memory.
|
||||
if (self.next_id >= self.layout.cap) {
|
||||
return error.OutOfMemory;
|
||||
}
|
||||
|
||||
const id = self.upsert(base, value, self.next_id);
|
||||
items[id].meta.ref += 1;
|
||||
|
||||
if (id == self.next_id) {
|
||||
self.next_id += 1;
|
||||
}
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
/// Add an item to the set if not present
|
||||
/// and increment its reference count.
|
||||
/// If possible, use the provided ID.
|
||||
///
|
||||
/// 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 instead.
|
||||
pub fn addWithId(self: *Self, base: anytype, value: T, id: Id) error{OutOfMemory}!?Id {
|
||||
const items = self.items.ptr(base);
|
||||
|
||||
if (id < self.next_id) {
|
||||
if (items[id].meta.ref == 0) {
|
||||
self.deleteItem(base, id);
|
||||
|
||||
const added_id = self.upsert(base, value, id);
|
||||
|
||||
items[added_id].meta.ref += 1;
|
||||
|
||||
return if (added_id == id) null else added_id;
|
||||
} else if (self.context.eql(value, items[id].value)) {
|
||||
items[id].meta.ref += 1;
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return try self.add(base, value);
|
||||
}
|
||||
|
||||
/// Increment an item's reference count by 1.
|
||||
///
|
||||
/// Asserts that the item's reference count is greater than 0.
|
||||
pub fn use(self: *const Self, base: anytype, id: Id) void {
|
||||
assert(id > 0);
|
||||
assert(id < self.layout.cap);
|
||||
|
||||
const items = self.items.ptr(base);
|
||||
const item = &items[id];
|
||||
|
||||
// If `use` is being called on an item with 0 references, then
|
||||
// either someone forgot to call it before, released too early
|
||||
// or lied about releasing. In any case something is wrong and
|
||||
// shouldn't be allowed.
|
||||
assert(item.meta.ref > 0);
|
||||
|
||||
item.meta.ref += 1;
|
||||
}
|
||||
|
||||
/// Increment an item's reference count by a specified number.
|
||||
///
|
||||
/// Asserts that the item's reference count is greater than 0.
|
||||
pub fn useMultiple(self: *const Self, base: anytype, id: Id, n: RefCountInt) void {
|
||||
assert(id > 0);
|
||||
assert(id < self.layout.cap);
|
||||
|
||||
const items = self.items.ptr(base);
|
||||
const item = &items[id];
|
||||
|
||||
// If `use` is being called on an item with 0 references, then
|
||||
// either someone forgot to call it before, released too early
|
||||
// or lied about releasing. In any case something is wrong and
|
||||
// shouldn't be allowed.
|
||||
assert(item.meta.ref > 0);
|
||||
|
||||
item.meta.ref += n;
|
||||
}
|
||||
|
||||
/// Get an item by its ID without incrementing its reference count.
|
||||
///
|
||||
/// Asserts that the item's reference count is greater than 0.
|
||||
pub fn get(self: *const Self, base: anytype, id: Id) *T {
|
||||
assert(id > 0);
|
||||
assert(id < self.layout.cap);
|
||||
|
||||
const items = self.items.ptr(base);
|
||||
const item = &items[id];
|
||||
|
||||
assert(item.meta.ref > 0);
|
||||
|
||||
return @ptrCast(&item.value);
|
||||
}
|
||||
|
||||
/// Releases a reference to an item by its ID.
|
||||
///
|
||||
/// Asserts that the item's reference count is greater than 0.
|
||||
pub fn release(self: *Self, base: anytype, id: Id) void {
|
||||
assert(id > 0);
|
||||
assert(id < self.layout.cap);
|
||||
|
||||
const items = self.items.ptr(base);
|
||||
const item = &items[id];
|
||||
|
||||
assert(item.meta.ref > 0);
|
||||
item.meta.ref -= 1;
|
||||
}
|
||||
|
||||
/// Release a specified number of references to an item by its ID.
|
||||
///
|
||||
/// Asserts that the item's reference count is at least `n`.
|
||||
pub fn releaseMultiple(self: *Self, base: anytype, id: Id, n: Id) void {
|
||||
assert(id > 0);
|
||||
assert(id < self.layout.cap);
|
||||
|
||||
const items = self.items.ptr(base);
|
||||
const item = &items[id];
|
||||
|
||||
assert(item.meta.ref >= n);
|
||||
item.meta.ref -= n;
|
||||
}
|
||||
|
||||
/// Get the ref count for an item by its ID.
|
||||
pub fn refCount(self: *const Self, base: anytype, id: Id) RefCountInt {
|
||||
assert(id > 0);
|
||||
assert(id < self.layout.cap);
|
||||
|
||||
const items = self.items.ptr(base);
|
||||
const item = &items[id];
|
||||
return item.meta.ref;
|
||||
}
|
||||
|
||||
/// Get the current number of non-dead items in the set.
|
||||
///
|
||||
/// NOT DESIGNED TO BE USED OUTSIDE OF TESTING, this is a very slow
|
||||
/// 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;
|
||||
}
|
||||
|
||||
//================================================//
|
||||
// The functions below are all internal functions //
|
||||
// for performing operations on the hash table. //
|
||||
//================================================//
|
||||
|
||||
/// Delete an item, removing any references from
|
||||
/// the table, and freeing its ID to be re-used.
|
||||
fn deleteItem(self: *Self, base: anytype, id: Id) void {
|
||||
const table = self.table.ptr(base);
|
||||
const items = self.items.ptr(base);
|
||||
|
||||
const item = items[id];
|
||||
|
||||
if (item.meta.bucket > self.layout.table_cap) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (table[item.meta.bucket] != id) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (comptime @hasDecl(Context, "deleted")) {
|
||||
// Inform the context struct that we're
|
||||
// deleting the dead item's value for good.
|
||||
self.context.deleted(item.value);
|
||||
}
|
||||
|
||||
self.psl_stats[item.meta.psl] -= 1;
|
||||
table[item.meta.bucket] = 0;
|
||||
items[id] = .{};
|
||||
|
||||
var p: Id = item.meta.bucket;
|
||||
var n: Id = (p + 1) & self.layout.table_mask;
|
||||
|
||||
while (table[n] != 0 and items[table[n]].meta.psl > 0) {
|
||||
items[table[n]].meta.bucket = p;
|
||||
self.psl_stats[items[table[n]].meta.psl] -= 1;
|
||||
items[table[n]].meta.psl -= 1;
|
||||
self.psl_stats[items[table[n]].meta.psl] += 1;
|
||||
table[p] = table[n];
|
||||
p = n;
|
||||
n = (n + 1) & self.layout.table_mask;
|
||||
}
|
||||
|
||||
while (self.max_psl > 0 and self.psl_stats[self.max_psl] == 0) {
|
||||
self.max_psl -= 1;
|
||||
}
|
||||
|
||||
table[p] = 0;
|
||||
}
|
||||
|
||||
/// Find an item in the table and return its ID.
|
||||
/// If the item does not exist in the table, null is returned.
|
||||
fn lookup(self: *Self, base: anytype, value: T) ?Id {
|
||||
const table = self.table.ptr(base);
|
||||
const items = self.items.ptr(base);
|
||||
|
||||
const hash: u64 = self.context.hash(value);
|
||||
|
||||
for (0..self.max_psl + 1) |i| {
|
||||
const p = (hash + i) & self.layout.table_mask;
|
||||
|
||||
const id = table[p];
|
||||
|
||||
// Empty bucket, our item cannot have probed to
|
||||
// any point after this, meaning it's not present.
|
||||
if (id == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const item = items[id];
|
||||
|
||||
// An item with a shorter probe sequence length would never
|
||||
// end up in the middle of another sequence, since it would
|
||||
// be swapped out if inserted before the new sequence, and
|
||||
// would not be swapped in if inserted afterwards.
|
||||
//
|
||||
// As such, our item cannot be present.
|
||||
if (item.meta.psl < i) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// We don't bother checking dead items.
|
||||
if (item.meta.ref == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If the item is a part of the same probe sequence,
|
||||
// we check if it matches the value we're looking for.
|
||||
if (item.meta.psl == i and
|
||||
self.context.eql(value, item.value))
|
||||
{
|
||||
return id;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Find the provided value in the hash table, or add a new item
|
||||
/// for it if not present. If a new item is added, `new_id` will
|
||||
/// be used as the ID.
|
||||
fn upsert(self: *Self, base: anytype, value: T, new_id: Id) Id {
|
||||
// If the item already exists, return it.
|
||||
if (self.lookup(base, value)) |id| {
|
||||
return id;
|
||||
}
|
||||
|
||||
const table = self.table.ptr(base);
|
||||
const items = self.items.ptr(base);
|
||||
|
||||
// The new item that we'll put in to the table.
|
||||
var new_item: Item = .{
|
||||
.value = value,
|
||||
.meta = .{
|
||||
.psl = 0,
|
||||
.ref = 0,
|
||||
},
|
||||
};
|
||||
|
||||
const hash: u64 = self.context.hash(value);
|
||||
|
||||
var held_id: Id = new_id;
|
||||
var held_item: *Item = &new_item;
|
||||
|
||||
var chosen_p: ?Id = null;
|
||||
var chosen_id: Id = new_id;
|
||||
|
||||
for (0..self.layout.table_cap - 1) |i| {
|
||||
const p: Id = @intCast((hash + i) & self.layout.table_mask);
|
||||
const id = table[p];
|
||||
|
||||
// Empty bucket, put our held item in to it and break.
|
||||
if (id == 0) {
|
||||
table[p] = held_id;
|
||||
held_item.meta.bucket = p;
|
||||
self.psl_stats[held_item.meta.psl] += 1;
|
||||
self.max_psl = @max(self.max_psl, held_item.meta.psl);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
const item = &items[id];
|
||||
|
||||
// If there's a dead item then we resurrect it
|
||||
// for our value so that we can re-use its ID.
|
||||
if (item.meta.ref == 0) {
|
||||
if (comptime @hasDecl(Context, "deleted")) {
|
||||
// Inform the context struct that we're
|
||||
// deleting the dead item's value for good.
|
||||
self.context.deleted(item.value);
|
||||
}
|
||||
|
||||
chosen_id = id;
|
||||
|
||||
held_item.meta.bucket = p;
|
||||
self.psl_stats[held_item.meta.psl] += 1;
|
||||
self.max_psl = @max(self.max_psl, held_item.meta.psl);
|
||||
|
||||
// If we're not still holding our new item then we
|
||||
// need to make sure that we put the re-used ID in
|
||||
// the right place, where we previously put new_id.
|
||||
if (chosen_p) |c| {
|
||||
table[c] = id;
|
||||
table[p] = held_id;
|
||||
} else {
|
||||
// If we're still holding our new item then we
|
||||
// don't actually have to do anything, because
|
||||
// the table already has the correct ID here.
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
// This item has a lower PSL, swap it out with our held item.
|
||||
if (item.meta.psl < held_item.meta.psl) {
|
||||
if (held_id == new_id) {
|
||||
chosen_p = p;
|
||||
new_item.meta.bucket = p;
|
||||
}
|
||||
|
||||
table[p] = held_id;
|
||||
items[held_id].meta.bucket = p;
|
||||
self.psl_stats[held_item.meta.psl] += 1;
|
||||
self.max_psl = @max(self.max_psl, held_item.meta.psl);
|
||||
|
||||
held_id = id;
|
||||
held_item = item;
|
||||
self.psl_stats[item.meta.psl] -= 1;
|
||||
}
|
||||
|
||||
// Advance to the next probe position for our held item.
|
||||
held_item.meta.psl += 1;
|
||||
}
|
||||
|
||||
items[chosen_id] = new_item;
|
||||
return chosen_id;
|
||||
}
|
||||
};
|
||||
}
|
@ -6,8 +6,11 @@ const page = @import("page.zig");
|
||||
const size = @import("size.zig");
|
||||
const Offset = size.Offset;
|
||||
const OffsetBuf = size.OffsetBuf;
|
||||
const hash_map = @import("hash_map.zig");
|
||||
const AutoOffsetHashMap = hash_map.AutoOffsetHashMap;
|
||||
|
||||
const Wyhash = std.hash.Wyhash;
|
||||
const autoHash = std.hash.autoHash;
|
||||
|
||||
const RefCountedSet = @import("ref_counted_set.zig").RefCountedSet;
|
||||
|
||||
/// The unique identifier for a style. This is at most the number of cells
|
||||
/// that can fit into a terminal page.
|
||||
@ -43,11 +46,34 @@ pub const Style = struct {
|
||||
none: void,
|
||||
palette: u8,
|
||||
rgb: color.RGB,
|
||||
|
||||
/// Formatting to make debug logs easier to read
|
||||
/// by only including non-default attributes.
|
||||
pub fn format(
|
||||
self: Color,
|
||||
comptime fmt: []const u8,
|
||||
options: std.fmt.FormatOptions,
|
||||
writer: anytype,
|
||||
) !void {
|
||||
_ = fmt;
|
||||
_ = options;
|
||||
switch (self) {
|
||||
.none => {
|
||||
_ = try writer.write("Color.none");
|
||||
},
|
||||
.palette => |p| {
|
||||
_ = try writer.print("Color.palette{{ {} }}", .{ p });
|
||||
},
|
||||
.rgb => |rgb| {
|
||||
_ = try writer.print("Color.rgb{{ {}, {}, {} }}", .{ rgb.r, rgb.g, rgb.b });
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/// True if the style is the default style.
|
||||
pub fn default(self: Style) bool {
|
||||
return std.meta.eql(self, .{});
|
||||
return self.eql(.{});
|
||||
}
|
||||
|
||||
/// True if the style is equal to another style.
|
||||
@ -133,6 +159,83 @@ pub const Style = struct {
|
||||
};
|
||||
}
|
||||
|
||||
/// Formatting to make debug logs easier to read
|
||||
/// by only including non-default attributes.
|
||||
pub fn format(
|
||||
self: Style,
|
||||
comptime fmt: []const u8,
|
||||
options: std.fmt.FormatOptions,
|
||||
writer: anytype,
|
||||
) !void {
|
||||
_ = fmt;
|
||||
_ = options;
|
||||
|
||||
const dflt: Style = .{};
|
||||
|
||||
_ = try writer.write("Style{ ");
|
||||
|
||||
var started = false;
|
||||
|
||||
inline for (std.meta.fields(Style)) |f| {
|
||||
if (std.mem.eql(u8, f.name, "flags")) {
|
||||
if (started) {
|
||||
_ = try writer.write(", ");
|
||||
}
|
||||
|
||||
_ = try writer.write("flags={ ");
|
||||
|
||||
started = false;
|
||||
|
||||
inline for (std.meta.fields(@TypeOf(self.flags))) |ff| {
|
||||
const v = @as(ff.type, @field(self.flags, ff.name));
|
||||
const d = @as(ff.type, @field(dflt.flags, ff.name));
|
||||
if (ff.type == bool) {
|
||||
if (v) {
|
||||
if (started) {
|
||||
_ = try writer.write(", ");
|
||||
}
|
||||
_ = try writer.print("{s}", .{ff.name});
|
||||
started = true;
|
||||
}
|
||||
} else if (!std.meta.eql(v, d)) {
|
||||
if (started) {
|
||||
_ = try writer.write(", ");
|
||||
}
|
||||
_ = try writer.print(
|
||||
"{s}={any}",
|
||||
.{ ff.name, v },
|
||||
);
|
||||
started = true;
|
||||
}
|
||||
}
|
||||
_ = try writer.write(" }");
|
||||
|
||||
started = true;
|
||||
comptime continue;
|
||||
}
|
||||
const value = @as(f.type, @field(self, f.name));
|
||||
const d_val = @as(f.type, @field(dflt, f.name));
|
||||
if (!std.meta.eql(value, d_val)) {
|
||||
if (started) {
|
||||
_ = try writer.write(", ");
|
||||
}
|
||||
_ = try writer.print(
|
||||
"{s}={any}",
|
||||
.{ f.name, value },
|
||||
);
|
||||
started = true;
|
||||
}
|
||||
}
|
||||
|
||||
_ = try writer.write(" }");
|
||||
}
|
||||
|
||||
pub fn hash(self: *const Style) u64 {
|
||||
var hasher = Wyhash.init(0);
|
||||
autoHash(&hasher, self.*);
|
||||
return hasher.final();
|
||||
}
|
||||
|
||||
test {
|
||||
// The size of the struct so we can be aware of changes.
|
||||
const testing = std.testing;
|
||||
@ -140,170 +243,24 @@ pub const Style = struct {
|
||||
}
|
||||
};
|
||||
|
||||
/// A set of styles.
|
||||
///
|
||||
/// This set is created with some capacity in mind. You can determine
|
||||
/// the exact memory requirement for a capacity by calling `layout`
|
||||
/// and checking the total size.
|
||||
///
|
||||
/// When the set exceeds capacity, `error.OutOfMemory` is returned
|
||||
/// from memory-using methods. The caller is responsible for determining
|
||||
/// a path forward.
|
||||
///
|
||||
/// The general idea behind this structure is that it is optimized for
|
||||
/// the scenario common in terminals where there aren't many unique
|
||||
/// styles, and many cells are usually drawn with a single style before
|
||||
/// changing styles.
|
||||
///
|
||||
/// Callers should call `upsert` when a new style is set. This will
|
||||
/// return a stable pointer to metadata. You should use this metadata
|
||||
/// to keep a ref count of the style usage. When it falls to zero you
|
||||
/// can remove it.
|
||||
pub const Set = struct {
|
||||
pub const base_align = @max(MetadataMap.base_align, IdMap.base_align);
|
||||
|
||||
/// The mapping of a style to associated metadata. This is
|
||||
/// the map that contains the actual style definitions
|
||||
/// (in the form of the key).
|
||||
styles: MetadataMap,
|
||||
|
||||
/// The mapping from ID to style.
|
||||
id_map: IdMap,
|
||||
|
||||
/// The next ID to use for a style that isn't in the set.
|
||||
/// When this overflows we'll begin returning an IdOverflow
|
||||
/// error and the caller must manually compact the style
|
||||
/// set.
|
||||
///
|
||||
/// Id zero is reserved and always is the default style. The
|
||||
/// default style isn't present in the map, its dependent on
|
||||
/// the terminal configuration.
|
||||
next_id: Id = 1,
|
||||
|
||||
/// Maps a style definition to metadata about that style.
|
||||
const MetadataMap = AutoOffsetHashMap(Style, Metadata);
|
||||
|
||||
/// Maps the unique style ID to the concrete style definition.
|
||||
const IdMap = AutoOffsetHashMap(Id, Offset(Style));
|
||||
|
||||
/// Returns the memory layout for the given base offset and
|
||||
/// desired capacity. The layout can be used by the caller to
|
||||
/// determine how much memory to allocate, and the layout must
|
||||
/// be used to initialize the set so that the set knows all
|
||||
/// the offsets for the various buffers.
|
||||
pub fn layout(cap: usize) Layout {
|
||||
const md_layout = MetadataMap.layout(@intCast(cap));
|
||||
const md_start = 0;
|
||||
const md_end = md_start + md_layout.total_size;
|
||||
|
||||
const id_layout = IdMap.layout(@intCast(cap));
|
||||
const id_start = std.mem.alignForward(usize, md_end, IdMap.base_align);
|
||||
const id_end = id_start + id_layout.total_size;
|
||||
|
||||
const total_size = id_end;
|
||||
|
||||
return .{
|
||||
.md_start = md_start,
|
||||
.md_layout = md_layout,
|
||||
.id_start = id_start,
|
||||
.id_layout = id_layout,
|
||||
.total_size = total_size,
|
||||
};
|
||||
pub const StyleSetContext = struct {
|
||||
pub fn hash(self: *StyleSetContext, style: Style) u64 {
|
||||
_ = self;
|
||||
return style.hash();
|
||||
}
|
||||
|
||||
pub const Layout = struct {
|
||||
md_start: usize,
|
||||
md_layout: MetadataMap.Layout,
|
||||
id_start: usize,
|
||||
id_layout: IdMap.Layout,
|
||||
total_size: usize,
|
||||
};
|
||||
|
||||
pub fn init(base: OffsetBuf, l: Layout) Set {
|
||||
const styles_buf = base.add(l.md_start);
|
||||
const id_buf = base.add(l.id_start);
|
||||
return .{
|
||||
.styles = MetadataMap.init(styles_buf, l.md_layout),
|
||||
.id_map = IdMap.init(id_buf, l.id_layout),
|
||||
};
|
||||
}
|
||||
|
||||
/// Possible errors for upsert.
|
||||
pub const UpsertError = error{
|
||||
/// No more space in the backing buffer. Remove styles or
|
||||
/// grow and reinitialize.
|
||||
OutOfMemory,
|
||||
|
||||
/// No more available IDs. Perform a garbage collection
|
||||
/// operation to compact ID space.
|
||||
Overflow,
|
||||
};
|
||||
|
||||
/// Upsert a style into the set and return a pointer to the metadata
|
||||
/// for that style. The pointer is valid for the lifetime of the set
|
||||
/// so long as the style is not removed.
|
||||
///
|
||||
/// The ref count for new styles is initialized to zero and
|
||||
/// for existing styles remains unmodified.
|
||||
pub fn upsert(self: *Set, base: anytype, style: Style) UpsertError!*Metadata {
|
||||
// If we already have the style in the map, this is fast.
|
||||
var map = self.styles.map(base);
|
||||
const gop = try map.getOrPut(style);
|
||||
if (gop.found_existing) return gop.value_ptr;
|
||||
|
||||
// New style, we need to setup all the metadata. First thing,
|
||||
// let's get the ID we'll assign, because if we're out of space
|
||||
// we need to fail early.
|
||||
errdefer map.removeByPtr(gop.key_ptr);
|
||||
const id = self.next_id;
|
||||
self.next_id = try std.math.add(Id, self.next_id, 1);
|
||||
errdefer self.next_id -= 1;
|
||||
gop.value_ptr.* = .{ .id = id };
|
||||
|
||||
// Setup our ID mapping
|
||||
var id_map = self.id_map.map(base);
|
||||
const id_gop = try id_map.getOrPut(id);
|
||||
errdefer id_map.removeByPtr(id_gop.key_ptr);
|
||||
assert(!id_gop.found_existing);
|
||||
id_gop.value_ptr.* = size.getOffset(Style, base, gop.key_ptr);
|
||||
return gop.value_ptr;
|
||||
}
|
||||
|
||||
/// Lookup a style by its unique identifier.
|
||||
pub fn lookupId(self: *const Set, base: anytype, id: Id) ?*Style {
|
||||
const id_map = self.id_map.map(base);
|
||||
const offset = id_map.get(id) orelse return null;
|
||||
return @ptrCast(offset.ptr(base));
|
||||
}
|
||||
|
||||
/// Remove a style by its id.
|
||||
pub fn remove(self: *Set, base: anytype, id: Id) void {
|
||||
// Lookup by ID, if it doesn't exist then we return. We use
|
||||
// getEntry so that we can make removal faster later by using
|
||||
// the entry's key pointer.
|
||||
var id_map = self.id_map.map(base);
|
||||
const id_entry = id_map.getEntry(id) orelse return;
|
||||
|
||||
var style_map = self.styles.map(base);
|
||||
const style_ptr: *Style = @ptrCast(id_entry.value_ptr.ptr(base));
|
||||
|
||||
id_map.removeByPtr(id_entry.key_ptr);
|
||||
style_map.removeByPtr(style_ptr);
|
||||
}
|
||||
|
||||
/// Return the number of styles currently in the set.
|
||||
pub fn count(self: *const Set, base: anytype) usize {
|
||||
return self.id_map.map(base).count();
|
||||
pub fn eql(self: *StyleSetContext, a: Style, b: Style) bool {
|
||||
_ = self;
|
||||
return a.eql(b);
|
||||
}
|
||||
};
|
||||
|
||||
/// Metadata about a style. This is used to track the reference count
|
||||
/// and the unique identifier for a style. The unique identifier is used
|
||||
/// to track the style in the full style map.
|
||||
pub const Metadata = struct {
|
||||
ref: size.CellCountInt = 0,
|
||||
id: Id = 0,
|
||||
};
|
||||
pub const Set = RefCountedSet(
|
||||
Style,
|
||||
Id,
|
||||
size.CellCountInt,
|
||||
StyleSetContext,
|
||||
);
|
||||
|
||||
test "Set basic usage" {
|
||||
const testing = std.testing;
|
||||
@ -313,29 +270,49 @@ test "Set basic usage" {
|
||||
defer alloc.free(buf);
|
||||
|
||||
const style: Style = .{ .flags = .{ .bold = true } };
|
||||
const style2: Style = .{ .flags = .{ .italic = true } };
|
||||
|
||||
var set = Set.init(OffsetBuf.init(buf), layout);
|
||||
var set = Set.init(OffsetBuf.init(buf), layout, StyleSetContext{});
|
||||
|
||||
// Upsert
|
||||
const meta = try set.upsert(buf, style);
|
||||
try testing.expect(meta.id > 0);
|
||||
// Add style
|
||||
const id = try set.add(buf, style);
|
||||
try testing.expect(id > 0);
|
||||
|
||||
// Second upsert should return the same metadata.
|
||||
// Second add should return the same metadata.
|
||||
{
|
||||
const meta2 = try set.upsert(buf, style);
|
||||
try testing.expectEqual(meta.id, meta2.id);
|
||||
const id2 = try set.add(buf, style);
|
||||
try testing.expectEqual(id, id2);
|
||||
}
|
||||
|
||||
// Look it up
|
||||
{
|
||||
const v = set.lookupId(buf, meta.id).?;
|
||||
const v = set.get(buf, id);
|
||||
try testing.expect(v.flags.bold);
|
||||
|
||||
const v2 = set.lookupId(buf, meta.id).?;
|
||||
const v2 = set.get(buf, id);
|
||||
try testing.expectEqual(v, v2);
|
||||
}
|
||||
|
||||
// Removal
|
||||
set.remove(buf, meta.id);
|
||||
try testing.expect(set.lookupId(buf, meta.id) == null);
|
||||
// Add a second style
|
||||
const id2 = try set.add(buf, style2);
|
||||
|
||||
// Look it up
|
||||
{
|
||||
const v = set.get(buf, id2);
|
||||
try testing.expect(v.flags.italic);
|
||||
}
|
||||
|
||||
// Ref count
|
||||
try testing.expect(set.refCount(buf, id) == 2);
|
||||
try testing.expect(set.refCount(buf, id2) == 1);
|
||||
|
||||
// Release
|
||||
set.release(buf, id);
|
||||
try testing.expect(set.refCount(buf, id) == 1);
|
||||
set.release(buf, id2);
|
||||
try testing.expect(set.refCount(buf, id2) == 0);
|
||||
|
||||
// We added the first one twice, so
|
||||
set.release(buf, id);
|
||||
try testing.expect(set.refCount(buf, id) == 0);
|
||||
}
|
||||
|
Reference in New Issue
Block a user