diff options
Diffstat (limited to 'tests_syntax_perf.zig')
| -rw-r--r-- | tests_syntax_perf.zig | 795 |
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); +} |
