summaryrefslogtreecommitdiff
path: root/test
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-08-09 10:41:33 -0300
committerGabriel Schneider <[email protected]>2026-08-10 09:17:07 -0300
commit9085cb5bfdd0b78ff3a62c0c71fc231dd7b5052a (patch)
tree91fce278abc85e4f7491de8b5f02184c8a2d5a56 /test
parent2b1fac6fecbf7d7cc08f9dad24687b05e9968c9a (diff)
downloadpardes-9085cb5bfdd0b78ff3a62c0c71fc231dd7b5052a.tar.gz
pardes-9085cb5bfdd0b78ff3a62c0c71fc231dd7b5052a.zip
replace ArrayLists with bounded storage
Diffstat (limited to 'test')
-rw-r--r--test/e2e_harness.zig50
-rw-r--r--test/hxdiff.zig163
-rw-r--r--test/image_harness.zig133
-rw-r--r--test/lspbench.zig29
-rw-r--r--test/pdf_bench.zig29
-rw-r--r--test/pdf_sections_bench.zig61
-rw-r--r--test/perf.zig94
-rw-r--r--test/snapshot.zig372
8 files changed, 631 insertions, 300 deletions
diff --git a/test/e2e_harness.zig b/test/e2e_harness.zig
index eb4c7a2f..c188827e 100644
--- a/test/e2e_harness.zig
+++ b/test/e2e_harness.zig
@@ -22,6 +22,24 @@ pub fn nowMs() i64 {
_ = libc.clock_gettime(.MONOTONIC, &ts);
return @as(i64, @intCast(ts.sec)) * 1000 + @divFloor(@as(i64, @intCast(ts.nsec)), 1_000_000);
}
+const raw_capture_max = 64 * 1024 * 1024;
+
+pub const RawCapture = struct {
+ bytes: [raw_capture_max]u8 = undefined,
+ items: []u8 = &.{},
+ full: bool = false,
+
+ fn appendSlice(self: *RawCapture, chunk: []const u8) error{CaptureFull}!void {
+ if (self.full) return error.CaptureFull;
+ if (chunk.len > self.bytes.len - self.items.len) {
+ self.full = true;
+ return error.CaptureFull;
+ }
+ const end = self.items.len + chunk.len;
+ @memcpy(self.bytes[self.items.len..end], chunk);
+ self.items = self.bytes[0..end];
+ }
+};
/// E2E test harness: forks the app in a pty and parses its output with a
/// ghostty terminal emulator. Tests read the 2D screen GRID (via plainString),
@@ -47,8 +65,9 @@ pub const Harness = struct {
trace: bool = false,
/// every raw byte the app has emitted (accumulated in pump). Lets tests assert
/// on control sequences the emulator consumes and never renders (e.g. OSC 52
- /// clipboard writes). gpa-owned; freed in deinit.
- raw: std.ArrayList(u8) = .empty,
+ /// clipboard writes). Capture is bounded explicitly so a runaway child cannot
+ /// consume unbounded test-runner memory.
+ raw: *RawCapture,
pub fn init(gpa: std.mem.Allocator, exe: [*:0]const u8, rows: u16, cols: u16) !Harness {
return initArgs(gpa, exe, rows, cols, null);
@@ -63,14 +82,16 @@ pub const Harness = struct {
/// Like init, but passes extra CLI args (e.g. "-l", "dump.zon").
pub fn initArgv(gpa: std.mem.Allocator, exe: [*:0]const u8, rows: u16, cols: u16, args: []const ?[*:0]const u8) !Harness {
+ const argv = try gpa.allocSentinel(?[*:0]const u8, args.len + 1, null);
+ defer gpa.free(argv);
+ argv[0] = exe;
+ for (args, 0..) |a, i| argv[i + 1] = a;
+
var master: c_int = undefined;
const ws = posix.winsize{ .row = rows, .col = cols, .xpixel = 0, .ypixel = 0 };
const pid = forkpty(&master, null, null, &ws);
if (pid == 0) {
- var argv: [8:null]?[*:0]const u8 = @splat(null);
- argv[0] = exe;
- for (args, 0..) |a, i| argv[i + 1] = a;
- _ = execvp(exe, &argv);
+ _ = execvp(exe, argv.ptr);
_exit(127);
}
const term = try gpa.create(ghostty_vt.Terminal);
@@ -84,6 +105,9 @@ pub const Harness = struct {
// vtStream() captures a pointer to `term`; term is heap-allocated so the
// pointer stays valid for the life of the harness.
const stream = term.vtStream();
+ const raw = try gpa.create(RawCapture);
+ errdefer gpa.destroy(raw);
+ raw.* = .{};
return .{
.gpa = gpa,
.master = master,
@@ -92,6 +116,7 @@ pub const Harness = struct {
.stream = stream,
.rows = rows,
.cols = cols,
+ .raw = raw,
};
}
@@ -99,7 +124,7 @@ pub const Harness = struct {
posix.kill(self.pid, posix.SIG.KILL) catch {};
_ = libc.close(self.master);
self.stream.deinit();
- self.raw.deinit(self.gpa);
+ self.gpa.destroy(self.raw);
self.term.deinit(self.gpa);
self.gpa.destroy(self.term);
}
@@ -128,7 +153,7 @@ pub const Harness = struct {
if ((fds[0].revents & posix.POLL.IN) != 0) {
const n = posix.read(self.master, &buf) catch break;
if (n == 0) break;
- self.raw.appendSlice(self.gpa, buf[0..n]) catch {};
+ self.recordRaw(buf[0..n]);
self.stream.nextSlice(buf[0..n]);
}
}
@@ -147,7 +172,7 @@ pub const Harness = struct {
var buf: [4096]u8 = undefined;
const n = posix.read(self.master, &buf) catch return false;
if (n == 0) return false;
- self.raw.appendSlice(self.gpa, buf[0..n]) catch {};
+ self.recordRaw(buf[0..n]);
self.stream.nextSlice(buf[0..n]);
return true;
}
@@ -206,7 +231,7 @@ pub const Harness = struct {
if ((fds[0].revents & posix.POLL.IN) != 0) {
const n = posix.read(self.master, &buf) catch break;
if (n == 0) break;
- self.raw.appendSlice(self.gpa, buf[0..n]) catch {};
+ self.recordRaw(buf[0..n]);
self.stream.nextSlice(buf[0..n]);
}
const text = try self.screenText();
@@ -219,6 +244,11 @@ pub const Harness = struct {
if (self.trace) self.traceScreen("waitFor miss");
return false;
}
+ fn recordRaw(self: *Harness, bytes: []const u8) void {
+ self.raw.appendSlice(bytes) catch |err| switch (err) {
+ error.CaptureFull => {},
+ };
+ }
/// The visual cursor position (viewport coords): { x, y }.
pub fn cursor(self: *Harness) struct { x: u16, y: u16 } {
diff --git a/test/hxdiff.zig b/test/hxdiff.zig
index f6ab5c6f..599390d6 100644
--- a/test/hxdiff.zig
+++ b/test/hxdiff.zig
@@ -90,7 +90,8 @@ pub fn main(init: std.process.Init) !void {
var parity = false;
var goldens_path: ?[]const u8 = null;
var waivers_path: ?[]const u8 = null;
- var case_paths: std.ArrayList([]const u8) = .empty;
+ const case_paths = arena.alloc([]const u8, args.len - 1) catch fatal("oom", .{});
+ var n_case_paths: usize = 0;
var want_waivers = false;
for (args[1..]) |a| {
if (want_waivers) {
@@ -100,17 +101,18 @@ pub fn main(init: std.process.Init) !void {
parity = true;
} else if (std.mem.eql(u8, a, "--waivers")) {
want_waivers = true;
- } else if (parity or case_paths.items.len == 0) {
+ } else if (parity or n_case_paths == 0) {
// diff mode is positional (cases, goldens, waivers); parity mode
// takes any number of case files and nothing else
- case_paths.append(arena, a) catch fatal("oom", .{});
+ case_paths[n_case_paths] = a;
+ n_case_paths += 1;
} else if (goldens_path == null) {
goldens_path = a;
} else if (waivers_path == null) {
waivers_path = a;
} else fatal("usage: pardes-hxdiff <cases.jsonl> [goldens.jsonl [waivers.jsonl]]", .{});
}
- if (case_paths.items.len == 0)
+ if (n_case_paths == 0)
fatal("usage: pardes-hxdiff [--parity] <cases.jsonl>... [goldens.jsonl [waivers.jsonl]]", .{});
// goldens + waivers, keyed by name (diff mode)
@@ -144,11 +146,11 @@ pub fn main(init: std.process.Init) !void {
}
}
- var out: std.ArrayList(u8) = .empty;
+ const stdout = std.Io.File.stdout();
var n_cases: usize = 0;
var n_bad: usize = 0;
var n_waived: usize = 0;
- for (case_paths.items) |path| {
+ for (case_paths[0..n_case_paths]) |path| {
const src = readFileAlloc(arena, path) catch |e| fatal("read {s}: {s}", .{ path, @errorName(e) });
var lines = std.mem.splitScalar(u8, src, '\n');
var lineno: usize = 0;
@@ -179,8 +181,8 @@ pub fn main(init: std.process.Init) !void {
// null sels/primary are DROPPED, not written: that is what
// keeps a one-selection line identical to the old contract
const jl = std.json.Stringify.valueAlloc(arena, res, .{ .emit_null_optional_fields = false }) catch fatal("oom", .{});
- out.appendSlice(arena, jl) catch fatal("oom", .{});
- out.append(arena, '\n') catch fatal("oom", .{});
+ try stdout.writeStreamingAll(init.io, jl);
+ try stdout.writeStreamingAll(init.io, "\n");
continue;
}
const g = goldens.get(case.name) orelse fatal("no golden for case {s} (regen goldens?)", .{case.name});
@@ -195,10 +197,7 @@ pub fn main(init: std.process.Init) !void {
std.debug.print("MISMATCH {s} (keys: {s})\n{s}", .{ case.name, case.keys, diff });
}
}
- if (!parity and goldens_path == null) {
- try std.Io.File.stdout().writeStreamingAll(init.io, out.items);
- return;
- }
+ if (!parity and goldens_path == null) return;
std.debug.print("{s}: {d} cases, {d} mismatches, {d} waived\n", .{ if (parity) "hxparity" else "hxdiff", n_cases, n_bad, n_waived });
if (n_bad > 0) std.process.exit(1);
}
@@ -235,18 +234,23 @@ const Waiver = struct {
/// field-by-field compare; returns a human-readable report ("" = match).
/// `wl`/`gl` name the two sides (helix vs pardes, or file vs pty).
fn diffResult(arena: std.mem.Allocator, want: Result, got: Result, wl: []const u8, gl: []const u8) []const u8 {
- var buf: std.ArrayList(u8) = .empty;
+ var parts: [5][]const u8 = undefined;
+ var n: usize = 0;
if (!std.mem.eql(u8, want.text, got.text)) {
- appendf(arena, &buf, " text: {s} {f} != {s} {f}\n", .{ wl, std.json.fmt(want.text, .{}), gl, std.json.fmt(got.text, .{}) });
+ parts[n] = std.fmt.allocPrint(arena, " text: {s} {f} != {s} {f}\n", .{ wl, std.json.fmt(want.text, .{}), gl, std.json.fmt(got.text, .{}) }) catch "";
+ n += 1;
}
if (!std.mem.eql(u8, want.mode, got.mode)) {
- appendf(arena, &buf, " mode: {s} {s} != {s} {s}\n", .{ wl, want.mode, gl, got.mode });
+ parts[n] = std.fmt.allocPrint(arena, " mode: {s} {s} != {s} {s}\n", .{ wl, want.mode, gl, got.mode }) catch "";
+ n += 1;
}
if (want.cursor.row != got.cursor.row or want.cursor.col != got.cursor.col) {
- appendf(arena, &buf, " cursor: {s} ({d},{d}) != {s} ({d},{d})\n", .{ wl, @as(i64, want.cursor.row), @as(i64, want.cursor.col), gl, @as(i64, got.cursor.row), @as(i64, got.cursor.col) });
+ parts[n] = std.fmt.allocPrint(arena, " cursor: {s} ({d},{d}) != {s} ({d},{d})\n", .{ wl, @as(i64, want.cursor.row), @as(i64, want.cursor.col), gl, @as(i64, got.cursor.row), @as(i64, got.cursor.col) }) catch "";
+ n += 1;
}
if (want.anchor.row != got.anchor.row or want.anchor.col != got.anchor.col) {
- appendf(arena, &buf, " anchor: {s} ({d},{d}) != {s} ({d},{d})\n", .{ wl, @as(i64, want.anchor.row), @as(i64, want.anchor.col), gl, @as(i64, got.anchor.row), @as(i64, got.anchor.col) });
+ parts[n] = std.fmt.allocPrint(arena, " anchor: {s} ({d},{d}) != {s} ({d},{d})\n", .{ wl, @as(i64, want.anchor.row), @as(i64, want.anchor.col), gl, @as(i64, got.anchor.row), @as(i64, got.anchor.col) }) catch "";
+ n += 1;
}
// the whole selection: absent on both sides = one range, and the two
// fields above already said everything about it
@@ -258,31 +262,58 @@ fn diffResult(arena: std.mem.Allocator, want: Result, got: Result, wl: []const u
a.anchor.row != b.anchor.row or a.anchor.col != b.anchor.col) sel_diff = true;
};
if (sel_diff) {
- appendf(arena, &buf, " sels: {s} {s} != {s} {s}\n", .{ wl, fmtSels(arena, ws, want.primary), gl, fmtSels(arena, gs, got.primary) });
+ parts[n] = std.fmt.allocPrint(arena, " sels: {s} {s} != {s} {s}\n", .{ wl, fmtSels(arena, ws, want.primary), gl, fmtSels(arena, gs, got.primary) }) catch "";
+ n += 1;
}
- return buf.items;
+ return concatParts(arena, parts[0..n]);
}
/// "2 of [(0,1)|(1,1) *(2,1)]" — every range as cursor|anchor (collapsed to
/// one pair when they are the same cell), the primary starred
fn fmtSels(arena: std.mem.Allocator, sels: []const Sel, primary: ?usize) []const u8 {
if (sels.len == 0) return "<one>";
- var buf: std.ArrayList(u8) = .empty;
- appendf(arena, &buf, "{d} of [", .{sels.len});
+ var part_count: usize = 2;
for (sels, 0..) |s, i| {
- if (i > 0) appendf(arena, &buf, " ", .{});
- if (i == (primary orelse 0)) appendf(arena, &buf, "*", .{});
- appendf(arena, &buf, "({d},{d})", .{ @as(i64, s.cursor.row), @as(i64, s.cursor.col) });
- if (s.anchor.row != s.cursor.row or s.anchor.col != s.cursor.col)
- appendf(arena, &buf, "|({d},{d})", .{ @as(i64, s.anchor.row), @as(i64, s.anchor.col) });
+ part_count += 1;
+ if (i > 0) part_count += 1;
+ if (i == (primary orelse 0)) part_count += 1;
+ if (s.anchor.row != s.cursor.row or s.anchor.col != s.cursor.col) part_count += 1;
}
- appendf(arena, &buf, "]", .{});
- return buf.items;
+ const parts = arena.alloc([]const u8, part_count) catch return "";
+ var n: usize = 0;
+ parts[n] = std.fmt.allocPrint(arena, "{d} of [", .{sels.len}) catch "";
+ n += 1;
+ for (sels, 0..) |s, i| {
+ if (i > 0) {
+ parts[n] = " ";
+ n += 1;
+ }
+ if (i == (primary orelse 0)) {
+ parts[n] = "*";
+ n += 1;
+ }
+ parts[n] = std.fmt.allocPrint(arena, "({d},{d})", .{ @as(i64, s.cursor.row), @as(i64, s.cursor.col) }) catch "";
+ n += 1;
+ if (s.anchor.row != s.cursor.row or s.anchor.col != s.cursor.col) {
+ parts[n] = std.fmt.allocPrint(arena, "|({d},{d})", .{ @as(i64, s.anchor.row), @as(i64, s.anchor.col) }) catch "";
+ n += 1;
+ }
+ }
+ parts[n] = "]";
+ n += 1;
+ return concatParts(arena, parts[0..n]);
}
-fn appendf(arena: std.mem.Allocator, buf: *std.ArrayList(u8), comptime fmt: []const u8, fargs: anytype) void {
- const s = std.fmt.allocPrint(arena, fmt, fargs) catch return;
- buf.appendSlice(arena, s) catch {};
+fn concatParts(arena: std.mem.Allocator, parts: []const []const u8) []const u8 {
+ var len: usize = 0;
+ for (parts) |part| len = std.math.add(usize, len, part.len) catch return "";
+ const out = arena.alloc(u8, len) catch return "";
+ var offset: usize = 0;
+ for (parts) |part| {
+ @memcpy(out[offset..][0..part.len], part);
+ offset += part.len;
+ }
+ return out;
}
fn runCase(arena: std.mem.Allocator, case: Case, pane_kind: []const u8) !Result {
@@ -297,12 +328,18 @@ fn runCase(arena: std.mem.Allocator, case: Case, pane_kind: []const u8) !Result
_ = try core.hxOpenFileContent(case.text);
} else if (std.mem.eql(u8, pane_kind, "tty")) {
// the harness is the pty here: cook \n to \r\n the way ONLCR would
- var cooked: std.ArrayList(u8) = .empty;
+ const cooked_len = std.math.add(usize, case.text.len, std.mem.count(u8, case.text, "\n")) catch return error.OutOfMemory;
+ const cooked = try arena.alloc(u8, cooked_len);
+ var cooked_i: usize = 0;
for (case.text) |ch| {
- if (ch == '\n') try cooked.append(arena, '\r');
- try cooked.append(arena, ch);
+ if (ch == '\n') {
+ cooked[cooked_i] = '\r';
+ cooked_i += 1;
+ }
+ cooked[cooked_i] = ch;
+ cooked_i += 1;
}
- core.update(.{ .output = .{ .pane = 0, .bytes = cooked.items } });
+ core.update(.{ .output = .{ .pane = 0, .bytes = cooked } });
pump(core);
// leave tty mode the way a live session does: Ctrl-<tty_toggle>,
// then park the cursor at (0,0) — the helix harness's initial
@@ -400,12 +437,19 @@ fn runCase(arena: std.mem.Allocator, case: Case, pane_kind: []const u8) !Result
// its line list is exactly a file content's — join it and you have the
// same bytes, trailing newline included, with nothing to patch up.
const pl = try core.paneCursorLines(pane);
- var buf: std.ArrayList(u8) = .empty;
+ var text_len: usize = if (pl.lines.len == 0) 0 else pl.lines.len - 1;
+ for (pl.lines) |ln| text_len = std.math.add(usize, text_len, ln.len) catch return error.OutOfMemory;
+ const joined = try arena.alloc(u8, text_len);
+ var offset: usize = 0;
for (pl.lines, 0..) |ln, i| {
- if (i > 0) try buf.append(arena, '\n');
- try buf.appendSlice(arena, ln);
+ if (i > 0) {
+ joined[offset] = '\n';
+ offset += 1;
+ }
+ @memcpy(joined[offset..][0..ln.len], ln);
+ offset += ln.len;
}
- text = buf.items;
+ text = joined;
}
var anchor: Pos = .{ .row = pane.cur_row, .col = pane.cur_col };
if (pane.vsel.active) {
@@ -423,7 +467,8 @@ fn runCase(arena: std.mem.Allocator, case: Case, pane_kind: []const u8) !Result
var sels: ?[]const Sel = null;
var primary: ?usize = null;
if (pane.nsel > 0) {
- var out: std.ArrayList(Sel) = .empty;
+ const out = try arena.alloc(Sel, @as(usize, pane.nsel) + 1);
+ var out_len: usize = 0;
const prim: Sel = .{ .cursor = .{ .row = pane.cur_row, .col = pane.cur_col }, .anchor = anchor };
const pr_row = @min(prim.cursor.row, prim.anchor.row);
const pr_col = if (prim.cursor.row == prim.anchor.row) @min(prim.cursor.col, prim.anchor.col) else if (prim.cursor.row < prim.anchor.row) prim.cursor.col else prim.anchor.col;
@@ -431,16 +476,18 @@ fn runCase(arena: std.mem.Allocator, case: Case, pane_kind: []const u8) !Result
const s_row = @min(s.row, s.arow);
const s_col = if (s.row == s.arow) @min(s.col, s.acol) else if (s.row < s.arow) s.col else s.acol;
if (primary == null and (pr_row < s_row or (pr_row == s_row and pr_col <= s_col))) {
- primary = out.items.len;
- try out.append(arena, prim);
+ primary = out_len;
+ out[out_len] = prim;
+ out_len += 1;
}
- try out.append(arena, .{ .cursor = .{ .row = s.row, .col = s.col }, .anchor = .{ .row = s.arow, .col = s.acol } });
+ out[out_len] = .{ .cursor = .{ .row = s.row, .col = s.col }, .anchor = .{ .row = s.arow, .col = s.acol } };
+ out_len += 1;
}
if (primary == null) {
- primary = out.items.len;
- try out.append(arena, prim);
+ primary = out_len;
+ out[out_len] = prim;
}
- sels = out.items;
+ sels = out;
}
return .{
.name = case.name,
@@ -464,18 +511,30 @@ fn readFileAlloc(arena: std.mem.Allocator, path: []const u8) ![]u8 {
const fd = libc.open(path_z, .{ .ACCMODE = .RDONLY });
if (fd < 0) return error.OpenFailed;
defer _ = libc.close(fd);
- var buf: std.ArrayList(u8) = .empty;
- var chunk: [16384]u8 = undefined;
+ const end = libc.lseek(fd, 0, libc.SEEK.END);
+ if (end < 0 or libc.lseek(fd, 0, libc.SEEK.SET) < 0) return error.StatFailed;
+ const size = std.math.cast(usize, end) orelse return error.FileTooLarge;
+ const buf = try arena.alloc(u8, size);
+ var offset: usize = 0;
+ while (offset < buf.len) {
+ const n = libc.read(fd, buf[offset..].ptr, buf.len - offset);
+ if (n < 0) {
+ if (libc.errno(n) == .INTR) continue;
+ return error.ReadFailed;
+ }
+ if (n == 0) return buf[0..offset];
+ offset += @intCast(n);
+ }
+ var extra: [1]u8 = undefined;
while (true) {
- const n = libc.read(fd, &chunk, chunk.len);
+ const n = libc.read(fd, &extra, extra.len);
if (n < 0) {
if (libc.errno(n) == .INTR) continue;
return error.ReadFailed;
}
- if (n == 0) break;
- try buf.appendSlice(arena, chunk[0..@intCast(n)]);
+ if (n > 0) return error.FileChanged;
+ return buf;
}
- return buf.items;
}
fn fatal(comptime fmt: []const u8, args: anytype) noreturn {
diff --git a/test/image_harness.zig b/test/image_harness.zig
index d4cb8955..601bce49 100644
--- a/test/image_harness.zig
+++ b/test/image_harness.zig
@@ -73,8 +73,27 @@ fn cleanupBase(arena: std.mem.Allocator, base: [:0]const u8) void {
_ = libc.rmdir(base);
}
-fn appendFmt(arena: std.mem.Allocator, out: *std.ArrayList(u8), comptime fmt: []const u8, args: anytype) !void {
- try out.appendSlice(arena, try std.fmt.allocPrint(arena, fmt, args));
+const pdf_fixture_cap = 4096;
+
+const BoundedPdf = struct {
+ bytes: [pdf_fixture_cap]u8 = undefined,
+ len: usize = 0,
+
+ fn appendSlice(out: *BoundedPdf, bytes: []const u8) !void {
+ if (bytes.len > out.bytes.len - out.len) return error.FixturePdfTooLarge;
+ @memcpy(out.bytes[out.len..][0..bytes.len], bytes);
+ out.len += bytes.len;
+ }
+
+ fn written(out: *const BoundedPdf) []const u8 {
+ return out.bytes[0..out.len];
+ }
+};
+
+fn appendFmt(out: *BoundedPdf, comptime fmt: []const u8, args: anytype) !void {
+ const bytes = std.fmt.bufPrint(out.bytes[out.len..], fmt, args) catch
+ return error.FixturePdfTooLarge;
+ out.len += bytes.len;
}
/// A self-contained, valid PDF fixture. The first two pages retain the red /
@@ -83,6 +102,7 @@ fn appendFmt(arena: std.mem.Allocator, out: *std.ArrayList(u8), comptime fmt: []
/// headers. Building it here keeps the backend test independent of mutool,
/// fonts, and checked-in binary fixtures while the xref offsets remain exact.
fn writePdf(arena: std.mem.Allocator, path: [:0]const u8) !void {
+ _ = arena;
const first_stream =
\\1 0 0 rg
\\0 0 72 48 re
@@ -128,54 +148,63 @@ fn writePdf(arena: std.mem.Allocator, path: [:0]const u8) !void {
\\ET
;
- var out: std.ArrayList(u8) = .empty;
+ var out: BoundedPdf = .{};
var offsets: [16]usize = @splat(0);
- try out.appendSlice(arena, "%PDF-1.4\n%\xE2\xE3\xCF\xD3\n");
+ try out.appendSlice("%PDF-1.4\n%\xE2\xE3\xCF\xD3\n");
- offsets[1] = out.items.len;
- try out.appendSlice(arena, "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /Outlines 12 0 R /PageMode /UseOutlines >>\nendobj\n");
- offsets[2] = out.items.len;
- try out.appendSlice(arena, "2 0 obj\n<< /Type /Pages /Count 4 /Kids [3 0 R 5 0 R 7 0 R 9 0 R] >>\nendobj\n");
- offsets[3] = out.items.len;
- try out.appendSlice(arena, "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 72 48] /Resources << /Font << /F1 11 0 R >> >> /Contents 4 0 R >>\nendobj\n");
- offsets[4] = out.items.len;
- try appendFmt(arena, &out, "4 0 obj\n<< /Length {d} >>\nstream\n{s}endstream\nendobj\n", .{ first_stream.len, first_stream });
- offsets[5] = out.items.len;
- try out.appendSlice(arena, "5 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 72 48] /Resources << /Font << /F1 11 0 R >> >> /Contents 6 0 R >>\nendobj\n");
- offsets[6] = out.items.len;
- try appendFmt(arena, &out, "6 0 obj\n<< /Length {d} >>\nstream\n{s}endstream\nendobj\n", .{ second_stream.len, second_stream });
- offsets[7] = out.items.len;
- try out.appendSlice(arena, "7 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 72 50] /Resources << /Font << /F1 11 0 R >> >> /Contents 8 0 R >>\nendobj\n");
- offsets[8] = out.items.len;
- try appendFmt(arena, &out, "8 0 obj\n<< /Length {d} >>\nstream\n{s}endstream\nendobj\n", .{ third_stream.len, third_stream });
- offsets[9] = out.items.len;
- try out.appendSlice(arena, "9 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 72 52] /Resources << /Font << /F1 11 0 R >> >> /Contents 10 0 R >>\nendobj\n");
- offsets[10] = out.items.len;
- try appendFmt(arena, &out, "10 0 obj\n<< /Length {d} >>\nstream\n{s}endstream\nendobj\n", .{ fourth_stream.len, fourth_stream });
- offsets[11] = out.items.len;
- try out.appendSlice(arena, "11 0 obj\n<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>\nendobj\n");
- offsets[12] = out.items.len;
- try out.appendSlice(arena, "12 0 obj\n<< /Type /Outlines /First 13 0 R /Last 15 0 R /Count 3 >>\nendobj\n");
- offsets[13] = out.items.len;
- try out.appendSlice(arena, "13 0 obj\n<< /Title (Upper) /Parent 12 0 R /First 14 0 R /Last 14 0 R /Next 15 0 R /Count 1 /Dest [3 0 R /XYZ 4 8 null] >>\nendobj\n");
- offsets[14] = out.items.len;
- try out.appendSlice(arena, "14 0 obj\n<< /Title (Lower) /Parent 13 0 R /Dest [3 0 R /XYZ 4 28 null] >>\nendobj\n");
- offsets[15] = out.items.len;
- try out.appendSlice(arena, "15 0 obj\n<< /Title (Page two) /Parent 12 0 R /Prev 13 0 R /Dest [5 0 R /XYZ 4 38 null] >>\nendobj\n");
+ offsets[1] = out.len;
+ try out.appendSlice("1 0 obj\n<< /Type /Catalog /Pages 2 0 R /Outlines 12 0 R /PageMode /UseOutlines >>\nendobj\n");
+ offsets[2] = out.len;
+ try out.appendSlice("2 0 obj\n<< /Type /Pages /Count 4 /Kids [3 0 R 5 0 R 7 0 R 9 0 R] >>\nendobj\n");
+ offsets[3] = out.len;
+ try out.appendSlice("3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 72 48] /Resources << /Font << /F1 11 0 R >> >> /Contents 4 0 R >>\nendobj\n");
+ offsets[4] = out.len;
+ try appendFmt(&out, "4 0 obj\n<< /Length {d} >>\nstream\n{s}endstream\nendobj\n", .{ first_stream.len, first_stream });
+ offsets[5] = out.len;
+ try out.appendSlice("5 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 72 48] /Resources << /Font << /F1 11 0 R >> >> /Contents 6 0 R >>\nendobj\n");
+ offsets[6] = out.len;
+ try appendFmt(&out, "6 0 obj\n<< /Length {d} >>\nstream\n{s}endstream\nendobj\n", .{ second_stream.len, second_stream });
+ offsets[7] = out.len;
+ try out.appendSlice("7 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 72 50] /Resources << /Font << /F1 11 0 R >> >> /Contents 8 0 R >>\nendobj\n");
+ offsets[8] = out.len;
+ try appendFmt(&out, "8 0 obj\n<< /Length {d} >>\nstream\n{s}endstream\nendobj\n", .{ third_stream.len, third_stream });
+ offsets[9] = out.len;
+ try out.appendSlice("9 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 72 52] /Resources << /Font << /F1 11 0 R >> >> /Contents 10 0 R >>\nendobj\n");
+ offsets[10] = out.len;
+ try appendFmt(&out, "10 0 obj\n<< /Length {d} >>\nstream\n{s}endstream\nendobj\n", .{ fourth_stream.len, fourth_stream });
+ offsets[11] = out.len;
+ try out.appendSlice("11 0 obj\n<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>\nendobj\n");
+ offsets[12] = out.len;
+ try out.appendSlice("12 0 obj\n<< /Type /Outlines /First 13 0 R /Last 15 0 R /Count 3 >>\nendobj\n");
+ offsets[13] = out.len;
+ try out.appendSlice("13 0 obj\n<< /Title (Upper) /Parent 12 0 R /First 14 0 R /Last 14 0 R /Next 15 0 R /Count 1 /Dest [3 0 R /XYZ 4 8 null] >>\nendobj\n");
+ offsets[14] = out.len;
+ try out.appendSlice("14 0 obj\n<< /Title (Lower) /Parent 13 0 R /Dest [3 0 R /XYZ 4 28 null] >>\nendobj\n");
+ offsets[15] = out.len;
+ try out.appendSlice("15 0 obj\n<< /Title (Page two) /Parent 12 0 R /Prev 13 0 R /Dest [5 0 R /XYZ 4 38 null] >>\nendobj\n");
- const xref = out.items.len;
- try out.appendSlice(arena, "xref\n0 16\n0000000000 65535 f \n");
+ const xref = out.len;
+ try out.appendSlice("xref\n0 16\n0000000000 65535 f \n");
for (offsets[1..]) |offset|
- try appendFmt(arena, &out, "{d:0>10} 00000 n \n", .{offset});
- try appendFmt(arena, &out, "trailer\n<< /Size 16 /Root 1 0 R >>\nstartxref\n{d}\n%%EOF\n", .{xref});
- try eh.writeFile(path, out.items);
+ try appendFmt(&out, "{d:0>10} 00000 n \n", .{offset});
+ try appendFmt(&out, "trailer\n<< /Size 16 /Root 1 0 R >>\nstartxref\n{d}\n%%EOF\n", .{xref});
+ try eh.writeFile(path, out.written());
}
fn writePpm(arena: std.mem.Allocator, path: [:0]const u8, width: usize, height: usize) !void {
- var out: std.ArrayList(u8) = .empty;
- try out.appendSlice(arena, try std.fmt.allocPrint(arena, "P6\n{d} {d}\n255\n", .{ width, height }));
- for (0..width * height) |_| try out.appendSlice(arena, &fixture_rgb);
- try eh.writeFile(path, out.items);
+ var header_buf: [64]u8 = undefined;
+ const header = std.fmt.bufPrint(&header_buf, "P6\n{d} {d}\n255\n", .{ width, height }) catch
+ return error.FixturePpmHeaderTooLarge;
+ const pixel_count = try std.math.mul(usize, width, height);
+ const pixel_bytes = try std.math.mul(usize, pixel_count, fixture_rgb.len);
+ const out = try arena.alloc(u8, try std.math.add(usize, header.len, pixel_bytes));
+ @memcpy(out[0..header.len], header);
+ var offset = header.len;
+ for (0..pixel_count) |_| {
+ @memcpy(out[offset..][0..fixture_rgb.len], &fixture_rgb);
+ offset += fixture_rgb.len;
+ }
+ try eh.writeFile(path, out);
}
fn runKitty(arena: std.mem.Allocator, exe: [:0]const u8, base: [:0]const u8) !void {
@@ -656,16 +685,18 @@ fn readFile(allocator: std.mem.Allocator, path: [:0]const u8) ![]u8 {
const fd = libc.open(path, .{ .ACCMODE = .RDONLY }, @as(libc.mode_t, 0));
if (fd < 0) return error.OpenFailed;
defer _ = libc.close(fd);
- var out: std.ArrayList(u8) = .empty;
- errdefer out.deinit(allocator);
- var buf: [8192]u8 = undefined;
- while (true) {
- const n = libc.read(fd, &buf, buf.len);
+ const end = libc.lseek(fd, 0, libc.SEEK.END);
+ if (end < 0 or libc.lseek(fd, 0, libc.SEEK.SET) < 0) return error.ReadFailed;
+ const out = try allocator.alloc(u8, @intCast(end));
+ errdefer allocator.free(out);
+ var filled: usize = 0;
+ while (filled < out.len) {
+ const n = libc.read(fd, out[filled..].ptr, out.len - filled);
if (n < 0) return error.ReadFailed;
- if (n == 0) break;
- try out.appendSlice(allocator, buf[0..@intCast(n)]);
+ if (n == 0) return error.UnexpectedEof;
+ filled += @intCast(n);
}
- return out.toOwnedSlice(allocator);
+ return out;
}
fn ppmPixels(bytes: []const u8) ?[]const u8 {
diff --git a/test/lspbench.zig b/test/lspbench.zig
index 329e5743..964d3c8c 100644
--- a/test/lspbench.zig
+++ b/test/lspbench.zig
@@ -147,8 +147,8 @@ pub fn main(init: std.process.Init) !void {
}
const rss0 = rssKib();
- var results: std.ArrayList(Result) = .empty;
- defer results.deinit(gpa);
+ var results: [anchors.len]Result = undefined;
+ var result_count: usize = 0;
for (anchors) |an| {
const path = try std.fs.path.join(gpa, &.{ root, an.file });
@@ -193,11 +193,12 @@ pub fn main(init: std.process.Init) !void {
lsp.query(gpa, arena.allocator(), req, &out.writer);
break :blk std.mem.indexOf(u8, out.written(), an.expect) != null;
});
- try results.append(gpa, r);
+ results[result_count] = r;
+ result_count += 1;
}
const rss = rssKib() -| rss0;
- if (json) reportJson(results.items, rss) else reportText(results.items, rss);
+ if (json) reportJson(results[0..result_count], rss) else reportText(results[0..result_count], rss);
}
fn runOnce(gpa: std.mem.Allocator, req: lsp.Req, r: *Result) u64 {
@@ -264,19 +265,23 @@ fn readZ(gpa: std.mem.Allocator, path: []const u8) ![:0]u8 {
const fd = libc.open(path_z, .{ .ACCMODE = .RDONLY });
if (fd < 0) return error.OpenFailed;
defer _ = libc.close(fd);
- var buf: std.ArrayList(u8) = .empty;
- errdefer buf.deinit(gpa);
- var chunk: [16384]u8 = undefined;
- while (true) {
- const n = libc.read(fd, &chunk, chunk.len);
+ const end = libc.lseek(fd, 0, libc.SEEK.END);
+ if (end < 0 or libc.lseek(fd, 0, libc.SEEK.SET) < 0) return error.ReadFailed;
+ const len: usize = @intCast(end);
+ const buf = try gpa.alloc(u8, len + 1);
+ errdefer gpa.free(buf);
+ var filled: usize = 0;
+ while (filled < len) {
+ const n = libc.read(fd, buf[filled..].ptr, len - filled);
if (n < 0) {
if (libc.errno(n) == .INTR) continue;
return error.ReadFailed;
}
- if (n == 0) break;
- try buf.appendSlice(gpa, chunk[0..@intCast(n)]);
+ if (n == 0) return error.UnexpectedEof;
+ filled += @intCast(n);
}
- return buf.toOwnedSliceSentinel(gpa, 0);
+ buf[len] = 0;
+ return buf[0..len :0];
}
/// VmHWM from /proc/self/status — the peak, not the current, so a backend that
diff --git a/test/pdf_bench.zig b/test/pdf_bench.zig
index fb911bee..0f09f72a 100644
--- a/test/pdf_bench.zig
+++ b/test/pdf_bench.zig
@@ -470,6 +470,8 @@ fn mib(bytes: u64) f64 {
return @as(f64, @floatFromInt(bytes)) / (1024.0 * 1024.0);
}
+const report_max_bytes = 64 * 1024;
+
fn reportText(
io: std.Io,
config: Config,
@@ -477,10 +479,9 @@ fn reportText(
multi_pages: usize,
results: []const Result,
) void {
- var out: std.ArrayList(u8) = .empty;
- defer out.deinit(gpa);
+ var storage: [report_max_bytes]u8 = undefined;
+ var out: std.Io.Writer = .fixed(&storage);
out.print(
- gpa,
"pardes PDF render benchmark (ReleaseFast)\n" ++
"path: {s} ({d} pages), selected page: {d}, multi-page pass: {d} pages\n" ++
"warmup: {d}, sampled reps: {d}; cold = fresh Document (OS cache uncontrolled)\n" ++
@@ -500,8 +501,7 @@ fn reportText(
tint_colors.foreground[2],
},
) catch return;
- out.appendSlice(
- gpa,
+ out.writeAll(
"profile scenario min ms median p90 max ms MPix/s GiB/s pixels/op RGBA MiB checksum\n" ++
"-----------------------------------------------------------------------------------------------------------------------------------\n",
) catch return;
@@ -509,7 +509,6 @@ fn reportText(
const profile = profiles[result.profile];
const scenario = scenarios[result.scenario];
out.print(
- gpa,
"{s:<20} {s:<26} {d:>8.2} {d:>8.2} {d:>8.2} {d:>8.2} {d:>7.1} {d:>6.2} {d:>10} {d:>9.2} {x:0>16}\n",
.{
profile.id,
@@ -526,10 +525,9 @@ fn reportText(
},
) catch return;
}
- out.appendSlice(gpa, "\nrequests:\n") catch return;
+ out.writeAll("\nrequests:\n") catch return;
for (profiles) |profile| {
out.print(
- gpa,
" {s}: dpi={d}, viewport-min={d}x{d}, max-dimension={d} — {s}\n",
.{
profile.id,
@@ -541,7 +539,7 @@ fn reportText(
},
) catch return;
}
- std.Io.File.stdout().writeStreamingAll(io, out.items) catch {};
+ std.Io.File.stdout().writeStreamingAll(io, out.buffered()) catch {};
}
fn reportJson(
@@ -551,12 +549,11 @@ fn reportJson(
multi_pages: usize,
results: []const Result,
) void {
- var out: std.ArrayList(u8) = .empty;
- defer out.deinit(gpa);
+ var storage: [report_max_bytes]u8 = undefined;
+ var out: std.Io.Writer = .fixed(&storage);
const path_json = std.json.Stringify.valueAlloc(gpa, config.path, .{}) catch return;
defer gpa.free(path_json);
out.print(
- gpa,
"{{\"benchmark\":\"pardes-pdf-render\",\"build\":\"ReleaseFast\"," ++
"\"path\":{s},\"document_pages\":{d},\"selected_page\":{d}," ++
"\"multi_pages\":{d},\"warmup\":{d},\"reps\":{d}," ++
@@ -579,7 +576,6 @@ fn reportJson(
) catch return;
for (profiles, 0..) |profile, i| {
out.print(
- gpa,
"{s}{{\"id\":\"{s}\",\"dpi\":{d},\"minimum_width\":{d}," ++
"\"minimum_height\":{d},\"max_dimension\":{d}}}",
.{
@@ -592,10 +588,9 @@ fn reportJson(
},
) catch return;
}
- out.appendSlice(gpa, "],\"results\":[") catch return;
+ out.writeAll("],\"results\":[") catch return;
for (results, 0..) |result, i| {
out.print(
- gpa,
"{s}{{\"profile\":\"{s}\",\"scenario\":\"{s}\"," ++
"\"min_ns\":{d},\"median_ns\":{d},\"p90_ns\":{d}," ++
"\"max_ns\":{d},\"sampled_ns\":{d},\"pixels\":{d}," ++
@@ -618,8 +613,8 @@ fn reportJson(
},
) catch return;
}
- out.appendSlice(gpa, "]}\n") catch return;
- std.Io.File.stdout().writeStreamingAll(io, out.items) catch {};
+ out.writeAll("]}\n") catch return;
+ std.Io.File.stdout().writeStreamingAll(io, out.buffered()) catch {};
}
fn fatal(comptime format: []const u8, args: anytype) noreturn {
diff --git a/test/pdf_sections_bench.zig b/test/pdf_sections_bench.zig
index 21327c9f..9d44a3dd 100644
--- a/test/pdf_sections_bench.zig
+++ b/test/pdf_sections_bench.zig
@@ -116,8 +116,8 @@ fn parseCount(flag: []const u8, text: []const u8) usize {
}
fn measureOutline(path: []const u8, config: Config) !Result {
- var samples: std.ArrayList(u64) = .empty;
- defer samples.deinit(gpa);
+ const samples = try gpa.alloc(u64, config.reps);
+ defer gpa.free(samples);
var expected: ?Identity = null;
for (0..config.warmup + config.reps) |round| {
// Document open/close is setup: this row isolates MuPDF outline load,
@@ -130,9 +130,9 @@ fn measureOutline(path: []const u8, config: Config) !Result {
const identity = outlineIdentity(outline.entries);
outline.deinit(gpa);
try verifyIdentity("outline-load-flatten-real", &expected, identity, round);
- if (round >= config.warmup) try samples.append(gpa, @max(1, elapsed));
+ if (round >= config.warmup) samples[round - config.warmup] = @max(1, elapsed);
}
- return summarize("outline-load-flatten-real", 1, samples.items, expected.?);
+ return summarize("outline-load-flatten-real", 1, samples, expected.?);
}
fn measureMaterialize(
@@ -140,8 +140,8 @@ fn measureMaterialize(
entries: []const pdf.OutlineEntry,
config: Config,
) !Result {
- var samples: std.ArrayList(u64) = .empty;
- defer samples.deinit(gpa);
+ const samples = try gpa.alloc(u64, config.reps);
+ defer gpa.free(samples);
var expected: ?Identity = null;
for (0..config.warmup + config.reps) |round| {
const started = nowNs();
@@ -150,17 +150,17 @@ fn measureMaterialize(
const identity = contentIdentity(content);
gpa.free(content);
try verifyIdentity("sections-first-materialize", &expected, identity, round);
- if (round >= config.warmup) try samples.append(gpa, @max(1, elapsed));
+ if (round >= config.warmup) samples[round - config.warmup] = @max(1, elapsed);
}
- return summarize("sections-first-materialize", 1, samples.items, expected.?);
+ return summarize("sections-first-materialize", 1, samples, expected.?);
}
fn measureCachedReopen(path: []const u8, config: Config) !Result {
const core = try preparedCore(path);
defer core.deinit();
const prepared = preparedReopen(core);
- var samples: std.ArrayList(u64) = .empty;
- defer samples.deinit(gpa);
+ const samples = try gpa.alloc(u64, config.reps);
+ defer gpa.free(samples);
var expected: ?Identity = null;
for (0..config.warmup + config.reps) |round| {
var observed: u64 = 0;
@@ -182,9 +182,9 @@ fn measureCachedReopen(path: []const u8, config: Config) !Result {
identity.checksum = mix(identity.checksum, observed);
try verifyIdentity("sections-clean-cached-reopen", &expected, identity, round);
if (round >= config.warmup)
- try samples.append(gpa, @max(1, elapsed / cached_batch));
+ samples[round - config.warmup] = @max(1, elapsed / cached_batch);
}
- return summarize("sections-clean-cached-reopen", cached_batch, samples.items, expected.?);
+ return summarize("sections-clean-cached-reopen", cached_batch, samples, expected.?);
}
/// Cost floor for the anti-elision workload above. This performs the same
@@ -194,8 +194,8 @@ fn measureCachedControl(path: []const u8, config: Config) !Result {
const core = try preparedCore(path);
defer core.deinit();
const prepared = preparedReopen(core);
- var samples: std.ArrayList(u64) = .empty;
- defer samples.deinit(gpa);
+ const samples = try gpa.alloc(u64, config.reps);
+ defer gpa.free(samples);
var expected: ?Identity = null;
for (0..config.warmup + config.reps) |round| {
var observed: u64 = 0;
@@ -212,17 +212,17 @@ fn measureCachedControl(path: []const u8, config: Config) !Result {
identity.checksum = mix(identity.checksum, observed);
try verifyIdentity("sections-perturb-observe-control", &expected, identity, round);
if (round >= config.warmup)
- try samples.append(gpa, @max(1, elapsed / cached_batch));
+ samples[round - config.warmup] = @max(1, elapsed / cached_batch);
}
- return summarize("sections-perturb-observe-control", cached_batch, samples.items, expected.?);
+ return summarize("sections-perturb-observe-control", cached_batch, samples, expected.?);
}
fn measureOrdinalStep(path: []const u8, config: Config) !Result {
const core = try preparedCore(path);
defer core.deinit();
const owner = core.panes[0].?;
- var samples: std.ArrayList(u64) = .empty;
- defer samples.deinit(gpa);
+ const samples = try gpa.alloc(u64, config.reps);
+ defer gpa.free(samples);
var expected: ?Identity = null;
for (0..config.warmup + config.reps) |round| {
owner.search_row = null;
@@ -244,14 +244,14 @@ fn measureOrdinalStep(path: []const u8, config: Config) !Result {
const identity = stepIdentity(core);
try verifyIdentity("sections-ordinal-destination-step", &expected, identity, round);
if (round >= config.warmup)
- try samples.append(gpa, @max(1, elapsed / step_batch));
+ samples[round - config.warmup] = @max(1, elapsed / step_batch);
}
- return summarize("sections-ordinal-destination-step", step_batch, samples.items, expected.?);
+ return summarize("sections-ordinal-destination-step", step_batch, samples, expected.?);
}
fn measureWorstShape(entries: []const pdf.OutlineEntry, config: Config) !Result {
- var samples: std.ArrayList(u64) = .empty;
- defer samples.deinit(gpa);
+ const samples = try gpa.alloc(u64, config.reps);
+ defer gpa.free(samples);
var expected: ?Identity = null;
for (0..config.warmup + config.reps) |round| {
const started = nowNs();
@@ -260,9 +260,9 @@ fn measureWorstShape(entries: []const pdf.OutlineEntry, config: Config) !Result
const identity = contentIdentity(content);
gpa.free(content);
try verifyIdentity("rows-near-limit-worst-shape", &expected, identity, round);
- if (round >= config.warmup) try samples.append(gpa, @max(1, elapsed));
+ if (round >= config.warmup) samples[round - config.warmup] = @max(1, elapsed);
}
- return summarize("rows-near-limit-worst-shape", 1, samples.items, expected.?);
+ return summarize("rows-near-limit-worst-shape", 1, samples, expected.?);
}
fn preparedCore(path: []const u8) !*pardes.Pardes {
@@ -495,14 +495,15 @@ fn reportText(config: Config, results: []const Result) void {
std.debug.print("\nBatched rows divide elapsed time by operations/sample: cached={d}, step={d}.\n", .{ cached_batch, step_batch });
}
+const json_report_max_bytes = 16 * 1024;
+
fn reportJson(io: std.Io, config: Config, results: []const Result) void {
- var out: std.ArrayList(u8) = .empty;
- defer out.deinit(gpa);
- out.print(gpa, "{{\"benchmark\":\"pardes-pdf-sections\",\"build\":\"ReleaseFast\",\"warmup\":{d},\"reps\":{d},\"results\":[", .{
+ var storage: [json_report_max_bytes]u8 = undefined;
+ var out: std.Io.Writer = .fixed(&storage);
+ out.print("{{\"benchmark\":\"pardes-pdf-sections\",\"build\":\"ReleaseFast\",\"warmup\":{d},\"reps\":{d},\"results\":[", .{
config.warmup, config.reps,
}) catch return;
for (results, 0..) |result, i| out.print(
- gpa,
"{s}{{\"scenario\":\"{s}\",\"operations_per_sample\":{d},\"min_ns\":{d},\"median_ns\":{d},\"p90_ns\":{d},\"max_ns\":{d},\"items\":{d},\"bytes\":{d},\"checksum\":\"{x:0>16}\"}}",
.{
if (i == 0) "" else ",",
@@ -517,8 +518,8 @@ fn reportJson(io: std.Io, config: Config, results: []const Result) void {
result.identity.checksum,
},
) catch return;
- out.appendSlice(gpa, "]}\n") catch return;
- std.Io.File.stdout().writeStreamingAll(io, out.items) catch {};
+ out.writeAll("]}\n") catch return;
+ std.Io.File.stdout().writeStreamingAll(io, out.buffered()) catch {};
}
fn writeFile(path: []const u8, bytes: []const u8) !void {
diff --git a/test/perf.zig b/test/perf.zig
index 368e6ce1..bf5f0200 100644
--- a/test/perf.zig
+++ b/test/perf.zig
@@ -166,8 +166,8 @@ pub fn main(init: std.process.Init) !void {
/// big a difference has to be before it is real.
fn measure(op: Op, fx: Fixture, path: []const u8, reps: usize) !Cell {
const warmup = 3;
- var samples: std.ArrayList(u64) = .empty;
- defer samples.deinit(gpa);
+ const samples = try gpa.alloc(u64, reps);
+ defer gpa.free(samples);
if (op == .open) {
// fresh core per sample: opening is a one-shot, and the second Look at
@@ -180,9 +180,9 @@ fn measure(op: Op, fx: Fixture, path: []const u8, reps: usize) !Cell {
pump(core);
_ = try frame(core);
const dt = nowNs() -| t0;
- if (n >= warmup) try samples.append(gpa, dt / 1000);
+ if (n >= warmup) samples[n - warmup] = dt / 1000;
}
- return summarize(samples.items);
+ return summarize(samples);
}
const core = try boot();
@@ -217,9 +217,9 @@ fn measure(op: Op, fx: Fixture, path: []const u8, reps: usize) !Cell {
pump(core);
_ = try frame(core);
const dt = nowNs() -| t0;
- if (n >= warmup) try samples.append(gpa, dt / 1000);
+ if (n >= warmup) samples[n - warmup] = dt / 1000;
}
- return summarize(samples.items);
+ return summarize(samples);
}
/// Park the cursor and the view at sample `n`'s position, walked across the
@@ -291,22 +291,39 @@ fn summarize(samples: []u64) Cell {
/// lines get their width from a comment tail — the alternative (an enormous
/// string literal) makes the whole file one token and flatters every scan that
/// looks for a newline.
+fn generatedPrefixLen(n: usize) usize {
+ return switch (n % 4) {
+ 0 => std.fmt.count("const value_{d}: u32 = {d}; // ", .{ n, n *% 2654435761 }),
+ 1 => std.fmt.count("pub fn helper_{d}(a: u32, b: u32) u32 {{ return a +% b *% {d}; }} // ", .{ n, n }),
+ 2 => std.fmt.count(" const text_{d} = \"lorem ipsum dolor sit amet {d}\"; // ", .{ n, n }),
+ else => std.fmt.count("// comment line {d} — ", .{n}),
+ };
+}
+
+fn writeGeneratedPrefix(out: *std.Io.Writer, n: usize) !void {
+ switch (n % 4) {
+ 0 => try out.print("const value_{d}: u32 = {d}; // ", .{ n, n *% 2654435761 }),
+ 1 => try out.print("pub fn helper_{d}(a: u32, b: u32) u32 {{ return a +% b *% {d}; }} // ", .{ n, n }),
+ 2 => try out.print(" const text_{d} = \"lorem ipsum dolor sit amet {d}\"; // ", .{ n, n }),
+ else => try out.print("// comment line {d} — ", .{n}),
+ }
+}
+
fn generate(fx: Fixture) ![]u8 {
- var out: std.ArrayList(u8) = .empty;
- try out.ensureTotalCapacity(gpa, fx.lines * (fx.cols + 1) + 64);
- var n: usize = 0;
- while (n < fx.lines) : (n += 1) {
- const start = out.items.len;
- switch (n % 4) {
- 0 => try out.print(gpa, "const value_{d}: u32 = {d}; // ", .{ n, n *% 2654435761 }),
- 1 => try out.print(gpa, "pub fn helper_{d}(a: u32, b: u32) u32 {{ return a +% b *% {d}; }} // ", .{ n, n }),
- 2 => try out.print(gpa, " const text_{d} = \"lorem ipsum dolor sit amet {d}\"; // ", .{ n, n }),
- else => try out.print(gpa, "// comment line {d} — ", .{n}),
- }
- while (out.items.len - start < fx.cols) try out.append(gpa, 'x');
- try out.append(gpa, '\n');
+ var byte_len: usize = 0;
+ for (0..fx.lines) |n| byte_len += @max(generatedPrefixLen(n), fx.cols) + 1;
+ const text = try gpa.alloc(u8, byte_len);
+ errdefer gpa.free(text);
+ var out: std.Io.Writer = .fixed(text);
+ for (0..fx.lines) |n| {
+ const start = out.end;
+ try writeGeneratedPrefix(&out, n);
+ const prefix_len = out.end - start;
+ if (prefix_len < fx.cols) try out.splatByteAll('x', fx.cols - prefix_len);
+ try out.writeByte('\n');
}
- return out.toOwnedSlice(gpa);
+ std.debug.assert(out.end == text.len);
+ return text;
}
fn writeFile(path: []const u8, text: []const u8) !void {
@@ -387,28 +404,30 @@ fn reportText(cells: *const [std.enums.values(Op).len][fixtures.len]Cell, bytes:
/// Real stdout, not std.debug.print's stderr: this is the form `--base` reads
/// back, and `zig build perf -- --json > runs/old.json` writing an empty file
/// would make the next comparison silently print no ratios at all.
+const json_report_max_bytes = 32 * 1024;
+
fn reportJson(io: std.Io, cells: *const [std.enums.values(Op).len][fixtures.len]Cell, bytes: *const [fixtures.len]usize, reps: usize) void {
- var out: std.ArrayList(u8) = .empty;
- defer out.deinit(gpa);
- out.print(gpa, "{{\"cols\":{d},\"rows\":{d},\"reps\":{d},\"fixtures\":[", .{ screen_cols, screen_rows, reps }) catch return;
+ var storage: [json_report_max_bytes]u8 = undefined;
+ var out: std.Io.Writer = .fixed(&storage);
+ out.print("{{\"cols\":{d},\"rows\":{d},\"reps\":{d},\"fixtures\":[", .{ screen_cols, screen_rows, reps }) catch return;
for (fixtures, 0..) |fx, fi| {
- out.print(gpa, "{s}{{\"name\":\"{s}\",\"lines\":{d},\"cols\":{d},\"bytes\":{d}}}", .{
+ out.print("{s}{{\"name\":\"{s}\",\"lines\":{d},\"cols\":{d},\"bytes\":{d}}}", .{
if (fi > 0) "," else "", fx.name, fx.lines, fx.cols, bytes[fi],
}) catch return;
}
- out.appendSlice(gpa, "],\"cells\":[") catch return;
+ out.writeAll("],\"cells\":[") catch return;
var first = true;
for (std.enums.values(Op), 0..) |op, oi| {
for (fixtures, 0..) |fx, fi| {
const c = cells[oi][fi];
- out.print(gpa, "{s}{{\"op\":\"{s}\",\"fixture\":\"{s}\",\"min_us\":{d},\"med_us\":{d},\"p90_us\":{d},\"max_us\":{d}}}", .{
+ out.print("{s}{{\"op\":\"{s}\",\"fixture\":\"{s}\",\"min_us\":{d},\"med_us\":{d},\"p90_us\":{d},\"max_us\":{d}}}", .{
if (first) "" else ",", op.label(), fx.name, c.min_us, c.med_us, c.p90_us, c.max_us,
}) catch return;
first = false;
}
}
- out.appendSlice(gpa, "]}\n") catch return;
- std.Io.File.stdout().writeStreamingAll(io, out.items) catch {};
+ out.writeAll("]}\n") catch return;
+ std.Io.File.stdout().writeStreamingAll(io, out.buffered()) catch {};
}
const Base = struct { med: [std.enums.values(Op).len][fixtures.len]u64 };
@@ -420,6 +439,7 @@ const Base = struct { med: [std.enums.values(Op).len][fixtures.len]u64 };
/// and did not get is worse than no comparison.
fn readBase(path: []const u8) ?Base {
const src = readFileAlloc(path) catch fatal("--base: cannot read {s}", .{path});
+ defer gpa.free(src);
var b: Base = .{ .med = @splat(@splat(0)) };
const parsed = std.json.parseFromSlice(struct {
cells: []const struct {
@@ -446,18 +466,22 @@ fn readFileAlloc(path: []const u8) ![]u8 {
const fd = libc.open(path_z, .{ .ACCMODE = .RDONLY });
if (fd < 0) return error.OpenFailed;
defer _ = libc.close(fd);
- var buf: std.ArrayList(u8) = .empty;
- var chunk: [16384]u8 = undefined;
- while (true) {
- const n = libc.read(fd, &chunk, chunk.len);
+ const end = libc.lseek(fd, 0, libc.SEEK.END);
+ if (end < 0 or libc.lseek(fd, 0, libc.SEEK.SET) < 0) return error.StatFailed;
+ const size = std.math.cast(usize, end) orelse return error.FileTooLarge;
+ const buf = try gpa.alloc(u8, size);
+ errdefer gpa.free(buf);
+ var offset: usize = 0;
+ while (offset < buf.len) {
+ const n = libc.read(fd, buf.ptr + offset, buf.len - offset);
if (n < 0) {
if (libc.errno(n) == .INTR) continue;
return error.ReadFailed;
}
- if (n == 0) break;
- try buf.appendSlice(gpa, chunk[0..@intCast(n)]);
+ if (n == 0) return error.UnexpectedEndOfFile;
+ offset += @intCast(n);
}
- return buf.items;
+ return buf;
}
fn fatal(comptime fmt: []const u8, args: anytype) noreturn {
diff --git a/test/snapshot.zig b/test/snapshot.zig
index 36a5b20b..c3611291 100644
--- a/test/snapshot.zig
+++ b/test/snapshot.zig
@@ -91,7 +91,7 @@ pub fn main(init: std.process.Init) !void {
// pass costs more than the extra parallelism saves.
const cpus = std.Thread.getCpuCount() catch 4;
var jobs: usize = cpus + cpus / 4;
- var scripts: std.ArrayList([]const u8) = .empty;
+ var script_count: usize = 0;
for (args[1..]) |a| {
if (std.mem.eql(u8, a, "--update")) {
update = true;
@@ -102,28 +102,42 @@ pub fn main(init: std.process.Init) !void {
} else if (exe == null) {
exe = try absPath(arena, orig_cwd, a);
} else {
- try scripts.append(arena, try absPath(arena, orig_cwd, a));
+ script_count += 1;
}
}
+ var scripts = try arena.alloc([]const u8, script_count);
+ var script_i: usize = 0;
+ var saw_exe = false;
+ for (args[1..]) |a| {
+ if (std.mem.eql(u8, a, "--update") or
+ std.mem.eql(u8, a, "--trace-stable") or
+ std.mem.startsWith(u8, a, "--jobs=")) continue;
+ if (!saw_exe) {
+ saw_exe = true;
+ continue;
+ }
+ scripts[script_i] = try absPath(arena, orig_cwd, a);
+ script_i += 1;
+ }
if (exe == null) fatal("usage: pardes-snap <exe> [scripts...] [--update] [--jobs=N]", .{});
const exe_z = try arena.dupeZ(u8, exe.?);
- if (scripts.items.len == 0) {
- try listSnaps(arena, try absPath(arena, orig_cwd, "test/snapshots"), &scripts);
- std.mem.sort([]const u8, scripts.items, {}, struct {
+ if (scripts.len == 0) {
+ scripts = try listSnaps(arena, try absPath(arena, orig_cwd, "test/snapshots"));
+ std.mem.sort([]const u8, scripts, {}, struct {
fn lt(_: void, x: []const u8, y: []const u8) bool {
return std.mem.lessThan(u8, x, y);
}
}.lt);
}
- if (scripts.items.len == 0) fatal("no .snap scripts found", .{});
+ if (scripts.len == 0) fatal("no .snap scripts found", .{});
try mkdir(arena, SNAP_BASE, true);
// one run at a time (this is the flock that used to be typed by hand)
_ = lockOrWait(SNAP_BASE ++ "/.run.lock", "another pardes-snap run");
- const codes = try arena.alloc(u8, scripts.items.len);
- try runBatch(arena, exe_z, toBatch(arena, scripts.items), update, jobs, codes);
+ const codes = try arena.alloc(u8, scripts.len);
+ try runBatch(arena, exe_z, toBatch(arena, scripts), update, jobs, codes);
// A failure in the parallel pass is more often contention than regression:
// the app can lose a race with a kworker while 20 siblings fight for the
@@ -135,11 +149,21 @@ pub fn main(init: std.process.Init) !void {
if (jobs > 1) {
var round: usize = 0;
while (round < 2) : (round += 1) {
- var retry: std.ArrayList(usize) = .empty;
- for (codes, 0..) |c, i| if (c != 0) try retry.append(arena, i);
- if (retry.items.len == 0 or retry.items.len > 10) break;
- const paths = try arena.alloc(Idx, retry.items.len);
- for (retry.items, paths, 0..) |i, *p, n| p.* = .{ .path = scripts.items[i], .code_idx = n };
+ var retry_count: usize = 0;
+ for (codes) |c| {
+ if (c != 0) retry_count += 1;
+ }
+ if (retry_count == 0 or retry_count > 10) break;
+ const retry = try arena.alloc(usize, retry_count);
+ var retry_i: usize = 0;
+ for (codes, 0..) |c, i| {
+ if (c != 0) {
+ retry[retry_i] = i;
+ retry_i += 1;
+ }
+ }
+ const paths = try arena.alloc(Idx, retry.len);
+ for (retry, paths, 0..) |i, *p, n| p.* = .{ .path = scripts[i], .code_idx = n };
std.debug.print("re-running {d} failed script(s) serially{s}:", .{
paths.len,
if (round == 0) "" else " with wider settle probes",
@@ -149,12 +173,12 @@ pub fn main(init: std.process.Init) !void {
careful = round > 0;
const recodes = try arena.alloc(u8, paths.len);
try runBatch(arena, exe_z, paths, update, 1, recodes);
- for (retry.items, recodes) |i, c| codes[i] = c;
+ for (retry, recodes) |i, c| codes[i] = c;
}
}
var failed: usize = 0;
- for (scripts.items, codes) |script_path, code| {
+ for (scripts, codes) |script_path, code| {
const report = readFileAlloc(arena, reportPath(arena, script_path) catch "") catch "";
std.debug.print("{s}", .{report});
if (code != 0) {
@@ -164,10 +188,10 @@ pub fn main(init: std.process.Init) !void {
}
}
if (failed > 0) {
- std.debug.print("{d}/{d} snapshot scripts FAILED\n", .{ failed, scripts.items.len });
+ std.debug.print("{d}/{d} snapshot scripts FAILED\n", .{ failed, scripts.len });
std.process.exit(1);
}
- std.debug.print("all {d} snapshot scripts ok\n", .{scripts.items.len});
+ std.debug.print("all {d} snapshot scripts ok\n", .{scripts.len});
}
/// Run `scripts` `jobs` at a time, one forked child each, filling `codes` with
@@ -198,24 +222,26 @@ fn runBatch(
codes: []u8,
) !void {
const Job = struct { pid: libc.pid_t, idx: usize };
- var running: std.ArrayList(Job) = .empty;
- defer running.deinit(arena);
+ const running = try arena.alloc(Job, @min(jobs, scripts.len));
+ var running_len: usize = 0;
var next: usize = 0;
- while (next < scripts.len or running.items.len > 0) {
- while (next < scripts.len and running.items.len < jobs) : (next += 1) {
+ while (next < scripts.len or running_len > 0) {
+ while (next < scripts.len and running_len < running.len) : (next += 1) {
const idx = next;
const pid = libc.fork();
if (pid == 0) std.process.exit(runOne(arena, exe_z, scripts[idx].path, update));
if (pid < 0) fatal("fork failed", .{});
- try running.append(arena, .{ .pid = pid, .idx = scripts[idx].code_idx });
+ running[running_len] = .{ .pid = pid, .idx = scripts[idx].code_idx };
+ running_len += 1;
}
var status: c_int = 0;
const pid = libc.waitpid(-1, &status, 0);
- for (running.items, 0..) |j, i| {
+ for (running[0..running_len], 0..) |j, i| {
if (j.pid != pid) continue;
// a killed child (crash in the harness itself) counts as a failure
codes[j.idx] = if (status == 0) 0 else 1;
- _ = running.swapRemove(i);
+ running_len -= 1;
+ running[i] = running[running_len];
break;
}
}
@@ -337,7 +363,8 @@ fn runScript(arena: std.mem.Allocator, exe_z: [:0]const u8, script_path: []const
_ = setenv("PARDES_DUMP", try arena.dupeZ(u8, dump_path), 1);
if (chdirRc(try arena.dupeZ(u8, work)) < 0) return error.ChdirFailed;
- var out: std.ArrayList(u8) = .empty;
+ const chunks = try arena.alloc([]const u8, countCaptures(src));
+ var chunk_len: usize = 0;
var h: ?eh.Harness = null;
defer if (h) |*hp| hp.deinit();
@@ -368,9 +395,8 @@ fn runScript(arena: std.mem.Allocator, exe_z: [:0]const u8, script_path: []const
// one) — a file that is long AND wide, which is what a horizontal
// scroll test needs and `file` can only give as one huge script line
const tail = tok.rest();
- var buf: std.ArrayList(u8) = .empty;
- for (1..n + 1) |i| try buf.appendSlice(arena, try std.fmt.allocPrint(arena, "line {d}{s}\n", .{ i, tail }));
- try eh.writeFile(try arena.dupeZ(u8, name), buf.items);
+ const buf = try numberedLines(arena, n, tail);
+ try eh.writeFile(try arena.dupeZ(u8, name), buf);
} else if (std.mem.eql(u8, cmd, "ppmdiag")) {
// The diagonal-split test PPM from the old e2e (white over blue),
// grown to 512x512 with the white half filled by a fixed LCG
@@ -381,19 +407,8 @@ fn runScript(arena: std.mem.Allocator, exe_z: [:0]const u8, script_path: []const
// the art, which a 64x64 image cannot show (its cells average
// down to flat color and a block always wins).
const name = tok.next() orelse return error.BadScript;
- var ppm: std.ArrayList(u8) = .empty;
- try ppm.appendSlice(arena, "P6\n512 512\n255\n");
- var seed: u64 = 12345;
- var py: usize = 0;
- while (py < 512) : (py += 1) {
- var px: usize = 0;
- while (px < 512) : (px += 1) {
- seed = seed *% 1103515245 +% 12345;
- const v: u8 = @truncate(seed >> 16);
- try ppm.appendSlice(arena, if (px + py < 512) &[_]u8{ v, v, v } else &[_]u8{ 0, 0, 255 });
- }
- }
- try eh.writeFile(try arena.dupeZ(u8, name), ppm.items);
+ const ppm = try ppmDiag(arena);
+ try eh.writeFile(try arena.dupeZ(u8, name), ppm);
} else if (std.mem.eql(u8, cmd, "dirmk")) {
try mkdir(arena, tok.next() orelse return error.BadScript, false);
} else if (std.mem.eql(u8, cmd, "run")) {
@@ -418,22 +433,10 @@ fn runScript(arena: std.mem.Allocator, exe_z: [:0]const u8, script_path: []const
} else if (std.mem.eql(u8, cmd, "snapfile")) {
const label = tok.next() orelse return error.BadScript;
const contents = try readFileAlloc(arena, tok.rest());
- try out.appendSlice(arena, try std.fmt.allocPrint(arena, "== file {s}\n", .{label}));
- // elide base64 payloads: they encode raw pty byte HISTORY, whose
+ // Elide base64 payloads: they encode raw pty byte HISTORY, whose
// micro-timing (prompt redraw cycles) legitimately differs between
// implementations; the cleaned text fields are the contract.
- var flines = std.mem.splitScalar(u8, contents, '\n');
- while (flines.next()) |fl| {
- if (std.mem.indexOf(u8, fl, "_b64 = \"") != null) {
- const eq = std.mem.indexOf(u8, fl, "=").?;
- try out.appendSlice(arena, fl[0 .. eq + 1]);
- try out.appendSlice(arena, " <elided>\n");
- } else {
- try out.appendSlice(arena, fl);
- try out.append(arena, '\n');
- }
- }
- try out.appendSlice(arena, "== end file\n");
+ try appendChunk(chunks, &chunk_len, try snapFile(arena, label, contents));
} else if (std.mem.eql(u8, cmd, "wait")) {
const ms = try std.fmt.parseInt(i64, tok.next() orelse return error.BadScript, 10);
try live(&h).expectWaitFor(tok.rest(), ms, "snapshot wait");
@@ -471,15 +474,119 @@ fn runScript(arena: std.mem.Allocator, exe_z: [:0]const u8, script_path: []const
const cols = try std.fmt.parseInt(u16, tok.next() orelse return error.BadScript, 10);
try live(&h).resize(rows, cols);
} else if (std.mem.eql(u8, cmd, "snap")) {
- try snapText(arena, &out, live(&h), tok.rest());
+ try appendChunk(chunks, &chunk_len, try snapText(arena, live(&h), tok.rest()));
} else if (std.mem.eql(u8, cmd, "snapstyle")) {
- try snapStyle(arena, &out, live(&h), tok.rest());
+ try appendChunk(chunks, &chunk_len, try snapStyle(arena, live(&h), tok.rest()));
} else {
std.debug.print("unknown command {s}\n", .{cmd});
return error.BadScript;
}
}
- return out.items;
+ return concatChunks(arena, chunks[0..chunk_len]);
+}
+
+fn countCaptures(src: []const u8) usize {
+ var count: usize = 0;
+ var lines = std.mem.splitScalar(u8, src, '\n');
+ while (lines.next()) |raw_line| {
+ const line = std.mem.trim(u8, raw_line, " \t\r");
+ if (line.len == 0 or line[0] == '#') continue;
+ var tok = std.mem.tokenizeScalar(u8, line, ' ');
+ const cmd = tok.next().?;
+ if (std.mem.eql(u8, cmd, "snap") or
+ std.mem.eql(u8, cmd, "snapstyle") or
+ std.mem.eql(u8, cmd, "snapfile")) count += 1;
+ }
+ return count;
+}
+
+fn appendChunk(chunks: [][]const u8, len: *usize, chunk: []const u8) !void {
+ if (len.* == chunks.len) return error.BadScript;
+ chunks[len.*] = chunk;
+ len.* += 1;
+}
+
+fn concatChunks(arena: std.mem.Allocator, chunks: []const []const u8) ![]u8 {
+ var total: usize = 0;
+ for (chunks) |chunk| total += chunk.len;
+ const out = try arena.alloc(u8, total);
+ var pos: usize = 0;
+ for (chunks) |chunk| copyBytes(out, &pos, chunk);
+ return out;
+}
+
+fn copyBytes(out: []u8, pos: *usize, bytes: []const u8) void {
+ @memcpy(out[pos.*..][0..bytes.len], bytes);
+ pos.* += bytes.len;
+}
+
+fn decimalDigits(value: usize) usize {
+ var v = value;
+ var digits: usize = 1;
+ while (v >= 10) : (digits += 1) v /= 10;
+ return digits;
+}
+
+fn numberedLines(arena: std.mem.Allocator, n: usize, tail: []const u8) ![]u8 {
+ var total: usize = 0;
+ for (1..n + 1) |i| total += "line ".len + decimalDigits(i) + tail.len + 1;
+ const out = try arena.alloc(u8, total);
+ var pos: usize = 0;
+ for (1..n + 1) |i| {
+ const line = try std.fmt.bufPrint(out[pos..], "line {d}{s}\n", .{ i, tail });
+ pos += line.len;
+ }
+ return out;
+}
+
+fn ppmDiag(arena: std.mem.Allocator) ![]u8 {
+ const header = "P6\n512 512\n255\n";
+ const out = try arena.alloc(u8, header.len + 512 * 512 * 3);
+ @memcpy(out[0..header.len], header);
+ var pos = header.len;
+ var seed: u64 = 12345;
+ var py: usize = 0;
+ while (py < 512) : (py += 1) {
+ var px: usize = 0;
+ while (px < 512) : (px += 1) {
+ seed = seed *% 1103515245 +% 12345;
+ const v: u8 = @truncate(seed >> 16);
+ const pixel: [3]u8 = if (px + py < 512) .{ v, v, v } else .{ 0, 0, 255 };
+ @memcpy(out[pos..][0..pixel.len], &pixel);
+ pos += pixel.len;
+ }
+ }
+ return out;
+}
+
+fn snapFile(arena: std.mem.Allocator, label: []const u8, contents: []const u8) ![]u8 {
+ const header = try std.fmt.allocPrint(arena, "== file {s}\n", .{label});
+ var total = header.len + "== end file\n".len;
+ var lines = std.mem.splitScalar(u8, contents, '\n');
+ while (lines.next()) |line| {
+ if (std.mem.indexOf(u8, line, "_b64 = \"") != null) {
+ const eq = std.mem.indexOfScalar(u8, line, '=').?;
+ total += eq + 1 + " <elided>\n".len;
+ } else {
+ total += line.len + 1;
+ }
+ }
+ const out = try arena.alloc(u8, total);
+ var pos: usize = 0;
+ copyBytes(out, &pos, header);
+ lines = std.mem.splitScalar(u8, contents, '\n');
+ while (lines.next()) |line| {
+ if (std.mem.indexOf(u8, line, "_b64 = \"") != null) {
+ const eq = std.mem.indexOfScalar(u8, line, '=').?;
+ copyBytes(out, &pos, line[0 .. eq + 1]);
+ copyBytes(out, &pos, " <elided>\n");
+ } else {
+ copyBytes(out, &pos, line);
+ copyBytes(out, &pos, "\n");
+ }
+ }
+ copyBytes(out, &pos, "== end file\n");
+ return out;
}
fn live(h: *?eh.Harness) *eh.Harness {
@@ -649,48 +756,78 @@ fn stateKey(h: *eh.Harness) ![]u8 {
return std.fmt.allocPrint(h.gpa, "{d},{d},{x}\n{s}", .{ c.x, c.y, styles.final(), text });
}
-fn snapText(arena: std.mem.Allocator, out: *std.ArrayList(u8), h: *eh.Harness, label: []const u8) !void {
+fn snapText(arena: std.mem.Allocator, h: *eh.Harness, label: []const u8) ![]u8 {
const text = try h.screenText();
defer h.gpa.free(text);
const c = h.cursor();
- try out.appendSlice(arena, try std.fmt.allocPrint(arena, "== snap {s} grid={d}x{d} cursor={d},{d}\n", .{ label, h.cols, h.rows, c.x, c.y }));
+ const header = try std.fmt.allocPrint(arena, "== snap {s} grid={d}x{d} cursor={d},{d}\n", .{ label, h.cols, h.rows, c.x, c.y });
+ var total = header.len;
var it = std.mem.splitScalar(u8, text, '\n');
var row: usize = 0;
while (row < h.rows) : (row += 1) {
const line = it.next() orelse "";
- try out.append(arena, '|');
- try out.appendSlice(arena, std.mem.trimEnd(u8, line, " "));
- try out.append(arena, '\n');
+ total += 1 + std.mem.trimEnd(u8, line, " ").len + 1;
}
+ const out = try arena.alloc(u8, total);
+ var pos: usize = 0;
+ copyBytes(out, &pos, header);
+ it = std.mem.splitScalar(u8, text, '\n');
+ row = 0;
+ while (row < h.rows) : (row += 1) {
+ const line = it.next() orelse "";
+ copyBytes(out, &pos, "|");
+ copyBytes(out, &pos, std.mem.trimEnd(u8, line, " "));
+ copyBytes(out, &pos, "\n");
+ }
+ return out;
}
/// Per-row style runs from the harness emulator: `|y: x0-x1 fg,bg,attrs ...`.
/// fg/bg: d (default) | pN (palette) | #rrggbb. attrs: b f i k r v x + u U ~ . -
/// for underline single/double/curly/dotted/dashed.
-fn snapStyle(arena: std.mem.Allocator, out: *std.ArrayList(u8), h: *eh.Harness, label: []const u8) !void {
- try out.appendSlice(arena, try std.fmt.allocPrint(arena, "== style {s}\n", .{label}));
+fn snapStyle(arena: std.mem.Allocator, h: *eh.Harness, label: []const u8) ![]u8 {
+ const header = try std.fmt.allocPrint(arena, "== style {s}\n", .{label});
+ const rows_len = styleRows(h, null);
+ const out = try arena.alloc(u8, header.len + rows_len);
+ var pos: usize = 0;
+ copyBytes(out, &pos, header);
+ _ = styleRows(h, out[pos..]);
+ return out;
+}
+
+fn styleRows(h: *eh.Harness, dst: ?[]u8) usize {
+ var pos: usize = 0;
var y: u16 = 0;
while (y < h.rows) : (y += 1) {
- try out.appendSlice(arena, try std.fmt.allocPrint(arena, "|{d}:", .{y}));
- var run_buf: [64]u8 = undefined;
+ var fmt_buf: [64]u8 = undefined;
+ emitBytes(dst, &pos, std.fmt.bufPrint(&fmt_buf, "|{d}:", .{y}) catch unreachable);
var key_buf: [64]u8 = undefined;
- var run_key: []const u8 = "";
+ var run_key: [64]u8 = undefined;
+ var run_len: usize = 0;
var run_start: u16 = 0;
var x: u16 = 0;
while (x <= h.cols) : (x += 1) {
const key: []const u8 = if (x == h.cols) "\x00end" else cellKey(h, x, y, &key_buf);
if (x == 0) {
- run_key = try arena.dupe(u8, key);
+ @memcpy(run_key[0..key.len], key);
+ run_len = key.len;
continue;
}
- if (std.mem.eql(u8, key, run_key)) continue;
- try out.appendSlice(arena, try std.fmt.bufPrint(&run_buf, " {d}-{d} ", .{ run_start, x - 1 }));
- try out.appendSlice(arena, run_key);
- run_key = try arena.dupe(u8, key);
+ if (std.mem.eql(u8, key, run_key[0..run_len])) continue;
+ emitBytes(dst, &pos, std.fmt.bufPrint(&fmt_buf, " {d}-{d} ", .{ run_start, x - 1 }) catch unreachable);
+ emitBytes(dst, &pos, run_key[0..run_len]);
+ @memcpy(run_key[0..key.len], key);
+ run_len = key.len;
run_start = x;
}
- try out.append(arena, '\n');
+ emitBytes(dst, &pos, "\n");
}
+ return pos;
+}
+
+fn emitBytes(dst: ?[]u8, pos: *usize, bytes: []const u8) void {
+ if (dst) |out| @memcpy(out[pos.*..][0..bytes.len], bytes);
+ pos.* += bytes.len;
}
fn cellKey(h: *eh.Harness, x: u16, y: u16, buf: *[64]u8) []const u8 {
@@ -810,29 +947,51 @@ fn buttonCode(name: []const u8) ?u16 {
}
fn unescape(arena: std.mem.Allocator, s: []const u8) ![]const u8 {
- var buf: std.ArrayList(u8) = .empty;
+ var len: usize = 0;
var i: usize = 0;
while (i < s.len) : (i += 1) {
if (s[i] != '\\' or i + 1 >= s.len) {
- try buf.append(arena, s[i]);
+ len += 1;
continue;
}
i += 1;
switch (s[i]) {
- 'e' => try buf.append(arena, 0x1b),
- 'r' => try buf.append(arena, '\r'),
- 'n' => try buf.append(arena, '\n'),
- 't' => try buf.append(arena, '\t'),
- '\\' => try buf.append(arena, '\\'),
+ 'e', 'r', 'n', 't', '\\' => len += 1,
'x' => {
if (i + 2 >= s.len) return error.BadEscape;
- try buf.append(arena, try std.fmt.parseInt(u8, s[i + 1 .. i + 3], 16));
+ _ = try std.fmt.parseInt(u8, s[i + 1 .. i + 3], 16);
+ len += 1;
i += 2;
},
else => return error.BadEscape,
}
}
- return buf.items;
+ const out = try arena.alloc(u8, len);
+ var pos: usize = 0;
+ i = 0;
+ while (i < s.len) : (i += 1) {
+ if (s[i] != '\\' or i + 1 >= s.len) {
+ out[pos] = s[i];
+ pos += 1;
+ continue;
+ }
+ i += 1;
+ out[pos] = switch (s[i]) {
+ 'e' => 0x1b,
+ 'r' => '\r',
+ 'n' => '\n',
+ 't' => '\t',
+ '\\' => '\\',
+ 'x' => value: {
+ const value = try std.fmt.parseInt(u8, s[i + 1 .. i + 3], 16);
+ i += 2;
+ break :value value;
+ },
+ else => unreachable,
+ };
+ pos += 1;
+ }
+ return out;
}
fn absPath(arena: std.mem.Allocator, cwd: []const u8, p: []const u8) ![]const u8 {
@@ -840,15 +999,33 @@ fn absPath(arena: std.mem.Allocator, cwd: []const u8, p: []const u8) ![]const u8
return try std.fmt.allocPrint(arena, "{s}/{s}", .{ cwd, p });
}
-fn listSnaps(arena: std.mem.Allocator, dir_path: []const u8, scripts: *std.ArrayList([]const u8)) !void {
+fn listSnaps(arena: std.mem.Allocator, dir_path: []const u8) ![][]const u8 {
const dir_z = try arena.dupeZ(u8, dir_path);
+ const count = countSnapEntries(dir_z, dir_path);
+ const scripts = try arena.alloc([]const u8, count);
+ const dir = libc.opendir(dir_z) orelse fatal("no snapshots/ dir at {s}", .{dir_path});
+ defer _ = libc.closedir(dir);
+ var i: usize = 0;
+ while (libc.readdir(dir)) |d| {
+ const name = std.mem.sliceTo(@as([*:0]const u8, @ptrCast(&d.name)), 0);
+ if (!std.mem.endsWith(u8, name, ".snap")) continue;
+ if (i == scripts.len) return error.DirectoryChanged;
+ scripts[i] = try std.fmt.allocPrint(arena, "{s}/{s}", .{ dir_path, name });
+ i += 1;
+ }
+ if (i != scripts.len) return error.DirectoryChanged;
+ return scripts;
+}
+
+fn countSnapEntries(dir_z: [:0]const u8, dir_path: []const u8) usize {
const dir = libc.opendir(dir_z) orelse fatal("no snapshots/ dir at {s}", .{dir_path});
defer _ = libc.closedir(dir);
+ var count: usize = 0;
while (libc.readdir(dir)) |d| {
const name = std.mem.sliceTo(@as([*:0]const u8, @ptrCast(&d.name)), 0);
- if (std.mem.endsWith(u8, name, ".snap"))
- try scripts.append(arena, try std.fmt.allocPrint(arena, "{s}/{s}", .{ dir_path, name }));
+ if (std.mem.endsWith(u8, name, ".snap")) count += 1;
}
+ return count;
}
fn readFileAlloc(arena: std.mem.Allocator, path: []const u8) ![]u8 {
@@ -856,18 +1033,27 @@ fn readFileAlloc(arena: std.mem.Allocator, path: []const u8) ![]u8 {
const fd = libc.open(path_z, .{ .ACCMODE = .RDONLY });
if (fd < 0) return error.OpenFailed;
defer _ = libc.close(fd);
- var buf: std.ArrayList(u8) = .empty;
- var chunk: [16384]u8 = undefined;
- while (true) {
- const n = libc.read(fd, &chunk, chunk.len);
+ const end = libc.lseek(fd, 0, libc.SEEK.END);
+ if (end < 0 or libc.lseek(fd, 0, libc.SEEK.SET) < 0) return error.ReadFailed;
+ const buf = try arena.alloc(u8, @intCast(end));
+ var pos: usize = 0;
+ while (pos < buf.len) {
+ const n = libc.read(fd, buf[pos..].ptr, buf.len - pos);
if (n < 0) {
if (libc.errno(n) == .INTR) continue;
return error.ReadFailed;
}
- if (n == 0) break;
- try buf.appendSlice(arena, chunk[0..@intCast(n)]);
+ if (n == 0) return error.ReadFailed;
+ pos += @intCast(n);
+ }
+ var extra: [1]u8 = undefined;
+ while (true) {
+ const n = libc.read(fd, &extra, extra.len);
+ if (n < 0 and libc.errno(n) == .INTR) continue;
+ if (n != 0) return error.ReadFailed;
+ break;
}
- return buf.items;
+ return buf;
}
fn mkdir(arena: std.mem.Allocator, path: []const u8, exist_ok: bool) !void {