引言
本章补齐 Zig 的日常运维工具:精确计时(std.time)、结构化日志(std.log)与终端友好的进度报告(std.Progress)。我们使流水线可观测、可度量且对用户友好。time.ziglog.zigProgress.zig
我们聚焦在 Zig 0.15.2 下可跨平台工作的确定性片段,突出注意事项、性能提示与最佳实践。
使用 std.time 进行计时
Zig 的 std.time 提供:
- 日历时间戳:timestamp()、milliTimestamp()、microTimestamp()、nanoTimestamp()。
- 持续时间/单位:诸如 ns_per_ms、ns_per_s、s_per_min 之类的常量用于转换。
- 高精度定时器:Instant(快速,不是严格单调)和 Timer(通过饱和实现单调行为)。
一般来说,对于测量经过的持续时间,优先使用 Timer。仅当您需要更快的采样并且可以容忍来自奇怪操作系统/固件环境的偶发非单调性时,才使用 Instant。
测量经过时间(Timer)
Timer 产生单调读数(在回归时饱和),非常适合基准测试和超时。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();
// 确保我们至少睡了 50ms
if (ns < 50 * std.time.ns_per_ms) return error.TimerResolutionTooLow;
// 打印稳定的消息
std.debug.print("Timer OK\n", .{});
}
$ zig run time_timer_sleep.zigTimer OK
睡眠使用 std.Thread.sleep(ns)。在大多数操作系统上,粒度约为 1ms 或更差;定时器的精度与底层时钟允许的精度一样。Thread.zig
Instant 采样与排序
Instant.now() 为当前进程提供快速、高精度的时间戳。它尝试在挂起期间推进,并且可以比较或求差。它不能保证在所有地方都严格单调。当您需要强制执行该属性时,请使用 Timer。
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
时间单位换算
优先使用提供的单位常量而非手写换算;它们能提升清晰度,并避免混合单位下的错误。
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
使用 std.log 记录日志
std.log 是一个小的、可组合的日志外观。您可以:
- 通过 std_options 控制日志级别(默认值取决于构建模式)。
- 使用作用域(命名空间)对消息进行分类。
- 提供自定义的 logFn 来更改格式化或重定向。
下面,我们设置 .log_level = .info 以抑制调试日志,并演示默认和作用域日志记录。
const std = @import("std");
// Configure logging for this program
// 为此程序配置日志记录
pub const std_options: std.Options = .{
.log_level = .info, // 隐藏调试信息
.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
注意:
- 默认日志记录器写入 stderr,因此我们在上面使用 2>&1 以在本书中内联显示。
- 在调试构建中,默认级别是 .debug。通过 std_options 覆盖,以使示例在各种优化模式下稳定。
使用 std.Progress 报告进度
std.Progress 向终端绘制一个小的任务树,并定期从另一个线程更新。它是非分配的,旨在跨终端和 Windows 控制台可移植。使用它来指示长时间运行的工作,例如构建、下载或分析过程。
下述演示在练习 API(根节点、子节点、completeOne、end)的同时,禁用打印以获得确定性输出。在真实工具中,去掉disable_printing即可呈现动态进度视图。
const std = @import("std");
pub fn main() void {
// 进度可以绘制到 stderr;在此演示中禁用打印以获得确定性输出。
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
提示:
- 使用 Options.estimated_total_items 显示计数("[3/10] compile");
- 使用 setName 更新名称;
- 通过 std.Progress.setStatus 发出成功/失败信号。
注意与警示
- Timer 与 Instant:对于经过时间和单调行为,优先使用
Timer;在偶发的非单调性可接受的情况下,使用Instant进行快速采样。 - 睡眠分辨率取决于操作系统。不要假设亚毫秒精度。
- 日志过滤器按作用域应用。使用
scoped(.your_component)干净地控制嘈杂的子系统。 std.Progress输出适应终端功能。在 CI/非 TTY 或禁用打印的情况下,不写入任何内容。- 时区支持:stdlib 在 0.15.2 中还没有稳定的
std.tz模块。如果您需要时区数学,请使用平台 API 或库。[TBD]
练习
- 使用
Timer编写微基准测试来比较两种格式化例程。打印更快的一种以及快了多少微秒。 - 使用来自
nanoTimestamp()的时间戳前缀的自定义logFn包装std.log。确保它保持非分配。 - 使用
std.Progress创建一个显示三个阶段的小型构建模拟器。使第二阶段动态增加estimated_total_items。
开放问题
- 标准库中的时区助手:未来
std.tz或等效模块的状态和路线图?[TBD]