diff --git a/src/benchmark/StyleSet.zig b/src/benchmark/StyleSet.zig new file mode 100644 index 000000000..ed863def5 --- /dev/null +++ b/src/benchmark/StyleSet.zig @@ -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); + } +} diff --git a/src/benchmark/cli.zig b/src/benchmark/cli.zig index dc4d7b8f4..5d9519910 100644 --- a/src/benchmark/cli.zig +++ b/src/benchmark/cli.zig @@ -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"), diff --git a/src/benchmark/main.zig b/src/benchmark/main.zig index f22891c71..f899c6d61 100644 --- a/src/benchmark/main.zig +++ b/src/benchmark/main.zig @@ -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");