diff --git a/bench/bench.zig b/bench/bench.zig new file mode 100644 index 00000000..83afc8b4 --- /dev/null +++ b/bench/bench.zig @@ -0,0 +1,62 @@ +const std = @import("std"); +const vaxis = @import("vaxis"); + +fn parseIterations(allocator: std.mem.Allocator) !usize { + var args = try std.process.argsWithAllocator(allocator); + defer args.deinit(); + _ = args.next(); + if (args.next()) |val| { + return std.fmt.parseUnsigned(usize, val, 10); + } + return 200; +} + +fn printResults(writer: anytype, label: []const u8, iterations: usize, elapsed_ns: u64, total_bytes: usize) !void { + const ns_per_frame = elapsed_ns / @as(u64, @intCast(iterations)); + const bytes_per_frame = total_bytes / iterations; + try writer.print( + "{s}: frames={d} total_ns={d} ns/frame={d} bytes={d} bytes/frame={d}\n", + .{ label, iterations, elapsed_ns, ns_per_frame, total_bytes, bytes_per_frame }, + ); +} + +pub fn main() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + defer _ = gpa.deinit(); + const allocator = gpa.allocator(); + + const iterations = try parseIterations(allocator); + + var vx = try vaxis.init(allocator, .{}); + var init_writer = std.io.Writer.Allocating.init(allocator); + defer init_writer.deinit(); + defer vx.deinit(allocator, &init_writer.writer); + + const winsize = vaxis.Winsize{ .rows = 24, .cols = 80, .x_pixel = 0, .y_pixel = 0 }; + try vx.resize(allocator, &init_writer.writer, winsize); + + const stdout = std.fs.File.stdout().deprecatedWriter(); + + var idle_writer = std.io.Writer.Allocating.init(allocator); + defer idle_writer.deinit(); + var timer = try std.time.Timer.start(); + var i: usize = 0; + while (i < iterations) : (i += 1) { + try vx.render(&idle_writer.writer); + } + const idle_ns = timer.read(); + const idle_bytes: usize = idle_writer.writer.end; + try printResults(stdout, "idle", iterations, idle_ns, idle_bytes); + + var dirty_writer = std.io.Writer.Allocating.init(allocator); + defer dirty_writer.deinit(); + timer.reset(); + i = 0; + while (i < iterations) : (i += 1) { + vx.queueRefresh(); + try vx.render(&dirty_writer.writer); + } + const dirty_ns = timer.read(); + const dirty_bytes: usize = dirty_writer.writer.end; + try printResults(stdout, "dirty", iterations, dirty_ns, dirty_bytes); +} diff --git a/build.zig b/build.zig index 8175193a..2265ac5f 100644 --- a/build.zig +++ b/build.zig @@ -66,6 +66,25 @@ pub fn build(b: *std.Build) void { const example_run = b.addRunArtifact(example); example_step.dependOn(&example_run.step); + // Benchmarks + const bench_step = b.step("bench", "Run benchmarks"); + const bench = b.addExecutable(.{ + .name = "bench", + .root_module = b.createModule(.{ + .root_source_file = b.path("bench/bench.zig"), + .target = target, + .optimize = optimize, + .imports = &.{ + .{ .name = "vaxis", .module = vaxis_mod }, + }, + }), + }); + const bench_run = b.addRunArtifact(bench); + if (b.args) |args| { + bench_run.addArgs(args); + } + bench_step.dependOn(&bench_run.step); + // Tests const tests_step = b.step("test", "Run tests"); diff --git a/src/Vaxis.zig b/src/Vaxis.zig index d52fbe8c..a9bb38b0 100644 --- a/src/Vaxis.zig +++ b/src/Vaxis.zig @@ -360,27 +360,17 @@ pub fn render(self: *Vaxis, tty: *IoWriter) !void { assert(self.screen.buf.len == @as(usize, @intCast(self.screen.width)) * self.screen.height); // correct size assert(self.screen.buf.len == self.screen_last.buf.len); // same size - // Set up sync before we write anything - // TODO: optimize sync so we only sync _when we have changes_. This - // requires a smarter buffered writer, we'll probably have to write - // our own - try tty.writeAll(ctlseqs.sync_set); - errdefer tty.writeAll(ctlseqs.sync_reset) catch {}; - - // Send the cursor to 0,0 - // TODO: this needs to move after we optimize writes. We only do - // this if we have an update to make. We also need to hide cursor - // and then reshow it if needed - try tty.writeAll(ctlseqs.hide_cursor); - if (self.state.alt_screen) - try tty.writeAll(ctlseqs.home) - else { - try tty.writeByte('\r'); - for (0..self.state.cursor.row) |_| { - try tty.writeAll(ctlseqs.ri); - } - } - try tty.writeAll(ctlseqs.sgr_reset); + var started: bool = false; + var sync_active: bool = false; + errdefer if (sync_active) tty.writeAll(ctlseqs.sync_reset) catch {}; + + const cursor_vis_changed = self.screen.cursor_vis != self.screen_last.cursor_vis; + const cursor_shape_changed = self.screen.cursor_shape != self.screen_last.cursor_shape; + const mouse_shape_changed = self.screen.mouse_shape != self.screen_last.mouse_shape; + const cursor_pos_changed = self.screen.cursor_vis and + (self.screen.cursor_row != self.state.cursor.row or + self.screen.cursor_col != self.state.cursor.col); + const needs_render = self.refresh or cursor_vis_changed or cursor_shape_changed or mouse_shape_changed or cursor_pos_changed; // initialize some variables var reposition: bool = false; @@ -388,20 +378,54 @@ pub fn render(self: *Vaxis, tty: *IoWriter) !void { var col: u16 = 0; var cursor: Style = .{}; var link: Hyperlink = .{}; - var cursor_pos: struct { + const CursorPos = struct { row: u16 = 0, col: u16 = 0, - } = .{}; - - // Clear all images - if (self.caps.kitty_graphics) - try tty.writeAll(ctlseqs.kitty_graphics_clear); + }; + var cursor_pos: CursorPos = .{}; + + const startRender = struct { + fn run( + vx: *Vaxis, + io: *IoWriter, + cursor_pos_ptr: *CursorPos, + reposition_ptr: *bool, + started_ptr: *bool, + sync_active_ptr: *bool, + ) !void { + if (started_ptr.*) return; + started_ptr.* = true; + sync_active_ptr.* = true; + // Set up sync before we write anything + try io.writeAll(ctlseqs.sync_set); + // Send the cursor to 0,0 + try io.writeAll(ctlseqs.hide_cursor); + if (vx.state.alt_screen) + try io.writeAll(ctlseqs.home) + else { + try io.writeByte('\r'); + for (0..vx.state.cursor.row) |_| { + try io.writeAll(ctlseqs.ri); + } + } + try io.writeAll(ctlseqs.sgr_reset); + cursor_pos_ptr.* = .{}; + reposition_ptr.* = true; + // Clear all images + if (vx.caps.kitty_graphics) + try io.writeAll(ctlseqs.kitty_graphics_clear); + } + }; // Reset skip flag on all last_screen cells for (self.screen_last.buf) |*last_cell| { last_cell.skip = false; } + if (needs_render) { + try startRender.run(self, tty, &cursor_pos, &reposition, &started, &sync_active); + } + var i: usize = 0; while (i < self.screen.buf.len) { const cell = self.screen.buf[i]; @@ -447,6 +471,9 @@ pub fn render(self: *Vaxis, tty: *IoWriter) !void { } continue; } + if (!started) { + try startRender.run(self, tty, &cursor_pos, &reposition, &started, &sync_active); + } self.screen_last.buf[i].skipped = false; defer { cursor = cell.style; @@ -730,6 +757,7 @@ pub fn render(self: *Vaxis, tty: *IoWriter) !void { cursor_pos.col = col + w; cursor_pos.row = row; } + if (!started) return; if (self.screen.cursor_vis) { if (self.state.alt_screen) { try tty.print( @@ -761,6 +789,7 @@ pub fn render(self: *Vaxis, tty: *IoWriter) !void { self.state.cursor.row = cursor_pos.row; self.state.cursor.col = cursor_pos.col; } + self.screen_last.cursor_vis = self.screen.cursor_vis; if (self.screen.mouse_shape != self.screen_last.mouse_shape) { try tty.print( ctlseqs.osc22_mouse_shape, @@ -1409,3 +1438,17 @@ pub fn setTerminalWorkingDirectory(_: *Vaxis, tty: *IoWriter, path: []const u8) try tty.print(ctlseqs.osc7, .{uri.fmt(.{ .scheme = true, .authority = true, .path = true })}); try tty.flush(); } + +test "render: no output when no changes" { + var vx = try Vaxis.init(std.testing.allocator, .{}); + var deinit_writer = std.io.Writer.Allocating.init(std.testing.allocator); + defer deinit_writer.deinit(); + defer vx.deinit(std.testing.allocator, &deinit_writer.writer); + + var render_writer = std.io.Writer.Allocating.init(std.testing.allocator); + defer render_writer.deinit(); + try vx.render(&render_writer.writer); + const output = try render_writer.toOwnedSlice(); + defer std.testing.allocator.free(output); + try std.testing.expectEqual(@as(usize, 0), output.len); +}