summaryrefslogtreecommitdiff
path: root/test/perf.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-29 13:19:08 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:16 -0300
commit4e224b7c7e67fafcbe7359fcf534048e254b701d (patch)
tree33539e34e8cee525792767facebf36356bc28b08 /test/perf.zig
parent7ec97214a7a6ee905167b7f94a800a8c1d70b100 (diff)
downloadpardes-4e224b7c7e67fafcbe7359fcf534048e254b701d.tar.gz
pardes-4e224b7c7e67fafcbe7359fcf534048e254b701d.zip
The theme's chrome is worked out once, not per grapheme; still post passes let the GUI rest; a perf gate
somtrmsz's contrast floors made ChromeTheme.fromTheme run its focus-tint and separator searches (pow calls each), and Output's RowDecoration.styleAt called it for every grapheme of every highlighted row: the 50k-line file's render went from 0.11 ms to 13 ms. Pardes.bodyChrome now keeps the theme's chrome, worked out again only when the theme differs; recolorSyntax asks for it once a decorated row, and a plain row (nearly every row of a file) never asks. ReleaseFast, medium fixture, median us, before -> after (main): render 13150 -> 76 (111), key-down 14658 -> 79 (110), wheel 13589 -> 76 (109), open 14891 -> 2095 (2055), edit-char 16703 -> 3006 (1251; the rest of that gap is editing and tree-sitter, not this). Post.animating asked for frames whenever the window had focus and any pass was ready, so a still pass kept the GUI drawing at the display's rate (Bloom: 49% of a core idle, in a hidden test window). A pass now says whether it moves on its own: the CRT (its hum and dither) and a Shadertoy file whose source reads iTime, iFrame or iDate (shader_build.readsTime; the flag rides the wire's post message); Bloom, Vignette and Grain are still. Bloom idle: 49% -> 1.2%, as with no pass. zig build perf-gate: the 50k-line file's gestures, each's fastest sample within 3x of the recorded baseline's (test/perf-baseline-<platform>- <optimize>.json, recorded from this build), run with every unit-test. On the regressed code it fails at 180x for render.
Diffstat (limited to 'test/perf.zig')
-rw-r--r--test/perf.zig52
1 files changed, 48 insertions, 4 deletions
diff --git a/test/perf.zig b/test/perf.zig
index 8eec1ed0..e244091c 100644
--- a/test/perf.zig
+++ b/test/perf.zig
@@ -323,6 +323,9 @@ const Options = struct {
mini: bool = false,
terminal_mib: ?usize = null,
self_test: bool = false,
+ /// A gate: fail when a cell's fastest sample is more than this many
+ /// times the baseline's fastest (--base's).
+ max_ratio: ?f64 = null,
fn parse(args: []const []const u8) !Options {
var opts: Options = .{};
@@ -330,7 +333,7 @@ const Options = struct {
while (i < args.len) : (i += 1) {
const arg = args[i];
if (!std.mem.startsWith(u8, arg, "--")) return error.UnknownOption;
- const option = std.meta.stringToEnum(enum { json, creation, startup, mini, @"terminal-mib", @"self-test", reps, cols, rows, base, only }, arg[2..]) orelse
+ const option = std.meta.stringToEnum(enum { json, creation, startup, mini, @"terminal-mib", @"self-test", reps, cols, rows, base, only, @"max-ratio" }, arg[2..]) orelse
return error.UnknownOption;
switch (option) {
.json => opts.json = true,
@@ -349,6 +352,7 @@ const Options = struct {
.base => opts.base = value,
.only => opts.only = value,
.@"terminal-mib" => opts.terminal_mib = std.fmt.parseInt(usize, value, 10) catch return error.InvalidNumber,
+ .@"max-ratio" => opts.max_ratio = std.fmt.parseFloat(f64, value) catch return error.InvalidNumber,
else => unreachable,
}
},
@@ -363,6 +367,10 @@ const Options = struct {
if (opts.creation or opts.only != null or opts.base != null) return error.IncompatibleOptions;
}
if (opts.base) |path| if (path.len == 0) return error.MissingValue;
+ if (opts.max_ratio) |ratio| {
+ if (opts.base == null) return error.IncompatibleOptions;
+ if (!(ratio >= 1)) return error.InvalidMeasurement;
+ }
if (opts.mini and (opts.creation or opts.terminal_mib != null or opts.only != null or opts.base != null))
return error.IncompatibleOptions;
if (opts.only) |name| {
@@ -431,8 +439,28 @@ pub fn main(init: std.process.Init) !void {
}
}
- if (opts.json) return reportJson(init.io, &cells, &term_cells, &bytes, opts);
- reportText(&cells, &term_cells, &bytes, reps, opts.base, base);
+ if (opts.json) try reportJson(init.io, &cells, &term_cells, &bytes, opts) else reportText(&cells, &term_cells, &bytes, reps, opts.base, base);
+ if (opts.max_ratio) |ratio| try gate(&cells, &base.?, ratio);
+}
+
+/// The performance gate (zig build perf-gate): every measured cell's
+/// fastest sample against the baseline's fastest. The fastest, not the
+/// median: a loaded machine slows the median, rarely the best of a run,
+/// while a regression slows every sample.
+fn gate(cells: *const [std.enums.values(Op).len][fixtures.len]Cell, base: *const Base, ratio: f64) !void {
+ var failed = false;
+ for (std.enums.values(Op), 0..) |op, oi| for (fixtures, 0..) |fx, fi| {
+ const then = base.min[oi][fi];
+ const now = cells[oi][fi].min_us;
+ if (then == 0 or now == 0) continue;
+ const x = @as(f64, @floatFromInt(now)) / @as(f64, @floatFromInt(then));
+ if (x > ratio) {
+ std.debug.print("perf gate: {s} on {s} took {d} us at best, {d:.1}x the baseline's {d} us (limit {d:.1}x)\n", .{ op.label(), fx.name, now, x, then, ratio });
+ failed = true;
+ }
+ };
+ if (failed) return error.PerformanceRegression;
+ std.debug.print("perf gate: every cell within {d:.1}x of the baseline\n", .{ratio});
}
fn measure(op: Op, fx: Fixture, path: []const u8, expected_bytes: usize, reps: usize) !Cell {
@@ -1378,6 +1406,7 @@ fn writeJson(
const Base = struct {
med: [std.enums.values(Op).len][fixtures.len]u64,
+ min: [std.enums.values(Op).len][fixtures.len]u64 = @splat(@splat(0)),
term_med: [std.enums.values(TermOp).len][term_fixtures.len]u64,
};
@@ -1443,6 +1472,7 @@ fn parseBase(src: []const u8, opts: Options, bytes: *const [fixtures.len]usize)
if (file_seen[oi][index]) return error.BaselineCoverage;
file_seen[oi][index] = true;
base.med[oi][index] = cell.med_us;
+ base.min[oi][index] = cell.min_us;
} else {
if (opts.only != null) return error.BaselineCoverage;
const ti = for (term_fixtures, 0..) |fixture, index| {
@@ -1487,8 +1517,22 @@ fn selfTest() !void {
&.{ "--terminal-mib", "1", "--only", "small" }, &.{ "--terminal-mib", "1", "--creation" },
&.{ "--terminal-mib", "1", "--base", "old.json" }, &.{ "--mini", "--creation" },
&.{ "--mini", "--terminal-mib", "1" }, &.{ "--mini", "--base", "old.json" },
- &.{ "--mini", "--only", "small" },
+ &.{ "--mini", "--only", "small" }, &.{ "--max-ratio", "3" },
}) |args| try testing.expectError(error.IncompatibleOptions, Options.parse(args));
+ try testing.expectError(error.InvalidMeasurement, Options.parse(&.{ "--base", "old.json", "--max-ratio", "0.5" }));
+ const gated = try Options.parse(&.{ "--only", "medium", "--base", "old.json", "--max-ratio", "3" });
+ try testing.expectEqual(@as(f64, 3), gated.max_ratio.?);
+ {
+ // A cell three and a half times its baseline's best fails; the
+ // rest pass.
+ var cells: [std.enums.values(Op).len][fixtures.len]Cell = @splat(@splat(.{}));
+ var base: Base = .{ .med = @splat(@splat(0)), .term_med = @splat(@splat(0)) };
+ base.min[1][1] = 100;
+ cells[1][1] = .{ .min_us = 290, .med_us = 300, .p90_us = 300, .max_us = 300 };
+ try gate(&cells, &base, 3);
+ cells[1][1].min_us = 350;
+ try testing.expectError(error.PerformanceRegression, gate(&cells, &base, 3));
+ }
const selected = try Options.parse(&.{ "--only", "small", "--reps", "2", "--cols", "80", "--rows", "24" });
try testing.expectEqual(@as(usize, 2), selected.reps);
try testing.expectEqual(@as(u16, 80), selected.cols);