Files
ghostty/src/benchmark/StyleSet.zig
Mitchell Hashimoto 5f16939905 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.
2026-07-12 06:58:39 -07:00

187 lines
5.3 KiB
Zig

//! Benchmark style-set lookup and dead-entry rebuilds.
//!
//! Terminal cells store stable style IDs, so ordinary printing and rendering
//! do not hash styles. Hash-table work is concentrated in actual SGR state
//! changes and rebuilding styles after cell references are cleared. The two
//! modes below isolate those workloads.
const StyleSet = @This();
const std = @import("std");
const Allocator = std.mem.Allocator;
const terminal = @import("../terminal/main.zig");
const style = @import("../terminal/style.zig");
const Benchmark = @import("Benchmark.zig");
const log = std.log.scoped(.@"style-set-bench");
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.
entries: u16 = 128,
/// Number of complete passes over the working set per step.
loops: u32 = 1,
/// Operation to perform in the timed region.
mode: Mode = .lookup,
};
pub const Mode = enum {
/// Add styles that are already live, then release the added reference.
lookup,
/// 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);
var page = try terminal.Page.init(.{
.cols = 1,
.rows = 1,
.styles = @intCast(style.Set.capacityForCount(opts.entries)),
});
errdefer page.deinit();
const values = try alloc.alloc(style.Style, opts.entries);
errdefer alloc.free(values);
const ids = try alloc.alloc(style.Id, opts.entries);
errdefer alloc.free(ids);
for (values, 0..) |*value, i| {
value.* = styleFor(@intCast(i + 1));
}
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.* = .{
.opts = opts,
.page = page,
.values = values,
.ids = ids,
};
return ptr;
}
pub fn destroy(self: *StyleSet, alloc: Allocator) void {
self.page.deinit();
alloc.free(self.ids);
alloc.free(self.values);
alloc.destroy(self);
}
pub fn benchmark(self: *StyleSet) Benchmark {
return .init(self, .{
.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));
for (0..self.opts.loops) |_| {
for (self.values) |value| {
const id = self.page.styles.add(self.page.memory, value) catch
return error.BenchmarkFailed;
std.mem.doNotOptimizeAway(id);
self.page.styles.release(self.page.memory, id);
}
}
}
fn stepChurn(ptr: *anyopaque) Benchmark.Error!void {
const self: *StyleSet = @ptrCast(@alignCast(ptr));
for (0..self.opts.loops) |_| {
for (self.values, self.ids) |value, *id| {
id.* = self.page.styles.add(self.page.memory, value) catch
return error.BenchmarkFailed;
}
for (self.ids) |id| self.page.styles.release(self.page.memory, id);
}
}
test StyleSet {
const alloc = std.testing.allocator;
inline for (.{ Mode.lookup, Mode.churn, Mode.replace }) |mode| {
const impl = try StyleSet.create(alloc, .{
.entries = 64,
.mode = mode,
});
defer impl.destroy(alloc);
const bench = impl.benchmark();
_ = try bench.run(.once);
}
}