From 5f16939905ce3796c2fead1dfc50cdc30f1a3035 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 11 Jul 2026 20:10:02 -0700 Subject: [PATCH] benchmark: add dense style replacement workload Add a replacement mode that keeps all but one requested style live while repeatedly adding and releasing unique styles. This isolates dense backward-shift deletion and ID reuse without triggering the all-dead bulk reset. Generate replacement styles on demand so long runs continue to exercise misses instead of cycling through the resident working set. --- src/benchmark/StyleSet.zig | 68 ++++++++++++++++++++++++++++++-------- 1 file changed, 55 insertions(+), 13 deletions(-) diff --git a/src/benchmark/StyleSet.zig b/src/benchmark/StyleSet.zig index ed863def5..f8aef54df 100644 --- a/src/benchmark/StyleSet.zig +++ b/src/benchmark/StyleSet.zig @@ -18,6 +18,7 @@ opts: Options, page: terminal.Page, values: []style.Style, ids: []style.Id, +cursor: u24 = 0x100000, pub const Options = struct { /// Number of distinct styles in the working set. @@ -37,13 +38,29 @@ pub const Mode = enum { /// Rebuild a working set whose previous entries are all dead, then clear /// all of its references again. churn, + + /// Keep the set nearly full while repeatedly adding and releasing unique + /// styles. This exercises dense deletion without the all-dead reset. + replace, }; +fn styleFor(n: u24) style.Style { + return .{ .fg_color = .{ .rgb = .{ + .r = @truncate(n), + .g = @truncate(n >> 8), + .b = @truncate(n >> 16), + } } }; +} + pub fn create(alloc: Allocator, opts: Options) !*StyleSet { if (opts.entries == 0 or opts.entries > style.Set.max_count) { log.err("entries must be between 1 and {}", .{style.Set.max_count}); return error.InvalidEntries; } + if (opts.mode == .replace and opts.entries < 2) { + log.err("replace mode requires at least 2 entries", .{}); + return error.InvalidEntries; + } const ptr = try alloc.create(StyleSet); errdefer alloc.destroy(ptr); @@ -62,20 +79,28 @@ pub fn create(alloc: Allocator, opts: Options) !*StyleSet { errdefer alloc.free(ids); for (values, 0..) |*value, i| { - const n: u24 = @intCast(i + 1); - value.* = .{ .fg_color = .{ .rgb = .{ - .r = @truncate(n), - .g = @truncate(n >> 8), - .b = @truncate(n >> 16), - } } }; + value.* = styleFor(@intCast(i + 1)); } - for (values, ids) |value, *id| { - id.* = try page.styles.add(page.memory, value); - } - - if (opts.mode == .churn) { - for (ids) |id| page.styles.release(page.memory, id); + switch (opts.mode) { + .lookup, .churn => { + for (values, ids) |value, *id| { + id.* = try page.styles.add(page.memory, value); + } + if (opts.mode == .churn) { + for (ids) |id| page.styles.release(page.memory, id); + } + }, + .replace => { + // Leave one slot for the unique style added by stepReplace while + // keeping the table dense enough to exercise deletion backshifts. + for ( + values[0 .. values.len - 1], + ids[0 .. ids.len - 1], + ) |value, *id| { + id.* = try page.styles.add(page.memory, value); + } + }, } ptr.* = .{ @@ -99,10 +124,27 @@ pub fn benchmark(self: *StyleSet) Benchmark { .stepFn = switch (self.opts.mode) { .lookup => stepLookup, .churn => stepChurn, + .replace => stepReplace, }, }); } +fn stepReplace(ptr: *anyopaque) Benchmark.Error!void { + const self: *StyleSet = @ptrCast(@alignCast(ptr)); + + for (0..self.opts.loops) |_| { + for (self.values) |_| { + const id = self.page.styles.add( + self.page.memory, + styleFor(self.cursor), + ) catch return error.BenchmarkFailed; + std.mem.doNotOptimizeAway(id); + self.page.styles.release(self.page.memory, id); + self.cursor +%= 1; + } + } +} + fn stepLookup(ptr: *anyopaque) Benchmark.Error!void { const self: *StyleSet = @ptrCast(@alignCast(ptr)); @@ -131,7 +173,7 @@ fn stepChurn(ptr: *anyopaque) Benchmark.Error!void { test StyleSet { const alloc = std.testing.allocator; - inline for (.{ Mode.lookup, Mode.churn }) |mode| { + inline for (.{ Mode.lookup, Mode.churn, Mode.replace }) |mode| { const impl = try StyleSet.create(alloc, .{ .entries = 64, .mode = mode,