diff --git a/src/benchmark/ScreenClone.zig b/src/benchmark/ScreenClone.zig new file mode 100644 index 000000000..942b08cd1 --- /dev/null +++ b/src/benchmark/ScreenClone.zig @@ -0,0 +1,155 @@ +//! This benchmark tests the performance of the Screen.clone +//! function. This is useful because it is one of the primary lock +//! holders that impact IO performance when the renderer is active. +//! We do this very frequently. +const ScreenClone = @This(); + +const std = @import("std"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; +const terminalpkg = @import("../terminal/main.zig"); +const Benchmark = @import("Benchmark.zig"); +const options = @import("options.zig"); +const Terminal = terminalpkg.Terminal; + +const log = std.log.scoped(.@"terminal-stream-bench"); + +opts: Options, +terminal: Terminal, + +pub const Options = struct { + /// The type of codepoint width calculation to use. + mode: Mode = .clone, + + /// The size of the terminal. This affects benchmarking when + /// dealing with soft line wrapping and the memory impact + /// of page sizes. + @"terminal-rows": u16 = 80, + @"terminal-cols": u16 = 120, + + /// The data to read as a filepath. If this is "-" then + /// we will read stdin. If this is unset, then we will + /// do nothing (benchmark is a noop). It'd be more unixy to + /// use stdin by default but I find that a hanging CLI command + /// with no interaction is a bit annoying. + /// + /// This will be used to initialize the terminal screen state before + /// cloning. This data can switch to alt screen if it wants. The time + /// to read this is not part of the benchmark. + data: ?[]const u8 = null, +}; + +pub const Mode = enum { + /// The baseline mode copies the screen by value. + noop, + + /// Full clone + clone, +}; + +pub fn create( + alloc: Allocator, + opts: Options, +) !*ScreenClone { + const ptr = try alloc.create(ScreenClone); + errdefer alloc.destroy(ptr); + + ptr.* = .{ + .opts = opts, + .terminal = try .init(alloc, .{ + .rows = opts.@"terminal-rows", + .cols = opts.@"terminal-cols", + }), + }; + + return ptr; +} + +pub fn destroy(self: *ScreenClone, alloc: Allocator) void { + self.terminal.deinit(alloc); + alloc.destroy(self); +} + +pub fn benchmark(self: *ScreenClone) Benchmark { + return .init(self, .{ + .stepFn = switch (self.opts.mode) { + .noop => stepNoop, + .clone => stepClone, + }, + .setupFn = setup, + .teardownFn = teardown, + }); +} + +fn setup(ptr: *anyopaque) Benchmark.Error!void { + const self: *ScreenClone = @ptrCast(@alignCast(ptr)); + + // Always reset our terminal state + self.terminal.fullReset(); + + // Setup our terminal state + const data_f: std.fs.File = (options.dataFile( + self.opts.data, + ) catch |err| { + log.warn("error opening data file err={}", .{err}); + return error.BenchmarkFailed; + }) orelse return; + + var stream = self.terminal.vtStream(); + defer stream.deinit(); + + var read_buf: [4096]u8 = undefined; + var f_reader = data_f.reader(&read_buf); + const r = &f_reader.interface; + + var buf: [4096]u8 = undefined; + while (true) { + const n = r.readSliceShort(&buf) catch { + log.warn("error reading data file err={?}", .{f_reader.err}); + return error.BenchmarkFailed; + }; + if (n == 0) break; // EOF reached + stream.nextSlice(buf[0..n]) catch |err| { + log.warn("error processing data file chunk err={}", .{err}); + return error.BenchmarkFailed; + }; + } +} + +fn teardown(ptr: *anyopaque) void { + const self: *ScreenClone = @ptrCast(@alignCast(ptr)); + _ = self; +} + +fn stepNoop(ptr: *anyopaque) Benchmark.Error!void { + const self: *ScreenClone = @ptrCast(@alignCast(ptr)); + + // We loop because its so fast that a single benchmark run doesn't + // properly capture our speeds. + for (0..1000) |_| { + const s: terminalpkg.Screen = self.terminal.screens.active.*; + std.mem.doNotOptimizeAway(s); + } +} + +fn stepClone(ptr: *anyopaque) Benchmark.Error!void { + const self: *ScreenClone = @ptrCast(@alignCast(ptr)); + + // We loop because its so fast that a single benchmark run doesn't + // properly capture our speeds. + for (0..1000) |_| { + const s: *terminalpkg.Screen = self.terminal.screens.active; + const copy = s.clone( + s.alloc, + .{ .viewport = .{} }, + null, + ) catch |err| { + log.warn("error cloning screen err={}", .{err}); + return error.BenchmarkFailed; + }; + std.mem.doNotOptimizeAway(copy); + + // Note: we purposely do not free memory because we don't want + // to benchmark that. We'll free when the benchmark exits. + } +} diff --git a/src/benchmark/cli.zig b/src/benchmark/cli.zig index 3b1c905eb..816ecd3f6 100644 --- a/src/benchmark/cli.zig +++ b/src/benchmark/cli.zig @@ -8,6 +8,7 @@ const cli = @import("../cli.zig"); pub const Action = enum { @"codepoint-width", @"grapheme-break", + @"screen-clone", @"terminal-parser", @"terminal-stream", @"is-symbol", @@ -22,6 +23,7 @@ pub const Action = enum { /// See TerminalStream for an example. pub fn Struct(comptime action: Action) type { return switch (action) { + .@"screen-clone" => @import("ScreenClone.zig"), .@"terminal-stream" => @import("TerminalStream.zig"), .@"codepoint-width" => @import("CodepointWidth.zig"), .@"grapheme-break" => @import("GraphemeBreak.zig"), diff --git a/src/benchmark/main.zig b/src/benchmark/main.zig index 3a59125fc..5673044f2 100644 --- a/src/benchmark/main.zig +++ b/src/benchmark/main.zig @@ -4,6 +4,7 @@ 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 ScreenClone = @import("ScreenClone.zig"); pub const TerminalParser = @import("TerminalParser.zig"); pub const IsSymbol = @import("IsSymbol.zig"); diff --git a/src/terminal/page.zig b/src/terminal/page.zig index b13c625ed..426676a1c 100644 --- a/src/terminal/page.zig +++ b/src/terminal/page.zig @@ -2233,6 +2233,84 @@ test "Page clone" { } } +test "Page clone graphemes" { + var page = try Page.init(.{ + .cols = 10, + .rows = 10, + .styles = 8, + }); + defer page.deinit(); + + // Append some graphemes + { + const rac = page.getRowAndCell(0, 0); + rac.cell.* = .init(0x09); + try page.appendGrapheme(rac.row, rac.cell, 0x0A); + try page.appendGrapheme(rac.row, rac.cell, 0x0B); + } + + // Clone it + var page2 = try page.clone(); + defer page2.deinit(); + { + const rac = page2.getRowAndCell(0, 0); + try testing.expect(rac.row.grapheme); + try testing.expect(rac.cell.hasGrapheme()); + try testing.expectEqualSlices(u21, &.{ 0x0A, 0x0B }, page2.lookupGrapheme(rac.cell).?); + } +} + +test "Page clone styles" { + var page = try Page.init(.{ + .cols = 10, + .rows = 10, + .styles = 8, + }); + defer page.deinit(); + + // Write with some styles + { + const id = try page.styles.add(page.memory, .{ .flags = .{ + .bold = true, + } }); + + for (0..page.size.cols) |x| { + const rac = page.getRowAndCell(x, 0); + rac.row.styled = true; + rac.cell.* = .{ + .content_tag = .codepoint, + .content = .{ .codepoint = @intCast(x + 1) }, + .style_id = id, + }; + page.styles.use(page.memory, id); + } + } + + // Clone it + var page2 = try page.clone(); + defer page2.deinit(); + { + const id: u16 = style: { + const rac = page2.getRowAndCell(0, 0); + break :style rac.cell.style_id; + }; + + for (0..page.size.cols) |x| { + const rac = page.getRowAndCell(x, 0); + try testing.expect(rac.row.styled); + try testing.expectEqual(id, rac.cell.style_id); + } + + const style = page.styles.get( + page.memory, + id, + ); + try testing.expect((Style{ .flags = .{ + .bold = true, + } }).eql(style.*)); + } +} + test "Page cloneFrom" { var page = try Page.init(.{ .cols = 10,