Introduction
This chapter rounds out everyday operational tools in Zig: precise time measurement (std.time), structured logging (std.log), and terminal-friendly progress reporting (std.Progress). Here we make pipelines observable, measurable, and user-friendly. time.ziglog.zigProgress.zig
We’ll focus on deterministic snippets that work across platforms under Zig 0.15.2, highlighting gotchas, performance notes, and best practices.
Timekeeping with std.time
Zig’s std.time provides:
- Calendar timestamps: timestamp(), milliTimestamp(), microTimestamp(), nanoTimestamp().
- Duration/units: constants like ns_per_ms, ns_per_s, s_per_min for conversions.
- High-precision timers: Instant (fast, not strictly monotonic) and Timer (monotonic behavior by saturation).
In general, prefer Timer for measuring elapsed durations. Reach for Instant only when you need faster sampling and can tolerate occasional non-monotonicity from quirky OS/firmware environments.
Measuring elapsed time (Timer)
Timer yields monotonic readings (saturating on regressions) and is ideal for benchmarking and timeouts. 39
const std = @import("std");
pub fn main() !void {
var t = try std.time.Timer.start();
std.Thread.sleep(50 * std.time.ns_per_ms);
const ns = t.read();
// Ensure we slept at least 50ms
if (ns < 50 * std.time.ns_per_ms) return error.TimerResolutionTooLow;
// Print a stable message
std.debug.print("Timer OK\n", .{});
}
$ zig run time_timer_sleep.zigTimer OK
Sleeping uses std.Thread.sleep(ns). On most OSes the granularity is ~1ms or worse; timers are as precise as the underlying clocks permit. Thread.zig
Instant sampling and ordering
Instant.now() gives a fast, high-precision timestamp for the current process. It tries to advance during suspend and can be compared or differenced. It is not guaranteed strictly monotonic everywhere. Use Timer when you need that property enforced.
const std = @import("std");
pub fn main() !void {
const a = try std.time.Instant.now();
std.Thread.sleep(1 * std.time.ns_per_ms);
const b = try std.time.Instant.now();
if (b.order(a) == .lt) return error.InstantNotMonotonic;
std.debug.print("Instant OK\n", .{});
}
$ zig run time_instant_order.zigInstant OK
Time unit conversions
Prefer the provided unit constants over hand-rolled math. They improve clarity and prevent mistakes in mixed units.
const std = @import("std");
pub fn main() !void {
const two_min_s = 2 * std.time.s_per_min;
const hour_ns = std.time.ns_per_hour;
std.debug.print("2 min = {d} s\n1 h = {d} ns\n", .{ two_min_s, hour_ns });
}
$ zig run time_units.zig2 min = 120 s 1 h = 3600000000000 ns
Logging with std.log
std.log is a small, composable logging façade. You can:
- Control log level via std_options (defaults are build-mode dependent).
- Use scopes (namespaces) to categorize messages.
- Provide a custom logFn to change formatting or redirects.
Below, we set .log_level = .info so debug logs are suppressed, and demonstrate both default and scoped logging.
const std = @import("std");
// Configure logging for this program
pub const std_options: std.Options = .{
.log_level = .info, // hide debug
.logFn = std.log.defaultLog,
};
pub fn main() void {
std.log.debug("debug hidden", .{});
std.log.info("starting", .{});
std.log.warn("high temperature", .{});
const app = std.log.scoped(.app);
app.info("running", .{});
}
$ zig run logging_basic.zig 2>&1 | catinfo: starting warning: high temperature info(app): running
NOTE:
- The default logger writes to stderr, so we use 2>&1 above to show it inline in this book.
- In Debug builds the default level is .debug. Override via std_options to make examples stable across optimize modes.
Progress reporting with std.Progress
std.Progress draws a small tree of tasks to the terminal, updating periodically from another thread. It is non-allocating and aims to be portable across terminals and Windows consoles. Use it to indicate long-running work such as builds, downloads, or analysis passes.
The following demo disables printing for deterministic output while exercising the API (root node, children, completeOne, end). In real tools, omit disable_printing to render a dynamic progress view.
const std = @import("std");
pub fn main() void {
// Progress can draw to stderr; disable printing in this demo for deterministic output.
const root = std.Progress.start(.{ .root_name = "build", .estimated_total_items = 3, .disable_printing = true });
var compile = root.start("compile", 2);
compile.completeOne();
compile.completeOne();
compile.end();
var link = root.start("link", 1);
link.completeOne();
link.end();
root.end();
}
$ zig run progress_basic.zigno output
TIP:
- Use Options.estimated_total_items to show counts (“[3/10] compile”);
- Update names with setName;
- Signal success/failure via std.Progress.setStatus.
Notes and caveats
- Timer vs. Instant: prefer
Timerfor elapsed time and monotonic behavior; useInstantfor fast samples when occasional non-monotonicity is acceptable. - Sleep resolution is OS-dependent. Don’t assume sub-millisecond precision.
- Logging filters apply per scope. Use
scoped(.your_component)to gate noisy subsystems cleanly. std.Progressoutput adapts to terminal capabilities. On CI/non-TTY or disabled printing, nothing is written.- Timezone support: stdlib does not yet have a stable
std.tzmodule in 0.15.2. Use platform APIs or a library if you need timezone math. [TBD]
Exercises
- Write a micro-benchmark using
Timerto compare two formatting routines. Print the faster one and by how many microseconds. - Wrap
std.logwith a customlogFnthat prefixes timestamps fromnanoTimestamp(). Ensure it remains non-allocating. - Create a small build simulator with
std.Progressshowing three phases. Make the second phase dynamically increaseestimated_total_items.
Open Questions
- Timezone helpers in std: status and roadmap for a future
std.tzor equivalent? [TBD]