diff --git a/src/benchmark/GraphemeMap.zig b/src/benchmark/GraphemeMap.zig deleted file mode 100644 index 717d6ec82..000000000 --- a/src/benchmark/GraphemeMap.zig +++ /dev/null @@ -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); - } - } -} diff --git a/src/benchmark/StyleSet.zig b/src/benchmark/StyleSet.zig deleted file mode 100644 index f8aef54df..000000000 --- a/src/benchmark/StyleSet.zig +++ /dev/null @@ -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); - } -} diff --git a/src/benchmark/cli.zig b/src/benchmark/cli.zig index 2bd95a898..dc4d7b8f4 100644 --- a/src/benchmark/cli.zig +++ b/src/benchmark/cli.zig @@ -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"), diff --git a/src/benchmark/main.zig b/src/benchmark/main.zig index 53687a1d5..f22891c71 100644 --- a/src/benchmark/main.zig +++ b/src/benchmark/main.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");