summaryrefslogtreecommitdiff
path: root/tests_syntax_perf.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-07-03 17:33:05 -0300
committerGabriel Schneider <[email protected]>2026-08-01 15:02:07 -0300
commitf3b62fe53b19e36f21f8020831718cbf095d6a7b (patch)
treecdfd008869bb5f5aeb69f0ef30584fec86f6f95b /tests_syntax_perf.zig
parent6b4b1f76bb6bf969fb5106f95fb3f9ad077fac08 (diff)
downloadpardes-f3b62fe53b19e36f21f8020831718cbf095d6a7b.tar.gz
pardes-f3b62fe53b19e36f21f8020831718cbf095d6a7b.zip
performance pass
Diffstat (limited to 'tests_syntax_perf.zig')
-rw-r--r--tests_syntax_perf.zig795
1 files changed, 795 insertions, 0 deletions
diff --git a/tests_syntax_perf.zig b/tests_syntax_perf.zig
new file mode 100644
index 00000000..4179cd38
--- /dev/null
+++ b/tests_syntax_perf.zig
@@ -0,0 +1,795 @@
+const std = @import("std");
+const h = @import("e2e_harness.zig");
+
+const Harness = h.Harness;
+const linux = std.os.linux;
+const posix = std.posix;
+
+const payload_len = 64;
+const wheel_events = 240;
+const j_events = 240;
+const ctrl_d_events = 60;
+const delete_line_input = "xxxxxxxxxxxxxxxxd";
+const delete_select_lines = delete_line_input.len - 1;
+const scroll_marker_line = wheel_events;
+const marker_wait_ms = 60_000;
+const scroll_quiet_ms = 1000;
+const large_stdlib_path = "/usr/lib/zig/std/Io/Threaded.zig";
+const target_ratio_x100 = 150;
+const movement_ratio_floor_us = 20_000;
+const startup_first_frame_target_us = 15_000;
+const startup_samples = 5;
+
+fn nowNs() u64 {
+ var ts: linux.timespec = undefined;
+ _ = linux.clock_gettime(linux.CLOCK.MONOTONIC, &ts);
+ return @intCast(ts.sec * 1_000_000_000 + ts.nsec);
+}
+
+fn fillPayload(buf: *[payload_len]u8, marker: []const u8) []const u8 {
+ const alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
+ for (buf, 0..) |*ch, i| ch.* = alphabet[(i * 7 + marker.len * 11) % alphabet.len];
+ @memcpy(buf[0..marker.len], marker);
+ return buf[0..marker.len];
+}
+
+fn waitRawGrowth(hs: *Harness, start_len: usize, ms: i64) !bool {
+ const deadline = nowNs() + @as(u64, @intCast(ms)) * 1_000_000;
+ var buf: [4096]u8 = undefined;
+ while (nowNs() < deadline) {
+ var fds = [_]posix.pollfd{.{ .fd = hs.master, .events = posix.POLL.IN, .revents = 0 }};
+ _ = posix.poll(&fds, 5) catch {};
+ if ((fds[0].revents & posix.POLL.IN) != 0) {
+ const n = posix.read(hs.master, &buf) catch break;
+ if (n == 0) break;
+ try hs.raw.appendSlice(hs.gpa, buf[0..n]);
+ hs.stream.nextSlice(buf[0..n]);
+ if (hs.raw.items.len > start_len) return true;
+ }
+ }
+ return hs.raw.items.len > start_len;
+}
+
+fn waitForOutputQuiet(hs: *Harness, quiet_ms: i64, max_ms: i64) !bool {
+ const deadline = nowNs() + @as(u64, @intCast(max_ms)) * 1_000_000;
+ var quiet_deadline = nowNs() + @as(u64, @intCast(quiet_ms)) * 1_000_000;
+ var buf: [4096]u8 = undefined;
+ while (nowNs() < deadline) {
+ if (nowNs() >= quiet_deadline) return true;
+ var fds = [_]posix.pollfd{.{ .fd = hs.master, .events = posix.POLL.IN, .revents = 0 }};
+ _ = posix.poll(&fds, 10) catch {};
+ if ((fds[0].revents & posix.POLL.IN) != 0) {
+ const n = posix.read(hs.master, &buf) catch break;
+ if (n == 0) break;
+ try hs.raw.appendSlice(hs.gpa, buf[0..n]);
+ hs.stream.nextSlice(buf[0..n]);
+ quiet_deadline = nowNs() + @as(u64, @intCast(quiet_ms)) * 1_000_000;
+ }
+ }
+ return false;
+}
+
+fn leftPaneTopLine(hs: *Harness) !usize {
+ const text = try hs.screenText();
+ defer hs.gpa.free(text);
+ var rows = std.mem.splitScalar(u8, text, '\n');
+ var row_idx: usize = 0;
+ while (rows.next()) |row| : (row_idx += 1) {
+ if (row_idx < 2) continue;
+ if (row_idx > 2) break;
+ const trimmed = std.mem.trimStart(u8, row, " ");
+ var end: usize = 0;
+ while (end < trimmed.len and std.ascii.isDigit(trimmed[end])) : (end += 1) {}
+ if (end > 0) return std.fmt.parseInt(usize, trimmed[0..end], 10) catch 0;
+ }
+ return 0;
+}
+
+fn waitForTopLineAbove(hs: *Harness, start_line: usize, ms: i64) !?usize {
+ const deadline = nowNs() + @as(u64, @intCast(ms)) * 1_000_000;
+ var buf: [4096]u8 = undefined;
+ while (nowNs() < deadline) {
+ var fds = [_]posix.pollfd{.{ .fd = hs.master, .events = posix.POLL.IN, .revents = 0 }};
+ _ = posix.poll(&fds, 10) catch {};
+ if ((fds[0].revents & posix.POLL.IN) != 0) {
+ const n = posix.read(hs.master, &buf) catch break;
+ if (n == 0) break;
+ try hs.raw.appendSlice(hs.gpa, buf[0..n]);
+ hs.stream.nextSlice(buf[0..n]);
+ }
+ const top_line = try leftPaneTopLine(hs);
+ if (top_line > start_line) return top_line;
+ }
+ return null;
+}
+
+fn waitForScreenMissing(hs: *Harness, needle: []const u8, ms: i64) !bool {
+ {
+ const text = try hs.screenText();
+ defer hs.gpa.free(text);
+ if (std.mem.indexOf(u8, text, needle) == null) return true;
+ }
+ const deadline = nowNs() + @as(u64, @intCast(ms)) * 1_000_000;
+ var buf: [4096]u8 = undefined;
+ while (nowNs() < deadline) {
+ var fds = [_]posix.pollfd{.{ .fd = hs.master, .events = posix.POLL.IN, .revents = 0 }};
+ _ = posix.poll(&fds, 10) catch {};
+ if ((fds[0].revents & posix.POLL.IN) != 0) {
+ const n = posix.read(hs.master, &buf) catch break;
+ if (n == 0) break;
+ try hs.raw.appendSlice(hs.gpa, buf[0..n]);
+ hs.stream.nextSlice(buf[0..n]);
+ }
+ const text = try hs.screenText();
+ defer hs.gpa.free(text);
+ if (std.mem.indexOf(u8, text, needle) == null) return true;
+ }
+ return false;
+}
+
+fn waitForScreenContainsPoll(hs: *Harness, needle: []const u8, ms: i64, poll_ms: i32) !bool {
+ {
+ const text = try hs.screenText();
+ defer hs.gpa.free(text);
+ if (std.mem.indexOf(u8, text, needle) != null) return true;
+ }
+ const deadline = nowNs() + @as(u64, @intCast(ms)) * 1_000_000;
+ var buf: [4096]u8 = undefined;
+ while (nowNs() < deadline) {
+ var fds = [_]posix.pollfd{.{ .fd = hs.master, .events = posix.POLL.IN, .revents = 0 }};
+ _ = posix.poll(&fds, poll_ms) catch {};
+ if ((fds[0].revents & posix.POLL.IN) != 0) {
+ const n = posix.read(hs.master, &buf) catch break;
+ if (n == 0) break;
+ try hs.raw.appendSlice(hs.gpa, buf[0..n]);
+ hs.stream.nextSlice(buf[0..n]);
+ }
+ const text = try hs.screenText();
+ defer hs.gpa.free(text);
+ if (std.mem.indexOf(u8, text, needle) != null) return true;
+ }
+ return false;
+}
+
+fn waitForRawContainsPoll(hs: *Harness, needle: []const u8, ms: i64, poll_ms: i32) !bool {
+ if (std.mem.indexOf(u8, hs.raw.items, needle) != null) return true;
+ const deadline = nowNs() + @as(u64, @intCast(ms)) * 1_000_000;
+ var buf: [4096]u8 = undefined;
+ while (nowNs() < deadline) {
+ var fds = [_]posix.pollfd{.{ .fd = hs.master, .events = posix.POLL.IN, .revents = 0 }};
+ _ = posix.poll(&fds, poll_ms) catch {};
+ if ((fds[0].revents & posix.POLL.IN) != 0) {
+ const n = posix.read(hs.master, &buf) catch break;
+ if (n == 0) break;
+ try hs.raw.appendSlice(hs.gpa, buf[0..n]);
+ hs.stream.nextSlice(buf[0..n]);
+ if (std.mem.indexOf(u8, hs.raw.items, needle) != null) return true;
+ }
+ }
+ return std.mem.indexOf(u8, hs.raw.items, needle) != null;
+}
+
+const startup_metric_prefix = "PARDES_STARTUP_US=";
+
+fn parseStartupMetric(raw: []const u8) ?u64 {
+ const start = std.mem.indexOf(u8, raw, startup_metric_prefix) orelse return null;
+ var i = start + startup_metric_prefix.len;
+ const digit_start = i;
+ while (i < raw.len and std.ascii.isDigit(raw[i])) : (i += 1) {}
+ if (i == digit_start) return null;
+ return std.fmt.parseInt(u64, raw[digit_start..i], 10) catch null;
+}
+
+fn waitForStartupMetric(hs: *Harness, ms: i64, poll_ms: i32) !?u64 {
+ if (parseStartupMetric(hs.raw.items)) |value| return value;
+ const deadline = nowNs() + @as(u64, @intCast(ms)) * 1_000_000;
+ var buf: [4096]u8 = undefined;
+ while (nowNs() < deadline) {
+ var fds = [_]posix.pollfd{.{ .fd = hs.master, .events = posix.POLL.IN, .revents = 0 }};
+ _ = posix.poll(&fds, poll_ms) catch {};
+ if ((fds[0].revents & posix.POLL.IN) != 0) {
+ const n = posix.read(hs.master, &buf) catch break;
+ if (n == 0) break;
+ try hs.raw.appendSlice(hs.gpa, buf[0..n]);
+ if (parseStartupMetric(hs.raw.items)) |value| return value;
+ }
+ }
+ return parseStartupMetric(hs.raw.items);
+}
+
+fn wheelDownBurst(gpa: std.mem.Allocator) ![]u8 {
+ var out: std.ArrayList(u8) = .empty;
+ errdefer out.deinit(gpa);
+ var i: usize = 0;
+ while (i < wheel_events) : (i += 1)
+ try out.appendSlice(gpa, "\x1b[<65;10;6M");
+ return out.toOwnedSlice(gpa);
+}
+
+const Movement = enum { wheel, j, ctrl_d };
+
+fn movementName(movement: Movement) []const u8 {
+ return switch (movement) {
+ .wheel => "wheel",
+ .j => "j",
+ .ctrl_d => "ctrl_d",
+ };
+}
+
+fn movementEventCount(movement: Movement) usize {
+ return switch (movement) {
+ .wheel => wheel_events,
+ .j => j_events,
+ .ctrl_d => ctrl_d_events,
+ };
+}
+
+fn movementInput(gpa: std.mem.Allocator, movement: Movement) ![]u8 {
+ if (movement == .wheel) return wheelDownBurst(gpa);
+ var out: std.ArrayList(u8) = .empty;
+ errdefer out.deinit(gpa);
+ var i: usize = 0;
+ const bytes = switch (movement) {
+ .wheel => unreachable,
+ .j => "j",
+ .ctrl_d => "\x04",
+ };
+ while (i < movementEventCount(movement)) : (i += 1)
+ try out.appendSlice(gpa, bytes);
+ return out.toOwnedSlice(gpa);
+}
+
+fn readFileAlloc(gpa: std.mem.Allocator, path: [*:0]const u8) ![]u8 {
+ const rc = linux.open(path, .{ .ACCMODE = .RDONLY }, 0);
+ const sfd: isize = @bitCast(rc);
+ if (sfd < 0) return error.OpenFailed;
+ const fd: c_int = @intCast(sfd);
+ defer _ = linux.close(fd);
+
+ var out: std.ArrayList(u8) = .empty;
+ errdefer out.deinit(gpa);
+ var buf: [16 * 1024]u8 = undefined;
+ while (true) {
+ const n = posix.read(fd, &buf) catch return error.ReadFailed;
+ if (n == 0) break;
+ try out.appendSlice(gpa, buf[0..n]);
+ }
+ return out.toOwnedSlice(gpa);
+}
+
+fn appendZigMarker(gpa: std.mem.Allocator, out: *std.ArrayList(u8), marker: []const u8) !void {
+ const line = try std.fmt.allocPrint(gpa, "const {s} = \"visible\";\n", .{marker});
+ defer gpa.free(line);
+ try out.appendSlice(gpa, line);
+}
+
+fn appendTextMarker(gpa: std.mem.Allocator, out: *std.ArrayList(u8), marker: []const u8) !void {
+ const line = try std.fmt.allocPrint(gpa, "{s} visible\n", .{marker});
+ defer gpa.free(line);
+ try out.appendSlice(gpa, line);
+}
+
+fn appendMarker(gpa: std.mem.Allocator, out: *std.ArrayList(u8), marker: []const u8, highlighted: bool) !void {
+ if (highlighted) {
+ try appendZigMarker(gpa, out, marker);
+ } else {
+ try appendTextMarker(gpa, out, marker);
+ }
+}
+
+fn makeSmallFixture(gpa: std.mem.Allocator, open_marker: []const u8, scroll_marker: []const u8, highlighted: bool) ![]u8 {
+ var out: std.ArrayList(u8) = .empty;
+ errdefer out.deinit(gpa);
+ try appendMarker(gpa, &out, open_marker, highlighted);
+ if (highlighted) {
+ try out.appendSlice(gpa, "const std = @import(\"std\");\n");
+ try out.appendSlice(gpa, "pub fn perf_entry() usize {\n");
+ try out.appendSlice(gpa, " return perf_func_0() + perf_func_199();\n");
+ try out.appendSlice(gpa, "}\n");
+ }
+ var line_no: usize = if (highlighted) 5 else 1;
+ var i: usize = 0;
+ while (line_no < scroll_marker_line) : ({
+ i += 1;
+ line_no += 1;
+ }) {
+ const line = if (highlighted)
+ try std.fmt.allocPrint(gpa, "fn perf_func_{d}() usize {{ return {d} + @as(usize, {d}); }}\n", .{ i, i, i % 17 })
+ else
+ try std.fmt.allocPrint(gpa, "plain perf line {d}: words numbers strings comments\n", .{i});
+ defer gpa.free(line);
+ try out.appendSlice(gpa, line);
+ }
+ try appendMarker(gpa, &out, scroll_marker, highlighted);
+ line_no += 1;
+ while (line_no < scroll_marker_line + 40) : ({
+ i += 1;
+ line_no += 1;
+ }) {
+ const line = if (highlighted)
+ try std.fmt.allocPrint(gpa, "fn perf_tail_{d}() usize {{ return {d}; }}\n", .{ i, i })
+ else
+ try std.fmt.allocPrint(gpa, "plain perf tail {d}: words numbers strings comments\n", .{i});
+ defer gpa.free(line);
+ try out.appendSlice(gpa, line);
+ }
+ return out.toOwnedSlice(gpa);
+}
+
+fn makeStdlibFixture(gpa: std.mem.Allocator, source: []const u8, open_marker: []const u8, scroll_marker: []const u8, highlighted: bool) ![]u8 {
+ var out: std.ArrayList(u8) = .empty;
+ errdefer out.deinit(gpa);
+ try appendMarker(gpa, &out, open_marker, highlighted);
+ var line_no: usize = 1;
+ var lines = std.mem.splitScalar(u8, source, '\n');
+ while (lines.next()) |line| {
+ if (line_no == scroll_marker_line) {
+ try appendMarker(gpa, &out, scroll_marker, highlighted);
+ line_no += 1;
+ }
+ try out.appendSlice(gpa, line);
+ try out.append(gpa, '\n');
+ line_no += 1;
+ }
+ while (line_no <= scroll_marker_line) : (line_no += 1) {
+ if (line_no == scroll_marker_line) {
+ try appendMarker(gpa, &out, scroll_marker, highlighted);
+ } else if (highlighted) {
+ const line = try std.fmt.allocPrint(gpa, "fn perf_padding_{d}() void {{}}\n", .{line_no});
+ defer gpa.free(line);
+ try out.appendSlice(gpa, line);
+ } else {
+ const line = try std.fmt.allocPrint(gpa, "plain stdlib padding {d}\n", .{line_no});
+ defer gpa.free(line);
+ try out.appendSlice(gpa, line);
+ }
+ }
+ return out.toOwnedSlice(gpa);
+}
+
+fn lineCount(content: []const u8) usize {
+ return std.mem.count(u8, content, "\n") + 1;
+}
+
+const Fixture = struct {
+ name: []const u8,
+ path: []const u8,
+ open_marker: []const u8,
+ edit_marker: []const u8,
+ scroll_marker: []const u8,
+ highlighted: bool,
+ bytes: usize,
+ lines: usize,
+};
+
+const BenchResult = struct {
+ startup_app_first_frame_us: u64,
+ startup_spawn_first_frame_us: u64,
+ startup_shell_ready_us: u64,
+ open_visible_us: u64,
+ open_repaint_us: ?u64,
+ edit_us: u64,
+ delete_line_us: u64,
+ undo_us: u64,
+ move_first_us: u64,
+ move_done_us: u64,
+ move_active_us: u64,
+ move_top_line: usize,
+};
+
+const StartupResult = struct {
+ internal_first_frame_us: u64,
+ harness_first_frame_us: u64,
+ spawn_first_frame_us: u64,
+};
+
+const BestOps = struct {
+ open_visible_us: u64 = std.math.maxInt(u64),
+ edit_us: u64 = std.math.maxInt(u64),
+ delete_line_us: u64 = std.math.maxInt(u64),
+ undo_us: u64 = std.math.maxInt(u64),
+
+ fn add(self: *BestOps, result: BenchResult) void {
+ self.open_visible_us = @min(self.open_visible_us, result.open_visible_us);
+ self.edit_us = @min(self.edit_us, result.edit_us);
+ self.delete_line_us = @min(self.delete_line_us, result.delete_line_us);
+ self.undo_us = @min(self.undo_us, result.undo_us);
+ }
+};
+
+const StartupStats = struct {
+ best_us: u64 = std.math.maxInt(u64),
+ worst_us: u64 = 0,
+ total_us: u64 = 0,
+ count: usize = 0,
+
+ fn add(self: *StartupStats, us: u64) void {
+ self.best_us = @min(self.best_us, us);
+ self.worst_us = @max(self.worst_us, us);
+ self.total_us += us;
+ self.count += 1;
+ }
+
+ fn avg(self: StartupStats) u64 {
+ if (self.count == 0) return 0;
+ return self.total_us / self.count;
+ }
+};
+
+fn runFixture(gpa: std.mem.Allocator, exe: [*:0]const u8, fixture: Fixture, movement: Movement) !BenchResult {
+ const startup_spawn_t0 = nowNs();
+ var hs = try Harness.init(gpa, exe, 30, 100);
+ defer hs.deinit();
+ const startup_app_t0 = nowNs();
+
+ if (!try waitForRawContainsPoll(&hs, "Kill Newcol", 8000, 1)) {
+ hs.dump("app first frame did not render before syntax perf");
+ return error.StartupFirstFrameTimeout;
+ }
+ const startup_app_first_frame_us = (nowNs() - startup_app_t0) / 1000;
+ const startup_spawn_first_frame_us = (nowNs() - startup_spawn_t0) / 1000;
+ try hs.expectWaitFor("tests.zig", 8000, "app never auto-ls'd before syntax perf");
+ const startup_shell_ready_us = (nowNs() - startup_spawn_t0) / 1000;
+ try hs.send("\x02"); // normal -> tty
+ try hs.pump(500);
+ {
+ var cmd_buf: [256]u8 = undefined;
+ const cmd = try std.fmt.bufPrint(&cmd_buf, "clear; echo {s}\r", .{fixture.path});
+ try hs.send(cmd);
+ }
+ try hs.expectWaitFor(fixture.path, 8000, "perf path did not render in tty");
+ try hs.send("\x02"); // tty -> normal
+ try hs.expectWaitFor("nm /", 6000, "failed to leave tty before syntax perf open");
+
+ const open_t0 = nowNs();
+ try hs.send("\x1b[<2;8;3M\x1b[<2;8;3m"); // right-click body row 0 path
+ try hs.expectWaitFor(fixture.open_marker, marker_wait_ms, "syntax perf file did not open");
+ const open_visible_us = (nowNs() - open_t0) / 1000;
+ var open_repaint_us: ?u64 = null;
+ if (fixture.highlighted) {
+ open_repaint_us = open_visible_us;
+ }
+ if (!try waitForOutputQuiet(&hs, 250, marker_wait_ms)) {
+ hs.dump("syntax perf open did not go quiet");
+ return error.OpenQuietTimeout;
+ }
+
+ try hs.send("i");
+ try hs.expectWaitFor("in /tmp/pardes_perf_", 5000, "syntax perf did not enter file insert mode");
+
+ var payload: [payload_len]u8 = undefined;
+ const edit_marker = fillPayload(&payload, fixture.edit_marker);
+ const edit_t0 = nowNs();
+ try hs.send(&payload);
+ try hs.expectWaitFor(edit_marker, marker_wait_ms, "syntax perf edit marker did not appear");
+ const edit_us = (nowNs() - edit_t0) / 1000;
+
+ try hs.send("\x1b"); // insert -> normal
+ try hs.expectWaitFor("nm /tmp/pardes_perf_", 5000, "syntax perf did not leave insert mode");
+ if (!try waitForOutputQuiet(&hs, 250, marker_wait_ms)) {
+ hs.dump("syntax perf edit repaint did not go quiet");
+ return error.EditRepaintQuietTimeout;
+ }
+
+ const delete_t0 = nowNs();
+ try hs.send(delete_line_input);
+ if (!try waitForScreenMissing(&hs, edit_marker, marker_wait_ms)) {
+ hs.dump("syntax perf line delete marker did not disappear");
+ return error.DeleteLineTimeout;
+ }
+ const delete_line_us = (nowNs() - delete_t0) / 1000;
+ if (!try waitForOutputQuiet(&hs, 250, marker_wait_ms)) {
+ hs.dump("syntax perf line delete repaint did not go quiet");
+ return error.DeleteLineQuietTimeout;
+ }
+
+ const undo_t0 = nowNs();
+ try hs.send("u");
+ try hs.expectWaitFor(edit_marker, marker_wait_ms, "syntax perf undo did not restore edit marker");
+ const undo_us = (nowNs() - undo_t0) / 1000;
+ if (!try waitForOutputQuiet(&hs, 250, marker_wait_ms)) {
+ hs.dump("syntax perf undo repaint did not go quiet");
+ return error.UndoQuietTimeout;
+ }
+
+ const move_input = try movementInput(gpa, movement);
+ defer gpa.free(move_input);
+
+ const start_top_line = @max(try leftPaneTopLine(&hs), 1);
+ const move_t0 = nowNs();
+ try hs.send(move_input);
+ if (try waitForTopLineAbove(&hs, start_top_line, marker_wait_ms)) |_| {
+ // Progress observed; keep draining below until the movement settles.
+ } else {
+ hs.dump("syntax perf movement did not move the visible file window");
+ return error.MovementDidNotMove;
+ }
+ const move_first_us = (nowNs() - move_t0) / 1000;
+ if (!try waitForOutputQuiet(&hs, scroll_quiet_ms, marker_wait_ms)) {
+ hs.dump("syntax perf movement did not go quiet");
+ return error.MovementQuietTimeout;
+ }
+ const move_done_us = (nowNs() - move_t0) / 1000;
+ const move_top_line = try leftPaneTopLine(&hs);
+
+ return .{
+ .startup_app_first_frame_us = startup_app_first_frame_us,
+ .startup_spawn_first_frame_us = startup_spawn_first_frame_us,
+ .startup_shell_ready_us = startup_shell_ready_us,
+ .open_visible_us = open_visible_us,
+ .open_repaint_us = open_repaint_us,
+ .edit_us = edit_us,
+ .delete_line_us = delete_line_us,
+ .undo_us = undo_us,
+ .move_first_us = move_first_us,
+ .move_done_us = move_done_us,
+ .move_active_us = move_done_us -| (@as(u64, scroll_quiet_ms) * 1000),
+ .move_top_line = move_top_line,
+ };
+}
+
+fn runStartup(gpa: std.mem.Allocator, exe: [*:0]const u8) !StartupResult {
+ const startup_spawn_t0 = nowNs();
+ var hs = try Harness.initArgs(gpa, exe, 30, 100, "--perf-startup");
+ defer hs.deinit();
+ const startup_app_t0 = nowNs();
+
+ const internal_first_frame_us = (try waitForStartupMetric(&hs, 8000, 1)) orelse {
+ hs.dump("app first frame did not render before startup perf");
+ return error.StartupFirstFrameTimeout;
+ };
+ if (!try waitForRawContainsPoll(&hs, "Kill Newcol", 8000, 1)) {
+ hs.dump("app first frame marker rendered without topbar text");
+ return error.StartupFirstFrameTimeout;
+ }
+ return .{
+ .internal_first_frame_us = internal_first_frame_us,
+ .harness_first_frame_us = (nowNs() - startup_app_t0) / 1000,
+ .spawn_first_frame_us = (nowNs() - startup_spawn_t0) / 1000,
+ };
+}
+
+fn printResult(fixture: Fixture, movement: Movement, result: BenchResult) void {
+ std.debug.print(
+ "case={s} movement={s} highlight={d} bytes={d} lines={d} startup_app_first_frame_us={d} startup_spawn_first_frame_us={d} startup_shell_ready_us={d} open_visible_us={d} open_repaint_us={d} edit_us={d} edit_us_per_char={d} delete_line_us={d} undo_us={d} move_first_us={d} move_done_us={d} move_active_us={d} move_top_line={d}\n",
+ .{
+ fixture.name,
+ movementName(movement),
+ @intFromBool(fixture.highlighted),
+ fixture.bytes,
+ fixture.lines,
+ result.startup_app_first_frame_us,
+ result.startup_spawn_first_frame_us,
+ result.startup_shell_ready_us,
+ result.open_visible_us,
+ result.open_repaint_us orelse 0,
+ result.edit_us,
+ result.edit_us / payload_len,
+ result.delete_line_us,
+ result.undo_us,
+ result.move_first_us,
+ result.move_done_us,
+ result.move_active_us,
+ result.move_top_line,
+ },
+ );
+}
+
+fn ratioX100(highlight_us: u64, plain_us: u64) u64 {
+ if (plain_us == 0) return 0;
+ return (highlight_us * 100 + plain_us / 2) / plain_us;
+}
+
+fn movementRatioX100(highlight_us: u64, plain_us: u64) u64 {
+ return ratioX100(highlight_us, @max(plain_us, movement_ratio_floor_us));
+}
+
+fn printRatioSummary(movement: Movement, small_txt: BenchResult, small_zig: BenchResult, large_txt: BenchResult, large_zig: BenchResult) void {
+ std.debug.print(
+ "ratio movement={s} small_move_active_x100={d} large_move_active_x100={d} target_x100={d} small_open_visible_x100={d} large_open_visible_x100={d} small_edit_x100={d} large_edit_x100={d} small_delete_line_x100={d} large_delete_line_x100={d} small_undo_x100={d} large_undo_x100={d}\n",
+ .{
+ movementName(movement),
+ movementRatioX100(small_zig.move_active_us, small_txt.move_active_us),
+ movementRatioX100(large_zig.move_active_us, large_txt.move_active_us),
+ target_ratio_x100,
+ ratioX100(small_zig.open_visible_us, small_txt.open_visible_us),
+ ratioX100(large_zig.open_visible_us, large_txt.open_visible_us),
+ ratioX100(small_zig.edit_us, small_txt.edit_us),
+ ratioX100(large_zig.edit_us, large_txt.edit_us),
+ ratioX100(small_zig.delete_line_us, small_txt.delete_line_us),
+ ratioX100(large_zig.delete_line_us, large_txt.delete_line_us),
+ ratioX100(small_zig.undo_us, small_txt.undo_us),
+ ratioX100(large_zig.undo_us, large_txt.undo_us),
+ },
+ );
+}
+
+fn checkMovementRatio(movement: Movement, size_name: []const u8, highlighted_us: u64, plain_us: u64) !void {
+ const ratio = movementRatioX100(highlighted_us, plain_us);
+ if (ratio > target_ratio_x100) {
+ std.debug.print(
+ "FAIL movement={s} size={s} move_active_x100={d} target_x100={d} highlighted_us={d} plain_us={d}\n",
+ .{ movementName(movement), size_name, ratio, target_ratio_x100, highlighted_us, plain_us },
+ );
+ return error.HighlightMovementPerfRegression;
+ }
+}
+
+fn checkMovementRatios(movement: Movement, small_txt: BenchResult, small_zig: BenchResult, large_txt: BenchResult, large_zig: BenchResult) !void {
+ try checkMovementRatio(movement, "small", small_zig.move_active_us, small_txt.move_active_us);
+ try checkMovementRatio(movement, "large", large_zig.move_active_us, large_txt.move_active_us);
+}
+
+fn printBestOpsRatioSummary(best: [4]BestOps) void {
+ std.debug.print(
+ "ratio aggregate=best_of_movements target_x100={d} small_open_visible_x100={d} large_open_visible_x100={d} small_edit_x100={d} large_edit_x100={d} small_delete_line_x100={d} large_delete_line_x100={d} small_undo_x100={d} large_undo_x100={d}\n",
+ .{
+ target_ratio_x100,
+ ratioX100(best[1].open_visible_us, best[0].open_visible_us),
+ ratioX100(best[3].open_visible_us, best[2].open_visible_us),
+ ratioX100(best[1].edit_us, best[0].edit_us),
+ ratioX100(best[3].edit_us, best[2].edit_us),
+ ratioX100(best[1].delete_line_us, best[0].delete_line_us),
+ ratioX100(best[3].delete_line_us, best[2].delete_line_us),
+ ratioX100(best[1].undo_us, best[0].undo_us),
+ ratioX100(best[3].undo_us, best[2].undo_us),
+ },
+ );
+}
+
+fn printStartupSummary(name: []const u8, target_us: ?u64, stats: StartupStats) void {
+ if (target_us) |target| {
+ std.debug.print(
+ "startup summary metric={s} target_us={d} samples={d} best_us={d} avg_us={d} worst_us={d}\n",
+ .{ name, target, stats.count, stats.best_us, stats.avg(), stats.worst_us },
+ );
+ return;
+ }
+ std.debug.print(
+ "startup summary metric={s} samples={d} best_us={d} avg_us={d} worst_us={d}\n",
+ .{ name, stats.count, stats.best_us, stats.avg(), stats.worst_us },
+ );
+}
+
+fn checkStartupAverage(name: []const u8, target_us: u64, stats: StartupStats) !void {
+ const avg_us = stats.avg();
+ if (avg_us <= target_us) return;
+ std.debug.print(
+ "FAIL startup metric={s} avg_us={d} target_us={d} samples={d} best_us={d} worst_us={d}\n",
+ .{ name, avg_us, target_us, stats.count, stats.best_us, stats.worst_us },
+ );
+ return error.StartupPerfRegression;
+}
+
+pub fn main(init: std.process.Init) !void {
+ const gpa = init.gpa;
+ const args = try init.minimal.args.toSlice(init.arena.allocator());
+ if (args.len != 2) {
+ std.debug.print("usage: {s} <pardes-exe>\n", .{args[0]});
+ return error.BadArgs;
+ }
+
+ const small_txt_path = "/tmp/pardes_perf_small_plain.txt";
+ const small_zig_path = "/tmp/pardes_perf_small_code.zig";
+ const large_txt_path = "/tmp/pardes_perf_large_plain.txt";
+ const large_zig_path = "/tmp/pardes_perf_large_code.zig";
+
+ const stdlib_content = try readFileAlloc(gpa, large_stdlib_path);
+ defer gpa.free(stdlib_content);
+ const small_txt_content = try makeSmallFixture(gpa, "SMALLTXTOPENPERFEND", "SMALLTXTSCROLLPERFEND", false);
+ defer gpa.free(small_txt_content);
+ const small_zig_content = try makeSmallFixture(gpa, "SMALLZIGOPENPERFEND", "SMALLZIGSCROLLPERFEND", true);
+ defer gpa.free(small_zig_content);
+ const large_txt_content = try makeStdlibFixture(gpa, stdlib_content, "LARGETXTOPENPERFEND", "LARGETXTSCROLLPERFEND", false);
+ defer gpa.free(large_txt_content);
+ const large_zig_content = try makeStdlibFixture(gpa, stdlib_content, "LARGEZIGOPENPERFEND", "LARGEZIGSCROLLPERFEND", true);
+ defer gpa.free(large_zig_content);
+
+ try h.writeFile(small_txt_path, small_txt_content);
+ try h.writeFile(small_zig_path, small_zig_content);
+ try h.writeFile(large_txt_path, large_txt_content);
+ try h.writeFile(large_zig_path, large_zig_content);
+
+ const fixtures = [_]Fixture{
+ .{
+ .name = "small_txt",
+ .path = small_txt_path,
+ .open_marker = "SMALLTXTOPENPERFEND",
+ .edit_marker = "SMALLTXTEDITPERFEND",
+ .scroll_marker = "SMALLTXTSCROLLPERFEND",
+ .highlighted = false,
+ .bytes = small_txt_content.len,
+ .lines = lineCount(small_txt_content),
+ },
+ .{
+ .name = "small_zig",
+ .path = small_zig_path,
+ .open_marker = "SMALLZIGOPENPERFEND",
+ .edit_marker = "SMALLZIGEDITPERFEND",
+ .scroll_marker = "SMALLZIGSCROLLPERFEND",
+ .highlighted = true,
+ .bytes = small_zig_content.len,
+ .lines = lineCount(small_zig_content),
+ },
+ .{
+ .name = "large_txt",
+ .path = large_txt_path,
+ .open_marker = "LARGETXTOPENPERFEND",
+ .edit_marker = "LARGETXTEDITPERFEND",
+ .scroll_marker = "LARGETXTSCROLLPERFEND",
+ .highlighted = false,
+ .bytes = large_txt_content.len,
+ .lines = lineCount(large_txt_content),
+ },
+ .{
+ .name = "large_zig",
+ .path = large_zig_path,
+ .open_marker = "LARGEZIGOPENPERFEND",
+ .edit_marker = "LARGEZIGEDITPERFEND",
+ .scroll_marker = "LARGEZIGSCROLLPERFEND",
+ .highlighted = true,
+ .bytes = large_zig_content.len,
+ .lines = lineCount(large_zig_content),
+ },
+ };
+
+ std.debug.print(
+ "syntax-perf backend=vaxis chars_per_edit={d} delete_select_lines={d} wheel_events={d} j_events={d} ctrl_d_events={d} scroll_quiet_ms={d} scroll_marker_line={d} large_stdlib={s}\n",
+ .{ payload_len, delete_select_lines, wheel_events, j_events, ctrl_d_events, scroll_quiet_ms, scroll_marker_line, large_stdlib_path },
+ );
+ var startup_only_internal_stats: StartupStats = .{};
+ var startup_only_harness_stats: StartupStats = .{};
+ var startup_only_spawn_stats: StartupStats = .{};
+ for (0..startup_samples) |sample| {
+ const startup = try runStartup(gpa, args[1].ptr);
+ startup_only_internal_stats.add(startup.internal_first_frame_us);
+ startup_only_harness_stats.add(startup.harness_first_frame_us);
+ startup_only_spawn_stats.add(startup.spawn_first_frame_us);
+ std.debug.print(
+ "startup_sample={d} internal_first_frame_us={d} harness_first_frame_us={d} spawn_first_frame_us={d}\n",
+ .{ sample, startup.internal_first_frame_us, startup.harness_first_frame_us, startup.spawn_first_frame_us },
+ );
+ }
+ printStartupSummary("startup_only_internal_first_frame", null, startup_only_internal_stats);
+ printStartupSummary("startup_only_harness_first_frame", startup_first_frame_target_us, startup_only_harness_stats);
+ printStartupSummary("startup_only_spawn_first_frame", null, startup_only_spawn_stats);
+ try checkStartupAverage("startup_only_harness_first_frame", startup_first_frame_target_us, startup_only_harness_stats);
+
+ const movements = [_]Movement{ .wheel, .j, .ctrl_d };
+ var best_ops: [4]BestOps = .{ .{}, .{}, .{}, .{} };
+ var startup_app_first_frame_stats: StartupStats = .{};
+ var startup_spawn_first_frame_stats: StartupStats = .{};
+ var startup_shell_ready_stats: StartupStats = .{};
+ for (movements) |movement| {
+ const small_txt = try runFixture(gpa, args[1].ptr, fixtures[0], movement);
+ startup_app_first_frame_stats.add(small_txt.startup_app_first_frame_us);
+ startup_spawn_first_frame_stats.add(small_txt.startup_spawn_first_frame_us);
+ startup_shell_ready_stats.add(small_txt.startup_shell_ready_us);
+ best_ops[0].add(small_txt);
+ printResult(fixtures[0], movement, small_txt);
+ const small_zig = try runFixture(gpa, args[1].ptr, fixtures[1], movement);
+ startup_app_first_frame_stats.add(small_zig.startup_app_first_frame_us);
+ startup_spawn_first_frame_stats.add(small_zig.startup_spawn_first_frame_us);
+ startup_shell_ready_stats.add(small_zig.startup_shell_ready_us);
+ best_ops[1].add(small_zig);
+ printResult(fixtures[1], movement, small_zig);
+ const large_txt = try runFixture(gpa, args[1].ptr, fixtures[2], movement);
+ startup_app_first_frame_stats.add(large_txt.startup_app_first_frame_us);
+ startup_spawn_first_frame_stats.add(large_txt.startup_spawn_first_frame_us);
+ startup_shell_ready_stats.add(large_txt.startup_shell_ready_us);
+ best_ops[2].add(large_txt);
+ printResult(fixtures[2], movement, large_txt);
+ const large_zig = try runFixture(gpa, args[1].ptr, fixtures[3], movement);
+ startup_app_first_frame_stats.add(large_zig.startup_app_first_frame_us);
+ startup_spawn_first_frame_stats.add(large_zig.startup_spawn_first_frame_us);
+ startup_shell_ready_stats.add(large_zig.startup_shell_ready_us);
+ best_ops[3].add(large_zig);
+ printResult(fixtures[3], movement, large_zig);
+ printRatioSummary(movement, small_txt, small_zig, large_txt, large_zig);
+ try checkMovementRatios(movement, small_txt, small_zig, large_txt, large_zig);
+ }
+ printBestOpsRatioSummary(best_ops);
+ printStartupSummary("app_first_frame", null, startup_app_first_frame_stats);
+ printStartupSummary("spawn_first_frame", null, startup_spawn_first_frame_stats);
+ printStartupSummary("shell_ready", null, startup_shell_ready_stats);
+}