//! Strided progress lines for a long phase: `<what> N/M <unit> (P%), R/s`.
//!
//! At most one line per `interval_ns`, and the clock is read once per
//! `stride` units rather than once per unit. Safe to advance from several
//! threads: the count is atomic and whichever thread wins the line prints
//! it, while the others go on -- so the writer is only ever used by one
//! thread at a time, provided its owner is not writing while the phase runs.
const std = @import("std");
pub const Meter = struct {
out: *std.Io.Writer,
io: std.Io,
/// The line's head, e.g. "import-git: verifying".
what: []const u8,
unit: []const u8,
total: u64,
interval_ns: u64 = default_interval_ns,
stride: u64 = default_stride,
done: std.atomic.Value(u64) = .init(0),
printing: std.atomic.Value(bool) = .init(false),
// Guarded by `printing`.
reported_at: u64 = 0,
reported_at_ns: i96 = 0,
pub const default_interval_ns: u64 = 2 * std.time.ns_per_s;
pub const default_stride: u64 = 64;
pub fn start(self: *Meter) void {
self.reported_at_ns = std.Io.Clock.now(.awake, self.io).nanoseconds;
}
pub fn add(self: *Meter, units: u64) void {
if (units == 0) return;
const before = self.done.fetchAdd(units, .monotonic);
const after = before + units;
if (before / self.stride == after / self.stride) return;
if (self.printing.swap(true, .acquire)) return;
defer self.printing.store(false, .release);
const now_ns = std.Io.Clock.now(.awake, self.io).nanoseconds;
const elapsed_ns: u64 = @intCast(@max(now_ns - self.reported_at_ns, 1));
if (elapsed_ns < self.interval_ns) return;
const done = self.done.load(.monotonic);
const rate = (done -| self.reported_at) * std.time.ns_per_s / elapsed_ns;
self.reported_at_ns = now_ns;
self.reported_at = done;
self.out.print("{s} {d}/{d} {s} ({d}%), {d}/s\n", .{
self.what,
done,
self.total,
self.unit,
done * 100 / @max(self.total, 1),
rate,
}) catch return;
// A line nobody sees for hours is the silence this exists to end.
self.out.flush() catch {};
}
};
test "a meter prints strided lines with count, percent and rate" {
var buffer: [512]u8 = undefined;
var out = std.Io.Writer.fixed(&buffer);
var meter: Meter = .{ .out = &out, .io = std.testing.io, .what = "x: doing", .unit = "frames", .total = 8, .interval_ns = 0, .stride = 4 };
meter.start();
for (0..8) |_| meter.add(1);
const text = out.buffered();
try std.testing.expect(std.mem.indexOf(u8, text, "x: doing 4/8 frames (50%), ") != null);
try std.testing.expect(std.mem.indexOf(u8, text, "x: doing 8/8 frames (100%), ") != null);
try std.testing.expectEqual(@as(usize, 2), std.mem.count(u8, text, "\n"));
}
test "a meter is silent inside its interval" {
var buffer: [512]u8 = undefined;
var out = std.Io.Writer.fixed(&buffer);
var meter: Meter = .{ .out = &out, .io = std.testing.io, .what = "x", .unit = "frames", .total = 1000, .stride = 1 };
meter.start();
for (0..1000) |_| meter.add(1);
try std.testing.expectEqualStrings("", out.buffered());
}