benchmark: add style set workloads

Add focused live-lookup and all-dead churn workloads for the terminal
style set. The benchmark uses a real page-backed set so its layout and
offset access match the production path.

The lookup mode repeatedly acquires existing styles while preserving their
liveness. The churn mode releases the complete working set between passes
to model clear and redraw behavior.
This commit is contained in:
Mitchell Hashimoto
2026-07-11 19:52:18 -07:00
parent 641df6cd2a
commit c68d27c5fa
3 changed files with 147 additions and 0 deletions

144
src/benchmark/StyleSet.zig Normal file
View File

@@ -0,0 +1,144 @@
//! 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,
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,
};
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;
}
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| {
const n: u24 = @intCast(i + 1);
value.* = .{ .fg_color = .{ .rgb = .{
.r = @truncate(n),
.g = @truncate(n >> 8),
.b = @truncate(n >> 16),
} } };
}
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);
}
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,
},
});
}
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| {
const impl = try StyleSet.create(alloc, .{
.entries = 64,
.mode = mode,
});
defer impl.destroy(alloc);
const bench = impl.benchmark();
_ = try bench.run(.once);
}
}

View File

@@ -13,6 +13,7 @@ pub const Action = enum {
@"page-compression",
@"scrollback-compression",
@"screen-clone",
@"style-set",
@"terminal-parser",
@"terminal-stream",
@"is-symbol",
@@ -31,6 +32,7 @@ pub const Action = enum {
.@"apc-parser" => @import("ApcParser.zig"),
.@"hyperlink-map" => @import("HyperlinkMap.zig"),
.@"screen-clone" => @import("ScreenClone.zig"),
.@"style-set" => @import("StyleSet.zig"),
.@"page-compression" => @import("PageCompression.zig"),
.@"scrollback-compression" => @import("ScrollbackCompression.zig"),
.@"terminal-stream" => @import("TerminalStream.zig"),

View File

@@ -6,6 +6,7 @@ pub const CodepointWidth = @import("CodepointWidth.zig");
pub const GraphemeBreak = @import("GraphemeBreak.zig");
pub const HyperlinkMap = @import("HyperlinkMap.zig");
pub const ScreenClone = @import("ScreenClone.zig");
pub const StyleSet = @import("StyleSet.zig");
pub const TerminalParser = @import("TerminalParser.zig");
pub const IsSymbol = @import("IsSymbol.zig");
pub const PageCompression = @import("PageCompression.zig");