Skip to content

Avoid render output on idle frames and add benchmark#285

Merged
rockorager merged 3 commits intorockorager:mainfrom
dyxushuai:fix/render-noop
Dec 28, 2025
Merged

Avoid render output on idle frames and add benchmark#285
rockorager merged 3 commits intorockorager:mainfrom
dyxushuai:fix/render-noop

Conversation

@dyxushuai
Copy link
Contributor

@dyxushuai dyxushuai commented Dec 28, 2025

TODO

render always emits sync/hide/home/SGR reset (and clears kitty graphics) even when the screen, cursor, and shapes are unchanged. This creates unnecessary terminal I/O on idle frames.

Steps

  • Detect changes (buffer, refresh, cursor visibility/position, cursor/mouse shapes).
  • Only initialize sync/hide/home/clear when a change is detected.
  • Track last cursor visibility to avoid redundant renders.
  • Add a test asserting no output for idle frames.

Benchmark (local)

Command: zig build bench -Doptimize=ReleaseFast -- 200

idle:  frames=200 total_ns=7175271  ns/frame=35876  bytes=0      bytes/frame=0
dirty: frames=200 total_ns=11949574 ns/frame=59747  bytes=407554 bytes/frame=2037

Tests

  • zig build test
  • zig build bench -Doptimize=ReleaseFast -- 200

Copilot AI review requested due to automatic review settings December 28, 2025 13:04
@rockorager rockorager merged commit c2f00c2 into rockorager:main Dec 28, 2025
@rockorager
Copy link
Owner

thanks!

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR optimizes the render function to avoid unnecessary terminal I/O when there are no changes to display (idle frames). The optimization introduces change detection for buffer contents, cursor visibility/position, and cursor/mouse shapes, only performing terminal writes when changes are detected.

Key changes:

  • Added conditional rendering initialization that only executes when changes are detected
  • Introduced tracking of cursor visibility in screen_last to avoid redundant renders
  • Added a test case to verify no output is produced on idle frames
  • Added a benchmark suite to measure performance improvements

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 7 comments.

File Description
src/Vaxis.zig Core optimization: adds change detection logic, conditional startRender execution, cursor visibility tracking, idle frame test
build.zig Adds benchmark build step with proper module configuration and argument forwarding
bench/bench.zig New benchmark implementation measuring idle vs dirty frame performance

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

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();
Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The method deprecatedWriter() does not exist on std.fs.File. This should be writer() to get a writer from stdout.

Suggested change
const stdout = std.fs.File.stdout().deprecatedWriter();
const stdout = std.fs.File.stdout().writer();

Copilot uses AI. Check for mistakes.
self.state.cursor.row = cursor_pos.row;
self.state.cursor.col = cursor_pos.col;
}
self.screen_last.cursor_vis = self.screen.cursor_vis;
Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After updating screen_last.cursor_vis at line 792, the code should also update screen_last.cursor_row and screen_last.cursor_col to track the last rendered cursor position. This is necessary for the cursor position change detection at lines 370-372 to work correctly. Add:

self.screen_last.cursor_row = self.screen.cursor_row;
self.screen_last.cursor_col = self.screen.cursor_col;
Suggested change
self.screen_last.cursor_vis = self.screen.cursor_vis;
self.screen_last.cursor_vis = self.screen.cursor_vis;
self.screen_last.cursor_row = self.screen.cursor_row;
self.screen_last.cursor_col = self.screen.cursor_col;

Copilot uses AI. Check for mistakes.
Comment on lines +48 to +60
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;
Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code accesses idle_writer.writer.end but this is incorrect. If Writer.Allocating follows the pattern of a buffering writer, it likely has the byte count accessible directly on the struct (e.g., idle_writer.end or similar), not on the .writer field. The .writer field typically returns a writer interface that doesn't expose the buffer size.

Copilot uses AI. Check for mistakes.
Comment on lines +371 to +372
(self.screen.cursor_row != self.state.cursor.row or
self.screen.cursor_col != self.state.cursor.col);
Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cursor position change detection compares against self.state.cursor instead of self.screen_last.cursor_row/cursor_col. However, self.screen_last.cursor_row and self.screen_last.cursor_col are never updated anywhere in the code (unlike cursor_vis at line 792 and cursor_shape at line 805). This means position changes won't be properly detected on subsequent renders unless self.state.cursor differs. You should add lines to update self.screen_last.cursor_row and self.screen_last.cursor_col after positioning the cursor (around line 792), and then use those fields for comparison in the change detection.

Suggested change
(self.screen.cursor_row != self.state.cursor.row or
self.screen.cursor_col != self.state.cursor.col);
(self.screen.cursor_row != self.screen_last.cursor_row or
self.screen.cursor_col != self.screen_last.cursor_col);

Copilot uses AI. Check for mistakes.
Comment on lines +1444 to +1449
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();
Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test uses std.io.Writer.Allocating (lowercase 'io'), but the existing code in tty.zig uses std.Io.Writer.Allocating (capital 'I'). This inconsistency will cause a compilation error since std.io.Writer.Allocating does not exist in Zig's standard library. It should be std.Io.Writer.Allocating to match the existing pattern in the codebase.

Copilot uses AI. Check for mistakes.
Comment on lines +31 to +52
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();
Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The benchmark uses std.io.Writer.Allocating (lowercase 'io'), but the existing code in tty.zig uses std.Io.Writer.Allocating (capital 'I'). This inconsistency will cause a compilation error since std.io.Writer.Allocating does not exist in Zig's standard library. It should be std.Io.Writer.Allocating to match the existing pattern in the codebase.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

Comments