benchmark: remove offset audit workloads

The offset audit added page-backed style-set and grapheme-map workloads
to measure candidate changes. They are useful for investigation but
broaden the benchmark CLI beyond the final production changes.

Remove the workload implementations and registrations for now.
Performance results remain in the individual commit messages, so the
harnesses can be restored later if needed.
This commit is contained in:
Mitchell Hashimoto
2026-07-12 06:44:57 -07:00
parent a5071a9e6e
commit 0a3b4dca1c
4 changed files with 0 additions and 361 deletions

View File

@@ -1,169 +0,0 @@
//! Benchmark grapheme storage on a page: the bitmap allocator that holds
//! multi-codepoint grapheme data plus the cell -> codepoints offset hash map.
//!
//! The `churn` mode models emoji-heavy terminal output that repeatedly
//! overwrites cells on a page whose grapheme allocator is close to full:
//! every overwritten cell frees its chunks and map entry, then allocates
//! and inserts again. This is the per-cell cost of printing a grapheme
//! over an existing one, and is sensitive to both allocator scan cost and
//! map probe/hash cost.
const GraphemeMap = @This();
const std = @import("std");
const Allocator = std.mem.Allocator;
const terminal = @import("../terminal/main.zig");
const pagepkg = @import("../terminal/page.zig");
const Benchmark = @import("Benchmark.zig");
const log = std.log.scoped(.@"grapheme-map-bench");
opts: Options,
page: terminal.Page,
entry_count: usize,
cps: []const u21,
pub const Options = struct {
/// Requested grapheme working-set size in cells. Must be a power of
/// two and at least 16.
entries: u16 = 4096,
/// Number of extra codepoints attached to each cell. Each chunk of
/// grapheme storage holds 4 codepoints, so values above 4 exercise
/// multi-chunk allocations.
cps: u8 = 1,
/// Percentage of the entries populated before the timed operation.
/// Values above 100 are treated as 100. The resident count is capped
/// by the page's production grapheme capacity.
@"load-percent": u8 = 100,
/// Number of complete passes over the populated cells per step.
loops: u16 = 1,
/// Operation to perform in the timed region.
mode: Mode = .churn,
};
pub const Mode = enum {
/// Look up the grapheme data for every populated cell.
lookup,
/// Clear and re-append the grapheme data for every populated cell.
churn,
};
pub fn create(alloc: Allocator, opts: Options) !*GraphemeMap {
if (opts.entries < 16 or !std.math.isPowerOfTwo(opts.entries)) {
log.err("entries must be a power of two greater than or equal to 16", .{});
return error.InvalidEntries;
}
if (opts.cps < 1) {
log.err("cps must be at least 1", .{});
return error.InvalidCps;
}
const ptr = try alloc.create(GraphemeMap);
errdefer alloc.destroy(ptr);
const cps = try alloc.alloc(u21, opts.cps);
errdefer alloc.free(cps);
for (cps, 0..) |*cp, i| cp.* = @intCast(0x1F600 + i);
// Size the allocator to exactly fit the requested working set. The
// production map load limit may cap the resident set before every
// allocator chunk can be occupied.
const chunks_per_entry = std.math.divCeil(
usize,
@as(usize, opts.cps) * @sizeOf(u21),
pagepkg.grapheme_chunk,
) catch unreachable;
var page = try terminal.Page.init(.{
.cols = opts.entries,
.rows = 1,
.grapheme_bytes = @intCast(
opts.entries * chunks_per_entry * pagepkg.grapheme_chunk,
),
});
errdefer page.deinit();
const load = @min(opts.@"load-percent", 100);
const entry_count = @max(1, @min(
page.graphemeCapacity(),
@divFloor(@as(usize, opts.entries) * load, 100),
));
for (0..entry_count) |x| {
const rac = page.getRowAndCell(x, 0);
rac.cell.* = .{
.content_tag = .codepoint,
.content = .{ .codepoint = 'A' },
};
try page.setGraphemes(rac.row, rac.cell, cps);
}
ptr.* = .{
.opts = opts,
.page = page,
.entry_count = entry_count,
.cps = cps,
};
return ptr;
}
pub fn destroy(self: *GraphemeMap, alloc: Allocator) void {
self.page.deinit();
alloc.free(self.cps);
alloc.destroy(self);
}
pub fn benchmark(self: *GraphemeMap) Benchmark {
return .init(self, .{
.stepFn = switch (self.opts.mode) {
.lookup => stepLookup,
.churn => stepChurn,
},
});
}
fn stepLookup(ptr: *anyopaque) Benchmark.Error!void {
const self: *GraphemeMap = @ptrCast(@alignCast(ptr));
for (0..self.opts.loops) |_| {
for (0..self.entry_count) |x| {
const cell = self.page.getRowAndCell(x, 0).cell;
const data = self.page.lookupGrapheme(cell) orelse
return error.BenchmarkFailed;
std.mem.doNotOptimizeAway(data);
}
}
}
fn stepChurn(ptr: *anyopaque) Benchmark.Error!void {
const self: *GraphemeMap = @ptrCast(@alignCast(ptr));
for (0..self.opts.loops) |_| {
for (0..self.entry_count) |x| {
const rac = self.page.getRowAndCell(x, 0);
self.page.clearGrapheme(rac.cell);
self.page.setGraphemes(rac.row, rac.cell, self.cps) catch
return error.BenchmarkFailed;
}
}
}
test GraphemeMap {
const alloc = std.testing.allocator;
inline for (.{ @as(u8, 1), 5 }) |cps| {
inline for (.{ Mode.lookup, Mode.churn }) |mode| {
const impl = try GraphemeMap.create(alloc, .{
.entries = 64,
.cps = cps,
.mode = mode,
});
defer impl.destroy(alloc);
const bench = impl.benchmark();
_ = try bench.run(.once);
}
}
}

View File

@@ -1,186 +0,0 @@
//! 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);
}
}

View File

@@ -9,12 +9,10 @@ pub const Action = enum {
@"apc-parser",
@"codepoint-width",
@"grapheme-break",
@"grapheme-map",
@"hyperlink-map",
@"page-compression",
@"scrollback-compression",
@"screen-clone",
@"style-set",
@"terminal-parser",
@"terminal-stream",
@"is-symbol",
@@ -31,10 +29,8 @@ pub const Action = enum {
pub fn Struct(comptime action: Action) type {
return switch (action) {
.@"apc-parser" => @import("ApcParser.zig"),
.@"grapheme-map" => @import("GraphemeMap.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

@@ -4,10 +4,8 @@ pub const CApi = @import("CApi.zig");
pub const TerminalStream = @import("TerminalStream.zig");
pub const CodepointWidth = @import("CodepointWidth.zig");
pub const GraphemeBreak = @import("GraphemeBreak.zig");
pub const GraphemeMap = @import("GraphemeMap.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");