mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-07-31 04:39:01 +00:00
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.
187 lines
5.3 KiB
Zig
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);
|
|
}
|
|
}
|