diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-29 13:19:08 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:16 -0300 |
| commit | 4e224b7c7e67fafcbe7359fcf534048e254b701d (patch) | |
| tree | 33539e34e8cee525792767facebf36356bc28b08 /test | |
| parent | 7ec97214a7a6ee905167b7f94a800a8c1d70b100 (diff) | |
| download | pardes-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')
| -rw-r--r-- | test/perf-baseline-gui-Debug.json | 1 | ||||
| -rw-r--r-- | test/perf-baseline-tty-Debug.json | 1 | ||||
| -rw-r--r-- | test/perf-baseline-tty-ReleaseFast.json | 1 | ||||
| -rw-r--r-- | test/perf.zig | 52 |
4 files changed, 51 insertions, 4 deletions
diff --git a/test/perf-baseline-gui-Debug.json b/test/perf-baseline-gui-Debug.json new file mode 100644 index 00000000..fd64ebf9 --- /dev/null +++ b/test/perf-baseline-gui-Debug.json @@ -0,0 +1 @@ +{"harness":"67f85bf9692f1778","build":{"compiler":"0.16.0","target":"x86_64-linux.5.10...6.19-gnu.2.38","cpu":"x86_64","driver_optimize":"ReleaseFast","core_optimize":"Debug","c_optimize":"ReleaseFast","terminal_optimize":"ReleaseSafe","platform":"gui","grammars":"full","mupdf":true,"jpx":true,"tracy":false,"quic":false,"theme_animation":true,"terminal_simd":true,"prebuilt_shaders":false},"cols":120,"rows":40,"reps":10,"only":"medium","fixtures":[{"name":"small","lines":1000,"cols":60,"bytes":62694},{"name":"medium","lines":50000,"cols":60,"bytes":3201944},{"name":"large","lines":300000,"cols":60,"bytes":19464444},{"name":"longline","lines":400,"cols":8000,"bytes":3200400},{"name":"esp32p4","lines":12,"cols":240,"bytes":2892}],"cells":[{"op":"open","fixture":"medium","min_us":1768,"med_us":2025,"p90_us":2122,"max_us":2125},{"op":"render","fixture":"medium","min_us":72,"med_us":75,"p90_us":76,"max_us":83},{"op":"key-down","fixture":"medium","min_us":73,"med_us":76,"p90_us":80,"max_us":84},{"op":"key-right","fixture":"medium","min_us":74,"med_us":77,"p90_us":81,"max_us":84},{"op":"page-down","fixture":"medium","min_us":73,"med_us":74,"p90_us":77,"max_us":79},{"op":"wheel","fixture":"medium","min_us":71,"med_us":75,"p90_us":81,"max_us":81},{"op":"edit-char","fixture":"medium","min_us":2413,"med_us":2674,"p90_us":2943,"max_us":3111}]} diff --git a/test/perf-baseline-tty-Debug.json b/test/perf-baseline-tty-Debug.json new file mode 100644 index 00000000..682ea028 --- /dev/null +++ b/test/perf-baseline-tty-Debug.json @@ -0,0 +1 @@ +{"harness":"67f85bf9692f1778","build":{"compiler":"0.16.0","target":"x86_64-linux.5.10...6.19-gnu.2.38","cpu":"x86_64","driver_optimize":"ReleaseFast","core_optimize":"Debug","c_optimize":"ReleaseFast","terminal_optimize":"ReleaseSafe","platform":"tty","grammars":"full","mupdf":true,"jpx":true,"tracy":false,"quic":false,"theme_animation":true,"terminal_simd":true,"prebuilt_shaders":false},"cols":120,"rows":40,"reps":10,"only":"medium","fixtures":[{"name":"small","lines":1000,"cols":60,"bytes":62694},{"name":"medium","lines":50000,"cols":60,"bytes":3201944},{"name":"large","lines":300000,"cols":60,"bytes":19464444},{"name":"longline","lines":400,"cols":8000,"bytes":3200400},{"name":"esp32p4","lines":12,"cols":240,"bytes":2892}],"cells":[{"op":"open","fixture":"medium","min_us":1944,"med_us":2130,"p90_us":2232,"max_us":2394},{"op":"render","fixture":"medium","min_us":71,"med_us":75,"p90_us":76,"max_us":91},{"op":"key-down","fixture":"medium","min_us":72,"med_us":79,"p90_us":93,"max_us":102},{"op":"key-right","fixture":"medium","min_us":73,"med_us":76,"p90_us":78,"max_us":81},{"op":"page-down","fixture":"medium","min_us":72,"med_us":73,"p90_us":76,"max_us":76},{"op":"wheel","fixture":"medium","min_us":71,"med_us":85,"p90_us":96,"max_us":102},{"op":"edit-char","fixture":"medium","min_us":2612,"med_us":3112,"p90_us":3238,"max_us":3618}]} diff --git a/test/perf-baseline-tty-ReleaseFast.json b/test/perf-baseline-tty-ReleaseFast.json new file mode 100644 index 00000000..4fd96d92 --- /dev/null +++ b/test/perf-baseline-tty-ReleaseFast.json @@ -0,0 +1 @@ +{"harness":"67f85bf9692f1778","build":{"compiler":"0.16.0","target":"x86_64-linux.5.10...6.19-gnu.2.38","cpu":"x86_64","driver_optimize":"ReleaseFast","core_optimize":"ReleaseFast","c_optimize":"ReleaseFast","terminal_optimize":"ReleaseFast","platform":"tty","grammars":"full","mupdf":true,"jpx":true,"tracy":false,"quic":false,"theme_animation":true,"terminal_simd":true,"prebuilt_shaders":false},"cols":120,"rows":40,"reps":10,"only":"medium","fixtures":[{"name":"small","lines":1000,"cols":60,"bytes":62694},{"name":"medium","lines":50000,"cols":60,"bytes":3201944},{"name":"large","lines":300000,"cols":60,"bytes":19464444},{"name":"longline","lines":400,"cols":8000,"bytes":3200400},{"name":"esp32p4","lines":12,"cols":240,"bytes":2892}],"cells":[{"op":"open","fixture":"medium","min_us":1971,"med_us":2259,"p90_us":2308,"max_us":2339},{"op":"render","fixture":"medium","min_us":71,"med_us":75,"p90_us":78,"max_us":79},{"op":"key-down","fixture":"medium","min_us":73,"med_us":77,"p90_us":79,"max_us":95},{"op":"key-right","fixture":"medium","min_us":72,"med_us":77,"p90_us":80,"max_us":88},{"op":"page-down","fixture":"medium","min_us":72,"med_us":76,"p90_us":81,"max_us":83},{"op":"wheel","fixture":"medium","min_us":71,"med_us":75,"p90_us":80,"max_us":91},{"op":"edit-char","fixture":"medium","min_us":2662,"med_us":2999,"p90_us":3375,"max_us":3622}]} 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); |
