//! Warm-path read timing probe (measurement gate before any zero-parse SoA
//! rewrite of the read path).
//!
//! `zig build bench -- warm <.zit path> [--reps N] [--warmup N]`.
//!
//! The `.zit-cache` (repocache.zig) is a REPLAY cache: on a warm hit it stores
//! the crosslink INPUT records and re-runs the full derivation
//! (`RepoView.buildFromRecordBody` → `RepoState.crosslink`), rebuilding the
//! derived hashmaps from scratch. A zero-parse image would remove only that
//! residual re-derivation term. This probe measures, on a WARM read, what
//! fraction of wall-time each phase costs:
//!
//! (a) cache-body I/O — read the record body off disk + CRC32 self-check
//! (what `repocache.tryLoad` does before it derives;
//! the 40-byte header / key / O(1) `validateTail` are
//! negligible and excluded);
//! (b) re-derivation — `RepoView.buildFromRecordBody` (the replay that
//! rebuilds `RepoState`) — the ONLY term zero-parse
//! could cut;
//! (c) formatting — the `zit leaves` render (gather → §7.5 sort → write
//! lines). `leaves` is fully cache-served, so it
//! isolates I/O-vs-derivation cleanly; `log`/`timeline`
//! comment rendering would add log reads OUTSIDE this
//! split and is deliberately not modelled here.
//!
//! It reports each phase's median ns + % of the warm total over N reps.
//!
//! This is thrown-away measurement tooling, not library code: the phase split
//! replays `tryLoad`'s dominant steps directly (repocache is library-internal
//! and stays that way) so each phase can be timed in isolation. Determinism
//! seam: the monotonic clock and the temp file both go through the injected
//! `std.Io`; the library modules stay pure.
const std = @import("std");
const Io = std.Io;
const storage = @import("zit").storage;
const logstore = @import("zit").logstore;
const repoview = @import("zit").repoview;
const repostate = @import("zit").repostate;
/// Closed harness error set. `Storage`/`Corrupt`/`OutOfMemory` propagate from
/// the log open and the derivation; `Io` is a failed temp/store file syscall;
/// `BadArgs` is a rejected flag or oversized rep count.
pub const Error = error{ OutOfMemory, Storage, Corrupt, Io, BadArgs };
/// Adds the writer's own error set to the harness causes — both explicit, never
/// `anyerror`.
const RunError = Error || Io.Writer.Error;
/// Explicit fixed caps (TigerStyle bounds).
const max_reps: u64 = 10_000;
const min_reps: u64 = 1;
const Config = struct {
path: []const u8,
reps: u64 = 50,
warmup: u64 = 5,
};
pub fn run(gpa: std.mem.Allocator, io: Io, args: []const []const u8, out: *Io.Writer) RunError!void {
const cfg = try parseArgs(args);
try probe(gpa, io, cfg, out);
try out.flush();
}
/// The sole positional is the `.zit` path; every remaining token is a
/// `--flag value` integer pair. A leading `--` (no path) is rejected.
fn parseArgs(args: []const []const u8) Error!Config {
std.debug.assert(args.len >= 2);
if (args.len < 3 or std.mem.startsWith(u8, args[2], "--")) return error.BadArgs;
var cfg = Config{ .path = args[2] };
var i: usize = 3;
while (i < args.len) : (i += 1) {
const flag = args[i];
i += 1;
if (i >= args.len) return error.BadArgs; // flag with no value
const val = std.fmt.parseInt(u64, args[i], 0) catch return error.BadArgs;
if (std.mem.eql(u8, flag, "--reps")) {
cfg.reps = val;
} else if (std.mem.eql(u8, flag, "--warmup")) {
cfg.warmup = val;
} else return error.BadArgs;
}
if (cfg.reps < min_reps or cfg.reps > max_reps) return error.BadArgs;
if (cfg.warmup > max_reps) return error.BadArgs;
return cfg;
}
// ---------------------------------------------------------------------------
// The probe.
// ---------------------------------------------------------------------------
fn probe(gpa: std.mem.Allocator, io: Io, cfg: Config, out: *Io.Writer) RunError!void {
// Warm step: one full read/derive over the log to produce the exact cache
// body a `.zit-cache` stores, plus the resolved main-branch. The whole-log
// scan here is the COLD path — untimed; it only populates what the warm
// loop replays.
var file = Io.Dir.openFileAbsolute(io, cfg.path, .{}) catch return error.Io;
defer file.close(io);
const st = file.stat(io) catch return error.Io;
var fs = storage.FileStorage.init(io, file, st.size);
var log = logstore.LogStore.openReadOnly(gpa, fs.storageFile()) catch |e| return mapOpen(e);
defer log.deinit(gpa);
var body: std.ArrayList(u8) = .empty;
defer body.deinit(gpa);
var warm_view: repoview.RepoView = undefined;
warm_view.loadCapturing(gpa, &log, &body) catch |e| return mapLoad(e);
const main_branch = try gpa.dupe(u8, warm_view.main_branch);
defer gpa.free(main_branch);
const artifact_count = warm_view.store.store().count();
warm_view.deinit(gpa);
// Persist the record body to a temp file so phase (a) reads real bytes off
// disk (page-cached, i.e. a warm read) rather than a memory copy.
var tmp = try Temp.create(io);
defer tmp.cleanup(io);
tmp.file.writePositionalAll(io, body.items, 0) catch return error.Io;
tmp.file.sync(io) catch return error.Io;
const reps: usize = @intCast(cfg.reps);
const a_ns = try gpa.alloc(u64, reps);
defer gpa.free(a_ns);
const b_ns = try gpa.alloc(u64, reps);
defer gpa.free(b_ns);
const c_ns = try gpa.alloc(u64, reps);
defer gpa.free(c_ns);
const read_buf = try gpa.alloc(u8, body.items.len);
defer gpa.free(read_buf);
var sink: std.ArrayList(u8) = .empty;
defer sink.deinit(gpa);
var leaf_count: usize = 0;
// Warmup reps prime the page cache and the allocator's arenas; their timings
// are discarded so the reported medians reflect steady state.
var w: usize = 0;
while (w < cfg.warmup) : (w += 1) {
_ = try phaseIo(io, tmp.file, read_buf);
var view: repoview.RepoView = undefined;
view.buildFromRecordBody(gpa, read_buf, main_branch) catch |e| return mapBuild(e);
_ = try formatLeaves(gpa, &view, &sink);
view.deinit(gpa);
}
for (a_ns, b_ns, c_ns) |*ra, *rb, *rc| {
// (a) cache-body I/O + self-checksum.
const a0 = now(io);
_ = try phaseIo(io, tmp.file, read_buf);
const a1 = now(io);
// (b) replay re-derivation — the only term a zero-parse image removes.
var view: repoview.RepoView = undefined;
view.buildFromRecordBody(gpa, read_buf, main_branch) catch |e| return mapBuild(e);
const b1 = now(io);
// (c) `zit leaves` render over the derived state.
leaf_count = try formatLeaves(gpa, &view, &sink);
const c1 = now(io);
view.deinit(gpa); // teardown — untimed
ra.* = elapsed(a0, a1);
rb.* = elapsed(a1, b1);
rc.* = elapsed(b1, c1);
}
const med_a = median(gpa, a_ns) catch return error.OutOfMemory;
const med_b = median(gpa, b_ns) catch return error.OutOfMemory;
const med_c = median(gpa, c_ns) catch return error.OutOfMemory;
try report(out, cfg, artifact_count, leaf_count, body.items.len, med_a, med_b, med_c);
}
/// Read the persisted record body off disk and CRC32 it — the dominant work
/// `repocache.tryLoad` does before it derives (read-into-buffer + self-check).
/// Returns the checksum so the read cannot be optimized away.
fn phaseIo(io: Io, file: Io.File, buf: []u8) Error!u32 {
const n = file.readPositionalAll(io, buf, 0) catch return error.Io;
if (n != buf.len) return error.Io;
var crc = std.hash.Crc32.init();
crc.update(buf);
return crc.final();
}
/// Model `zit leaves` (main.zig `renderLeaves`): gather open-leaf rows, §7.5
/// sort (mtime desc, id asc), and write each ` <hash> <branch>` line into a
/// reused discard buffer. Returns the leaf-row count so the work is observable.
fn formatLeaves(gpa: std.mem.Allocator, view: *repoview.RepoView, sink: *std.ArrayList(u8)) Error!usize {
sink.clearRetainingCapacity();
const rs = &view.state;
var rows: std.ArrayList(LeafRow) = .empty;
defer rows.deinit(gpa);
const bound = rs.leaves().rids.len;
var scanned: usize = 0;
var it = rs.leaves();
while (it.next()) |rid| {
scanned += 1;
std.debug.assert(scanned <= bound);
if (!rs.isOpenLeaf(rid)) continue; // default listing: open leaves only
const mtime = rs.mtimeOf(rid) orelse unreachable; // a leaf always bears an event
try rows.append(gpa, .{ .mtime = mtime, .hash = view.hashOf(rid) });
}
std.mem.sort(LeafRow, rows.items, {}, leafRowLess);
var line: [160]u8 = undefined;
for (rows.items) |row| {
const branch = rs.branchOf(view.lookup(row.hash).?, view.main_branch);
const s = std.fmt.bufPrint(&line, " {s} {s}\n", .{ row.hash, branch }) catch return error.Corrupt;
try sink.appendSlice(gpa, s);
}
return rows.items.len;
}
/// A leaf row for the §7.5 render sort (the fields the ordering reads).
const LeafRow = struct { mtime: repostate.Mtime, hash: []const u8 };
/// §7.5 order: mtime descending, ties broken by hash ascending.
fn leafRowLess(_: void, a: LeafRow, b: LeafRow) bool {
if (a.mtime != b.mtime) return a.mtime > b.mtime;
return std.mem.order(u8, a.hash, b.hash) == .lt;
}
// ---------------------------------------------------------------------------
// Reporting.
// ---------------------------------------------------------------------------
fn report(
out: *Io.Writer,
cfg: Config,
artifacts: usize,
leaves: usize,
body_len: usize,
a: u64,
b: u64,
c: u64,
) Io.Writer.Error!void {
const total = a + b + c;
try out.print("warm-read probe: {s}\n", .{cfg.path});
try out.print(" artifacts={d} open-leaves={d} cache-body={d}B reps={d} (warmup {d})\n", .{
artifacts, leaves, body_len, cfg.reps, cfg.warmup,
});
try out.writeAll(" phase median % of warm total\n");
try out.print(" (a) cache-body I/O + crc32 {d:>9.2} us {d:>6.1}%\n", .{ nsToUs(a), pct(a, total) });
try out.print(" (b) replay re-derivation {d:>9.2} us {d:>6.1}%\n", .{ nsToUs(b), pct(b, total) });
try out.print(" (c) leaves formatting {d:>9.2} us {d:>6.1}%\n", .{ nsToUs(c), pct(c, total) });
try out.print(" warm total (a+b+c) {d:>9.2} us\n", .{nsToUs(total)});
try out.writeAll(" gate: re-derivation (b) >= 40% of warm total ⇒ GO (zero-parse SoA); else DEFER\n");
}
// ---------------------------------------------------------------------------
// Timing, median, temp file, error mapping.
// ---------------------------------------------------------------------------
fn now(io: Io) Io.Timestamp {
return Io.Timestamp.now(io, .awake);
}
fn elapsed(t0: Io.Timestamp, t1: Io.Timestamp) u64 {
const ns = t0.durationTo(t1).nanoseconds; // i96, monotonic so >= 0
return if (ns < 0) 0 else @intCast(ns);
}
/// Median of `samples` (a copy is sorted so the caller's timing array is left
/// in rep order). Even counts take the lower-middle element — a probe reports a
/// real observed sample, never a synthesized average.
fn median(gpa: std.mem.Allocator, samples: []const u64) error{OutOfMemory}!u64 {
std.debug.assert(samples.len >= 1);
const copy = try gpa.dupe(u64, samples);
defer gpa.free(copy);
std.mem.sort(u64, copy, {}, std.sort.asc(u64));
return copy[(copy.len - 1) / 2];
}
fn pct(part: u64, whole: u64) f64 {
if (whole == 0) return 0;
return @as(f64, @floatFromInt(part)) * 100.0 / @as(f64, @floatFromInt(whole));
}
fn nsToUs(ns: u64) f64 {
return @as(f64, @floatFromInt(ns)) / 1000.0;
}
/// A throwaway temp file for the record body; created readable so the phase-(a)
/// loop reads it back. Removed on cleanup.
const Temp = struct {
file: Io.File,
path_buf: [64]u8,
path_len: usize,
fn create(io: Io) Error!Temp {
var t: Temp = undefined;
const nonce = Io.Clock.Timestamp.now(io, .awake).raw.nanoseconds;
const p = std.fmt.bufPrint(&t.path_buf, "/tmp/zit-warmprobe-{d}.bin", .{nonce}) catch return error.BadArgs;
t.path_len = p.len;
t.file = Io.Dir.createFileAbsolute(io, t.path_buf[0..t.path_len], .{ .read = true }) catch return error.Io;
return t;
}
fn cleanup(self: *Temp, io: Io) void {
self.file.close(io);
Io.Dir.deleteFileAbsolute(io, self.path_buf[0..self.path_len]) catch {};
}
};
fn mapOpen(e: logstore.OpenError) Error {
return switch (e) {
error.OutOfMemory => error.OutOfMemory,
error.Storage => error.Storage,
error.Corrupt => error.Corrupt,
};
}
fn mapLoad(e: repoview.RepoView.LoadError) Error {
return switch (e) {
error.OutOfMemory => error.OutOfMemory,
error.Storage => error.Storage,
error.Corrupt => error.Corrupt,
};
}
fn mapBuild(e: repoview.RepoView.BuildError) Error {
return switch (e) {
error.OutOfMemory => error.OutOfMemory,
error.Corrupt => error.Corrupt,
};
}