Skip to content
zit Artifact [c93d5b27bd]
Login

Artifact [c93d5b27bd]

Artifact c93d5b27bdbb35e2982261ac8e4f4972e74afc6b05cfcec1608c9fbd593f5108:


//! 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());
}