//! 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;
const repoimage = @import("zit").repoimage;
/// 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();
// Materialize the zero-parse image from the same derived state (§ Phase A):
// the memo a warm `.fast_read` reloads instead of re-deriving.
var image: std.ArrayList(u8) = .empty;
defer image.deinit(gpa);
repoimage.serialize(gpa, &warm_view.state, main_branch, &image) catch |e| return mapBuild2(e);
warm_view.deinit(gpa);
// Persist the record body and the image to temp files so the read phases
// read real bytes off disk (page-cached, i.e. a warm read).
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;
var itmp = try Temp.create(io);
defer itmp.cleanup(io);
itmp.file.writePositionalAll(io, image.items, 0) catch return error.Io;
itmp.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);
// Image path: (a') image I/O + crc, (b') zero-parse image load, (c') render.
const ai_ns = try gpa.alloc(u64, reps);
defer gpa.free(ai_ns);
const bi_ns = try gpa.alloc(u64, reps);
defer gpa.free(bi_ns);
const ci_ns = try gpa.alloc(u64, reps);
defer gpa.free(ci_ns);
// Read both cache files ONCE into resident buffers, untimed. A warm read is
// page-cache-resident by definition (repo-state §1 memo), so the timed I/O
// phase is the CRC integrity pass `repocache.tryLoad` runs over the resident
// bytes — not a disk seek, whose latency is environment noise that swamps
// the derivation term Phase A actually changes.
const read_buf = try gpa.alloc(u8, body.items.len);
defer gpa.free(read_buf);
if ((tmp.file.readPositionalAll(io, read_buf, 0) catch return error.Io) != read_buf.len) return error.Io;
const image_buf = try gpa.alloc(u8, image.items.len);
defer gpa.free(image_buf);
if ((itmp.file.readPositionalAll(io, image_buf, 0) catch return error.Io) != image_buf.len) return error.Io;
var sink: std.ArrayList(u8) = .empty;
defer sink.deinit(gpa);
var leaf_count: usize = 0;
// Each path is measured in its OWN loop with its own warmup, so its cache
// file stays page-cache hot and the two paths never perturb each other's
// I/O phase (interleaving them thrashes the page cache and inflates I/O).
// --- Derive path (today's warm read) ---
var w: usize = 0;
while (w < cfg.warmup) : (w += 1) {
_ = phaseCrc(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| {
const a0 = now(io);
_ = phaseCrc(read_buf); // (a) cache-body integrity crc32 over the resident bytes
const a1 = now(io);
var view: repoview.RepoView = undefined;
view.buildFromRecordBody(gpa, read_buf, main_branch) catch |e| return mapBuild(e); // (b) re-derivation
const b1 = now(io);
leaf_count = try formatLeaves(gpa, &view, &sink); // (c) render
const c1 = now(io);
view.deinit(gpa); // untimed teardown
ra.* = elapsed(a0, a1);
rb.* = elapsed(a1, b1);
rc.* = elapsed(b1, c1);
}
// --- Image path (Phase A .fast_read) ---
w = 0;
while (w < cfg.warmup) : (w += 1) {
_ = phaseCrc(image_buf);
var iview: repoview.RepoView = undefined;
const owned = try gpa.dupe(u8, image_buf);
iview.buildFromImage(gpa, owned, owned, main_branch) catch |e| return mapBuild(e);
_ = try formatLeaves(gpa, &iview, &sink);
iview.deinit(gpa);
}
for (ai_ns, bi_ns, ci_ns) |*rai, *rbi, *rci| {
const ai0 = now(io);
_ = phaseCrc(image_buf); // (a') image integrity crc32 over the resident bytes
const ai1 = now(io);
var iview: repoview.RepoView = undefined;
const owned = try gpa.dupe(u8, image_buf); // the alloc tryLoad also pays
iview.buildFromImage(gpa, owned, owned, main_branch) catch |e| return mapBuild(e); // (b') zero-parse load
const bi1 = now(io);
_ = try formatLeaves(gpa, &iview, &sink); // (c') render
const ci1 = now(io);
iview.deinit(gpa); // untimed teardown
rai.* = elapsed(ai0, ai1);
rbi.* = elapsed(ai1, bi1);
rci.* = elapsed(bi1, ci1);
}
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;
const med_ai = median(gpa, ai_ns) catch return error.OutOfMemory;
const med_bi = median(gpa, bi_ns) catch return error.OutOfMemory;
const med_ci = median(gpa, ci_ns) catch return error.OutOfMemory;
try report(out, cfg, artifact_count, leaf_count, body.items.len, image.items.len, .{
.a = med_a,
.b = med_b,
.c = med_c,
.ai = med_ai,
.bi = med_bi,
.ci = med_ci,
});
}
/// The CRC32 integrity pass `repocache.tryLoad` runs over the resident cache
/// bytes before it trusts them — the per-read cost on a warm (page-cache-hot)
/// read. Returns the checksum so the pass cannot be optimized away.
fn phaseCrc(buf: []const u8) u32 {
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.
// ---------------------------------------------------------------------------
const Medians = struct { a: u64, b: u64, c: u64, ai: u64, bi: u64, ci: u64 };
fn report(
out: *Io.Writer,
cfg: Config,
artifacts: usize,
leaves: usize,
body_len: usize,
image_len: usize,
m: Medians,
) Io.Writer.Error!void {
const derive_total = m.a + m.b + m.c;
const image_total = m.ai + m.bi + m.ci;
try out.print("warm-read probe: {s}\n", .{cfg.path});
try out.print(" artifacts={d} open-leaves={d} cache-body={d}B image={d}B reps={d} (warmup {d})\n", .{
artifacts, leaves, body_len, image_len, cfg.reps, cfg.warmup,
});
try out.writeAll(" DERIVE path (today's warm read) median % of warm total\n");
try out.print(" (a) integrity crc32 (resident){d:>9.2} us {d:>6.1}%\n", .{ nsToUs(m.a), pct(m.a, derive_total) });
try out.print(" (b) replay re-derivation {d:>9.2} us {d:>6.1}%\n", .{ nsToUs(m.b), pct(m.b, derive_total) });
try out.print(" (c) leaves formatting {d:>9.2} us {d:>6.1}%\n", .{ nsToUs(m.c), pct(m.c, derive_total) });
try out.print(" DERIVE warm total (a+b+c) {d:>9.2} us\n", .{nsToUs(derive_total)});
try out.writeAll(" IMAGE path (Phase A .fast_read) median % of warm total\n");
try out.print(" (a') integrity crc32 (resident){d:>9.2} us {d:>6.1}%\n", .{ nsToUs(m.ai), pct(m.ai, image_total) });
try out.print(" (b') zero-parse image load {d:>9.2} us {d:>6.1}%\n", .{ nsToUs(m.bi), pct(m.bi, image_total) });
try out.print(" (c') leaves formatting {d:>9.2} us {d:>6.1}%\n", .{ nsToUs(m.ci), pct(m.ci, image_total) });
try out.print(" IMAGE warm total (a'+b'+c') {d:>9.2} us\n", .{nsToUs(image_total)});
try out.print(" SPEEDUP (derive / image) {d:>9.2}x\n", .{speedup(derive_total, image_total)});
// Phase B removes remaining hashmap-fill from the load. Phase A's image
// load (b') has NO hashmap fill — it is offset-view setup + leaf-rid widen.
// So the gate is whether the load still does derivation-scale work: b' as a
// fraction of the derive term (b) it replaced. Warm-total fraction is the
// wrong denominator once the total collapses.
try out.print(" derivation term eliminated: {d:.1}% (b {d:.2}us -> b' {d:.2}us)\n", .{
pct(m.b - m.bi, m.b), nsToUs(m.b), nsToUs(m.bi),
});
try out.print(" image load (b') as % of the derive it replaced: {d:.2}% ⇒ {s}\n", .{
pct(m.bi, m.b),
if (pct(m.bi, m.b) >= 40.0) "Phase B candidate" else "Phase A sufficient (no re-derivation residual)",
});
}
fn speedup(derive_total: u64, image_total: u64) f64 {
if (image_total == 0) return 0;
return @as(f64, @floatFromInt(derive_total)) / @as(f64, @floatFromInt(image_total));
}
// ---------------------------------------------------------------------------
// 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,
};
}
fn mapBuild2(e: error{OutOfMemory}) Error {
return switch (e) {
error.OutOfMemory => error.OutOfMemory,
};
}