mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-07-31 04:39:01 +00:00
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:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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"),
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user