diff --git a/README.md b/README.md
index 21cdeca..2b49b72 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,7 @@
- Fast & Accurate Benchmarking for Zig
+ Fast & Accurate Microbenchmarking for Zig
@@ -18,6 +18,56 @@
+## Demo
+
+Lets benchmark fibonacci:
+
+```zig
+const std = @import("std");
+const bench = @import("bench");
+
+fn fibNaive(n: u64) u64 {
+ if (n <= 1) return n;
+ return fibNaive(n - 1) + fibNaive(n - 2);
+}
+
+fn fibIterative(n: u64) u64 {
+ if (n == 0) return 0;
+ var a: u64 = 0;
+ var b: u64 = 1;
+ for (2..n + 1) |_| {
+ const c = a + b;
+ a = b;
+ b = c;
+ }
+ return b;
+}
+
+pub fn main() !void {
+ const allocator = std.heap.smp_allocator;
+ const opts = bench.Options{
+ .sample_size = 100,
+ .warmup_iters = 3,
+ };
+ const m_naive = try bench.run(allocator, "fibNaive/30", fibNaive, .{30}, opts);
+ const m_iter = try bench.run(allocator, "fibIterative/30", fibIterative, .{30}, opts);
+
+ try bench.report(.{
+ .metrics = &.{ m_naive, m_iter },
+ .baseline_index = 0, // naive as baseline
+ });
+}
+```
+
+Run it and you will get the following output:
+
+| Benchmark | Time | Relative | Iterations | Ops/s | Cycles | Instructions | IPC | Cache Misses |
+| :---------------- | ------: | -------: | ---------: | -------: | -----: | -----------: | ---: | -----------: |
+| `fibNaive/30` | 1.77 ms | 1.00x | 1 | 564.7/s | 8.1M | 27.8M | 3.41 | 0.3 |
+| `fibIterative/30` | 3.44 ns | 0.00x | 300006 | 290.7M/s | 15.9 | 82.0 | 5.15 | 0.0 |
+
+Yes, the output is markdown table.
+
## Features
- **CPU Counters**: Measures CPU cycles, instructions, IPC, and cache misses
@@ -32,8 +82,6 @@
and data throughput (MB/s, GB/s) when payload size is provided.
- **Robust Statistics**: Uses median and standard deviation to provide reliable
metrics despite system noise.
-- **Zero Dependencies**: Implemented in pure Zig using only the standard
- library.
## Installation
@@ -156,19 +204,14 @@ const res = try bench.run(allocator, "Heavy Task", heavyFn, .{
### Built-in Reporter
-The default `bench.report` prints a human-readable table to stdout. It handles
-units (ns, us, ms, s) and coloring automatically.
+The default bench.report prints a clean, Markdown-compatible table to stdout. It
+automatically handles unit scaling (`ns`, `us`, `ms`, `s`) and formatting.
```sh
-$ zig build quicksort
-Benchmarking Sorting Algorithms Against Random Input (N=10000)...
-Benchmark Summary: 3 benchmarks run
-├─ Unsafe Quicksort (Lomuto) 358.64us 110.98MB/s 1.29x faster
-│ └─ cycles: 1.6M instructions: 1.2M ipc: 0.75 miss: 65
-├─ Unsafe Quicksort (Hoare) 383.02us 104.32MB/s 1.21x faster
-│ └─ cycles: 1.7M instructions: 1.3M ipc: 0.76 miss: 56
-└─ std.mem.sort 462.25us 86.45MB/s [baseline]
- └─ cycles: 2.0M instructions: 2.6M ipc: 1.30 miss: 143
+| Benchmark | Time | Relative | Iterations | Ops/s | Cycles | Instructions | IPC | Cache Misses |
+| :---------------- | ------: | -------: | ---------: | -------: | -----: | -----------: | ---: | -----------: |
+| `fibNaive/30` | 1.77 ms | 1.00x | 1 | 564.7/s | 8.1M | 27.8M | 3.41 | 0.3 |
+| `fibIterative/30` | 3.44 ns | 0.00x | 300006 | 290.7M/s | 15.9 | 82.0 | 5.15 | 0.0 |
```
### Custom Reporter
diff --git a/build.zig b/build.zig
index 87a0bc1..cbd39a0 100644
--- a/build.zig
+++ b/build.zig
@@ -23,25 +23,7 @@ pub fn build(b: *std.Build) void {
test_step.dependOn(&mod_test_run.step);
///////////////////////////////////////////////////////////////////////////
- // zig build quicksort
-
- const quicksort_step = b.step("quicksort", "Run quicksort benchmark");
- const quicksort_exe = b.addExecutable(.{
- .name = "quicksort-bench",
- .root_module = b.createModule(.{
- .root_source_file = b.path("examples/quicksort.zig"),
- .target = target,
- .optimize = .ReleaseFast,
- .imports = &.{
- .{
- .name = "bench",
- .module = mod,
- },
- },
- }),
- });
- const quicksort_run = b.addRunArtifact(quicksort_exe);
- quicksort_step.dependOn(&quicksort_run.step);
+ // zig build fibonacci
const fibonacci_step = b.step("fibonacci", "Run fibonacci benchmark");
const fibonacci_exe = b.addExecutable(.{
diff --git a/examples/fibonacci.zig b/examples/fibonacci.zig
index 9434580..9c21765 100644
--- a/examples/fibonacci.zig
+++ b/examples/fibonacci.zig
@@ -8,7 +8,6 @@ fn fibNaive(n: u64) u64 {
fn fibIterative(n: u64) u64 {
if (n == 0) return 0;
-
var a: u64 = 0;
var b: u64 = 1;
for (2..n + 1) |_| {
@@ -16,7 +15,6 @@ fn fibIterative(n: u64) u64 {
a = b;
b = c;
}
-
return b;
}
@@ -26,8 +24,8 @@ pub fn main() !void {
.sample_size = 100,
.warmup_iters = 3,
};
- const m_naive = try bench.run(allocator, "fibNaive", fibNaive, .{30}, opts);
- const m_iter = try bench.run(allocator, "fibIterative", fibIterative, .{30}, opts);
+ const m_naive = try bench.run(allocator, "fibNaive/30", fibNaive, .{30}, opts);
+ const m_iter = try bench.run(allocator, "fibIterative/30", fibIterative, .{30}, opts);
try bench.report(.{
.metrics = &.{ m_naive, m_iter },
diff --git a/examples/quicksort.zig b/examples/quicksort.zig
deleted file mode 100644
index 3911bd2..0000000
--- a/examples/quicksort.zig
+++ /dev/null
@@ -1,128 +0,0 @@
-const std = @import("std");
-const bench = @import("bench");
-
-fn swap(a: *i32, b: *i32) void {
- const temp = a.*;
- a.* = b.*;
- b.* = temp;
-}
-
-// Implementation 1: Lomuto Partition Scheme
-// Simpler code, but generally performs more swaps than Hoare.
-fn partitionLomuto(arr: []i32, low: usize, high: usize) usize {
- const pivot = arr[high];
- var i = low;
-
- var j = low;
- while (j < high) : (j += 1) {
- if (arr[j] < pivot) {
- swap(&arr[i], &arr[j]);
- i += 1;
- }
- }
- swap(&arr[i], &arr[high]);
- return i;
-}
-
-fn quickSortLomuto(arr: []i32, low: isize, high: isize) void {
- if (low < high) {
- const p_idx = partitionLomuto(arr, @intCast(low), @intCast(high));
- quickSortLomuto(arr, low, @as(isize, @intCast(p_idx)) - 1);
- quickSortLomuto(arr, @as(isize, @intCast(p_idx)) + 1, high);
- }
-}
-
-// Implementation 2: Hoare Partition Scheme
-// More efficient partition logic, often 3x fewer swaps than Lomuto.
-fn partitionHoare(arr: []i32, low: usize, high: usize) usize {
- const pivot = arr[low];
- var i: isize = @as(isize, @intCast(low)) - 1;
- var j: isize = @as(isize, @intCast(high)) + 1;
-
- while (true) {
- while (true) {
- i += 1;
- if (arr[@intCast(i)] >= pivot) break;
- }
- while (true) {
- j -= 1;
- if (arr[@intCast(j)] <= pivot) break;
- }
-
- if (i >= j) return @intCast(j);
- swap(&arr[@intCast(i)], &arr[@intCast(j)]);
- }
-}
-
-fn quickSortHoare(arr: []i32, low: isize, high: isize) void {
- if (low < high) {
- const p_idx = partitionHoare(arr, @intCast(low), @intCast(high));
- quickSortHoare(arr, low, @intCast(p_idx));
- quickSortHoare(arr, @as(isize, @intCast(p_idx)) + 1, high);
- }
-}
-
-fn stdSort(arr: []i32) void {
- std.mem.sort(i32, arr, {}, std.sort.asc(i32));
-}
-
-// Benchmark Wrappers
-
-fn runLomuto(allocator: std.mem.Allocator, input: []const i32) !void {
- const arr = try allocator.alloc(i32, input.len);
- defer allocator.free(arr);
- @memcpy(arr, input);
- quickSortLomuto(arr, 0, @as(isize, @intCast(arr.len)) - 1);
- std.mem.doNotOptimizeAway(arr);
-}
-
-fn runHoare(allocator: std.mem.Allocator, input: []const i32) !void {
- const arr = try allocator.alloc(i32, input.len);
- defer allocator.free(arr);
- @memcpy(arr, input);
- quickSortHoare(arr, 0, @as(isize, @intCast(arr.len)) - 1);
- std.mem.doNotOptimizeAway(arr);
-}
-
-fn runStdSort(allocator: std.mem.Allocator, input: []const i32) !void {
- const arr = try allocator.alloc(i32, input.len);
- defer allocator.free(arr);
- @memcpy(arr, input);
- stdSort(arr);
- std.mem.doNotOptimizeAway(arr);
-}
-
-pub fn main() !void {
- // Use general purpose allocator to catch leaks if any, though page_allocator is fine for bench
- var gpa = std.heap.GeneralPurposeAllocator(.{}){};
- defer _ = gpa.deinit();
- const allocator = gpa.allocator();
-
- // Prepare Data
- const size = 10_000;
- const input = try allocator.alloc(i32, size);
- defer allocator.free(input);
-
- var prng = std.Random.DefaultPrng.init(42);
- const rand = prng.random();
- for (input) |*x| x.* = rand.int(i32);
-
- std.debug.print("Benchmarking Sorting Algorithms Against Random Input (N={d})...\n", .{size});
-
- // Run Benchmarks
- const opts = bench.Options{
- .sample_size = 100,
- .warmup_iters = 20,
- .bytes_per_op = size * @sizeOf(i32),
- };
-
- const m_lomuto = try bench.run(allocator, "Unsafe Quicksort (Lomuto)", runLomuto, .{ allocator, input }, opts);
- const m_hoare = try bench.run(allocator, "Unsafe Quicksort (Hoare)", runHoare, .{ allocator, input }, opts);
- const m_std = try bench.run(allocator, "std.mem.sort", runStdSort, .{ allocator, input }, opts);
-
- // We use std.mem.sort as the baseline (index 2)
- try bench.report(.{
- .metrics = &.{ m_lomuto, m_hoare, m_std },
- .baseline_index = 2,
- });
-}
diff --git a/src/Metrics.zig b/src/Metrics.zig
index 8463445..74baf62 100644
--- a/src/Metrics.zig
+++ b/src/Metrics.zig
@@ -5,6 +5,8 @@
name: []const u8,
/// Total number of measurement samples collected
samples: usize,
+/// Number of executions per sample (batch size)
+iterations: u64,
///////////////////////////////////////////////////////////////////////////////
// Time
diff --git a/src/Reporter.test.zig b/src/Reporter.test.zig
index d40c49c..dd5ae6c 100644
--- a/src/Reporter.test.zig
+++ b/src/Reporter.test.zig
@@ -1,34 +1,120 @@
const std = @import("std");
const testing = std.testing;
+const Writer = std.Io.Writer;
-const Runner = @import("Runner.zig");
+const Metrics = @import("Metrics.zig");
const Reporter = @import("Reporter.zig");
-fn fibNaive(n: u64) u64 {
- if (n <= 1) return n;
- return fibNaive(n - 1) + fibNaive(n - 2);
+fn createMetrics(name: []const u8, ns: f64) Metrics {
+ return Metrics{
+ .name = name,
+ .samples = 100,
+ .iterations = 1000000,
+ .min_ns = ns,
+ .max_ns = ns,
+ .mean_ns = ns,
+ .median_ns = ns,
+ .std_dev_ns = 0,
+ .ops_sec = if (ns > 0) 1_000_000_000.0 / ns else 0,
+ .mb_sec = 0,
+ .cycles = null,
+ .instructions = null,
+ .ipc = null,
+ .cache_misses = null,
+ };
}
-fn fibIterative(n: u64) u64 {
- if (n == 0) return 0;
- var a: u64 = 0;
- var b: u64 = 1;
- for (2..n + 1) |_| {
- const c = a + b;
- a = b;
- b = c;
- }
- return b;
+test "Reporter: Time Unit Scaling (ns, us, ms, s)" {
+ const m_ns = createMetrics("Nano", 100.0);
+ const m_us = createMetrics("Micro", 15_000.0);
+ const m_ms = createMetrics("Milli", 250_000_000.0);
+ const m_s = createMetrics("Second", 5_000_000_000.0);
+
+ var buffer: [16 * 1024]u8 = undefined;
+ var w: Writer = .fixed(&buffer);
+
+ try Reporter.write(&w, .{ .metrics = &.{ m_ns, m_us, m_ms, m_s } });
+
+ const expected =
+ "| Benchmark | Time | Iterations | Ops/s | \n" ++
+ "| :-------- | --------: | ---------: | ------: | \n" ++
+ "| `Nano` | 100.00 ns | 1000000 | 10.0M/s | \n" ++
+ "| `Micro` | 15.00 us | 1000000 | 66.7k/s | \n" ++
+ "| `Milli` | 250.00 ms | 1000000 | 4.0/s | \n" ++
+ "| `Second` | 5.00 s | 1000000 | 0.2/s | \n";
+
+ try testing.expectEqualStrings(expected, w.buffered());
}
-test "report fib" {
- const allocator = testing.allocator;
- const opts = Runner.Options{
- .sample_size = 100,
- .warmup_iters = 3,
- };
- const m_naive = try Runner.run(allocator, "fibNaive", fibNaive, .{@as(u64, 20)}, opts);
- const m_iter = try Runner.run(allocator, "fibIterative", fibIterative, .{@as(u64, 20)}, opts);
+test "Reporter: Throughput Mixing (Bytes vs Ops)" {
+ // Case A: Only Ops/s (Default)
+ const m_ops = createMetrics("OpsOnly", 100.0);
+
+ // Case B: Bytes/s (High throughput)
+ // 1000ns = 1M Ops/s. We manually set MB/s.
+ var m_bytes = createMetrics("BytesOnly", 1000.0);
+ m_bytes.mb_sec = 500.0;
+
+ var buffer: [16 * 1024]u8 = undefined;
+ var w: Writer = .fixed(&buffer);
+
+ try Reporter.write(&w, .{ .metrics = &.{ m_ops, m_bytes } });
+
+ // Expectation:
+ // 1. Both "Bytes/s" and "Ops/s" columns appear because at least one metric triggered each.
+ // 2. OpsOnly has no Bytes/s -> "-"
+ // 3. BytesOnly has Bytes/s -> "500.00MB/s". It also has valid Ops/s (1M), so it prints that too.
+ const expected =
+ "| Benchmark | Time | Iterations | Bytes/s | Ops/s | \n" ++
+ "| :---------- | --------: | ---------: | ---------: | ------: | \n" ++
+ "| `OpsOnly` | 100.00 ns | 1000000 | - | 10.0M/s | \n" ++
+ "| `BytesOnly` | 1.00 us | 1000000 | 500.00MB/s | 1.0M/s | \n";
+
+ try testing.expectEqualStrings(expected, w.buffered());
+}
+
+test "Reporter: Hardware Counters (Sparse Data)" {
+ const m_base = createMetrics("Baseline", 100.0);
+
+ var m_full = createMetrics("WithHW", 100.0);
+ m_full.cycles = 2500.0;
+ m_full.instructions = 5000.0;
+ m_full.ipc = 2.0;
+ m_full.cache_misses = 15.0;
+
+ var buffer: [16 * 1024]u8 = undefined;
+ var w: Writer = .fixed(&buffer);
+
+ try Reporter.write(&w, .{ .metrics = &.{ m_base, m_full } });
+
+ // Expectation:
+ // HW columns should appear. Baseline fills them with "-". WithHW fills them with values.
+ const expected =
+ "| Benchmark | Time | Iterations | Ops/s | Cycles | Instructions | IPC | Cache Misses | \n" ++
+ "| :--------- | --------: | ---------: | ------: | -----: | -----------: | ---: | -----------: | \n" ++
+ "| `Baseline` | 100.00 ns | 1000000 | 10.0M/s | - | - | - | - | \n" ++
+ "| `WithHW` | 100.00 ns | 1000000 | 10.0M/s | 2.5k | 5.0k | 2.00 | 15.0 | \n";
+
+ try testing.expectEqualStrings(expected, w.buffered());
+}
+
+test "Reporter: Baseline Comparison" {
+ const m_base = createMetrics("Base", 100.0); // Baseline (100ns)
+ const m_fast = createMetrics("Fast", 50.0); // 2x faster (0.50x duration)
+ const m_slow = createMetrics("Slow", 200.0); // 2x slower (2.00x duration)
+
+ var buffer: [16 * 1024]u8 = undefined;
+ var w: Writer = .fixed(&buffer);
+
+ // Set baseline_index to 0 ("Base")
+ try Reporter.write(&w, .{ .metrics = &.{ m_base, m_fast, m_slow }, .baseline_index = 0 });
+
+ const expected =
+ "| Benchmark | Time | Relative | Iterations | Ops/s | \n" ++
+ "| :-------- | --------: | -------: | ---------: | ------: | \n" ++
+ "| `Base` | 100.00 ns | 1.00x | 1000000 | 10.0M/s | \n" ++
+ "| `Fast` | 50.00 ns | 0.50x | 1000000 | 20.0M/s | \n" ++
+ "| `Slow` | 200.00 ns | 2.00x | 1000000 | 5.0M/s | \n";
- try Reporter.report(.{ .metrics = &.{ m_naive, m_iter }, .baseline_index = 0 });
+ try testing.expectEqualStrings(expected, w.buffered());
}
diff --git a/src/Reporter.zig b/src/Reporter.zig
index 118cdf6..3f27055 100644
--- a/src/Reporter.zig
+++ b/src/Reporter.zig
@@ -1,201 +1,245 @@
const std = @import("std");
const Writer = std.Io.Writer;
-const tty = std.Io.tty;
-
const Metrics = @import("Metrics.zig");
pub const Options = struct {
metrics: []const Metrics,
- /// The index in 'metrics' to use as the baseline for comparison (e.g 1.00x).
- /// If null, no comparison column is shown.
baseline_index: ?usize = null,
};
-/// Prints a formatted summary table to stdout.
-pub fn report(options: Options) !void {
- var buffer: [0x2000]u8 = undefined;
+const Column = struct {
+ title: []const u8,
+ width: usize,
+ align_right: bool,
+ active: bool,
+};
+
+pub fn print(options: Options) !void {
+ var buffer: [64 * 1024]u8 = undefined;
var w: Writer = .fixed(&buffer);
- try writeReport(&w, options);
+ try write(&w, options);
std.debug.print("{s}", .{w.buffered()});
}
-/// Writes the formatted report to a specific writer
-pub fn writeReport(writer: *Writer, options: Options) !void {
+pub fn write(w: *Writer, options: Options) !void {
if (options.metrics.len == 0) return;
- try writer.print("Benchmark Summary: {d} benchmarks run\n", .{options.metrics.len});
+ // Initialize columns with Header names and default visibility
+ var col_name = Column{ .title = "Benchmark", .width = 0, .align_right = false, .active = true };
+ var col_time = Column{ .title = "Time", .width = 0, .align_right = true, .active = true };
+ var col_relative = Column{ .title = "Relative", .width = 0, .align_right = true, .active = false };
+ var col_iter = Column{ .title = "Iterations", .width = 0, .align_right = true, .active = true };
- var max_name_len: usize = 0;
- for (options.metrics) |m| max_name_len = @max(max_name_len, m.name.len);
+ var col_bytes = Column{ .title = "Bytes/s", .width = 0, .align_right = true, .active = false };
+ var col_ops = Column{ .title = "Ops/s", .width = 0, .align_right = true, .active = false };
+ var col_cycles = Column{ .title = "Cycles", .width = 0, .align_right = true, .active = false };
+ var col_instr = Column{ .title = "Instructions", .width = 0, .align_right = true, .active = false };
+ var col_ipc = Column{ .title = "IPC", .width = 0, .align_right = true, .active = false };
+ var col_miss = Column{ .title = "Cache Misses", .width = 0, .align_right = true, .active = false };
- for (options.metrics, 0..) |m, i| {
- const is_last_item = i == options.metrics.len - 1;
+ // We must format every number to a temporary buffer to know its length.
+ var buf: [64]u8 = undefined;
- // --- ROW 1: High Level (Name | Time | Speed | Comparison) ---
- const tree_char = if (is_last_item) "└─ " else "├─ ";
- try writeColor(writer, .bright_black, tree_char);
- try writeColor(writer, .cyan, m.name);
- // try writer.print("{s}{s}", .{ tree_char, m.name });
+ // Activate Rel column if baseline_index is valid
+ if (options.baseline_index) |idx| {
+ if (idx < options.metrics.len) {
+ col_relative.active = true;
+ }
+ }
- // Align name
- const padding = max_name_len - m.name.len + 2;
- _ = try writer.splatByte(' ', padding);
+ // Check headers first
+ col_name.width = col_name.title.len;
+ col_time.width = col_time.title.len;
+ col_relative.width = col_relative.title.len;
+ // col_cpu.width = col_cpu.title.len;
+ col_iter.width = col_iter.title.len;
+ col_bytes.width = col_bytes.title.len;
+ col_ops.width = col_ops.title.len;
+ col_cycles.width = col_cycles.title.len;
+ col_instr.width = col_instr.title.len;
+ col_ipc.width = col_ipc.title.len;
+ col_miss.width = col_miss.title.len;
+
+ for (options.metrics) |m| {
+ // Name: +2 for backticks
+ col_name.width = @max(col_name.width, m.name.len + 2);
+
+ // Time
+ const s_time = try fmtTime(&buf, m.mean_ns);
+ col_time.width = @max(col_time.width, s_time.len);
+
+ // Relative
+ if (col_relative.active) {
+ const base = options.metrics[options.baseline_index.?];
+ // Avoid division by zero
+ const ratio = if (base.mean_ns > 0) m.mean_ns / base.mean_ns else 0;
+ const s_rel = try std.fmt.bufPrint(&buf, "{d:.2}x", .{ratio});
+ col_relative.width = @max(col_relative.width, s_rel.len);
+ }
- try fmtTime(writer, m.median_ns);
- try writer.writeAll(" ");
+ // Iterations
+ const s_iter = try std.fmt.bufPrint(&buf, "{d}", .{m.samples});
+ col_iter.width = @max(col_iter.width, s_iter.len);
+ // Optional Columns (Enable & Measure)
if (m.mb_sec > 0.001) {
- try fmtBandwidth(writer, m.mb_sec);
- } else {
- try fmtOps(writer, m.ops_sec);
+ col_bytes.active = true;
+ const s = try fmtBytes(&buf, m.mb_sec);
+ col_bytes.width = @max(col_bytes.width, s.len);
}
-
- // Comparison (On the first line now)
- if (options.baseline_index) |base_idx| {
- try writer.writeAll(" ");
- if (i == base_idx) {
- try writeColor(writer, .blue, "[baseline]");
- } else if (base_idx < options.metrics.len) {
- const base = options.metrics[base_idx];
- const base_f = base.median_ns;
- const curr_f = m.median_ns;
-
- if (curr_f > 0 and base_f > 0) {
- if (curr_f < base_f) {
- try writer.writeAll("\x1b[32m"); // Green manually to mix with print
- try writer.print("{d:.2}x faster", .{base_f / curr_f});
- try writer.writeAll("\x1b[0m");
- } else {
- try writer.writeAll("\x1b[31m");
- try writer.print("{d:.2}x slower", .{curr_f / base_f});
- try writer.writeAll("\x1b[0m");
- }
- } else {
- try writer.writeAll("-");
- }
- }
+ if (m.ops_sec > 0.001 and m.mb_sec <= 0.001) {
+ col_ops.active = true;
+ const s_val = try fmtMetric(&buf, m.ops_sec);
+ // We append "/s" in the final output, so add 2 to length
+ col_ops.width = @max(col_ops.width, s_val.len + 2);
}
- try writer.writeByte('\n');
-
- // Only printed if we have hardware stats
- if (m.cycles) |cycles| {
- const sub_tree_prefix = if (is_last_item) " └─ " else "│ └─ ";
- try writer.writeAll(sub_tree_prefix);
- try writeColor(writer, .dim, "cycles: ");
- try fmtInt(writer, cycles);
+ if (m.cycles) |v| {
+ col_cycles.active = true;
+ const s = try fmtMetric(&buf, v);
+ col_cycles.width = @max(col_cycles.width, s.len);
}
-
- if (m.instructions) |instructions| {
- try writer.writeAll("\t");
- try writeColor(writer, .dim, "instructions: ");
- try fmtInt(writer, instructions);
+ if (m.instructions) |v| {
+ col_instr.active = true;
+ const s = try fmtMetric(&buf, v);
+ col_instr.width = @max(col_instr.width, s.len);
}
-
- if (m.ipc) |ipc| {
- try writer.writeAll("\t");
- try writeColor(writer, .dim, "ipc: ");
- try writer.print("{d:.2}", .{ipc});
+ if (m.ipc) |v| {
+ col_ipc.active = true;
+ const s = try std.fmt.bufPrint(&buf, "{d:.2}", .{v});
+ col_ipc.width = @max(col_ipc.width, s.len);
}
-
- if (m.cache_misses) |cache_missess| {
- try writer.writeAll("\t");
- try writeColor(writer, .dim, "miss: ");
- try fmtInt(writer, cache_missess);
-
- try writer.writeByte('\n');
+ if (m.cache_misses) |v| {
+ col_miss.active = true;
+ const s = try fmtMetric(&buf, v);
+ col_miss.width = @max(col_miss.width, s.len);
}
}
-}
-fn writeColor(writer: *Writer, color: tty.Color, text: []const u8) !void {
- const config = tty.Config.detect(std.fs.File.stdout());
- if (config != .no_color) {
- switch (color) {
- .reset => try writer.writeAll("\x1b[0m"),
- .red => try writer.writeAll("\x1b[31m"),
- .green => try writer.writeAll("\x1b[32m"),
- .blue => try writer.writeAll("\x1b[34m"),
- .cyan => try writer.writeAll("\x1b[36m"),
- .dim => try writer.writeAll("\x1b[2m"),
- .black => try writer.writeAll("\x1b[90m"),
- else => try writer.writeAll(""),
+ // Header Row
+ try w.writeAll("| ");
+ try printCell(w, col_name.title, col_name);
+ try printCell(w, col_time.title, col_time);
+ if (col_relative.active) try printCell(w, col_relative.title, col_relative);
+ try printCell(w, col_iter.title, col_iter);
+ if (col_bytes.active) try printCell(w, col_bytes.title, col_bytes);
+ if (col_ops.active) try printCell(w, col_ops.title, col_ops);
+ if (col_cycles.active) try printCell(w, col_cycles.title, col_cycles);
+ if (col_instr.active) try printCell(w, col_instr.title, col_instr);
+ if (col_ipc.active) try printCell(w, col_ipc.title, col_ipc);
+ if (col_miss.active) try printCell(w, col_miss.title, col_miss);
+ try w.writeAll("\n");
+
+ // Separator Row
+ try w.writeAll("| ");
+ try printDivider(w, col_name);
+ try printDivider(w, col_time);
+ if (col_relative.active) try printDivider(w, col_relative);
+ try printDivider(w, col_iter);
+ if (col_bytes.active) try printDivider(w, col_bytes);
+ if (col_ops.active) try printDivider(w, col_ops);
+ if (col_cycles.active) try printDivider(w, col_cycles);
+ if (col_instr.active) try printDivider(w, col_instr);
+ if (col_ipc.active) try printDivider(w, col_ipc);
+ if (col_miss.active) try printDivider(w, col_miss);
+ try w.writeAll("\n");
+
+ // Data Rows
+ for (options.metrics) |m| {
+ try w.writeAll("| ");
+
+ // Name
+ const name_s = try std.fmt.bufPrint(&buf, "`{s}`", .{m.name});
+ try printCell(w, name_s, col_name);
+
+ // Time
+ try printCell(w, try fmtTime(&buf, m.mean_ns), col_time);
+
+ // Relative
+ if (col_relative.active) {
+ const base = options.metrics[options.baseline_index.?];
+ const ratio = if (base.mean_ns > 0) m.mean_ns / base.mean_ns else 0;
+ const s_rel = try std.fmt.bufPrint(&buf, "{d:.2}x", .{ratio});
+ try printCell(w, s_rel, col_relative);
}
- }
- try writer.writeAll(text);
- if (config != .no_color) try writer.writeAll("\x1b[0m");
-}
-////////////////////////////////////////////////////////////////////////////////
-// formatters
+ // Iterations
+ const iter_s = try std.fmt.bufPrint(&buf, "{d}", .{m.iterations});
+ try printCell(w, iter_s, col_iter);
-fn fmtInt(writer: *Writer, val: f64) !void {
- if (val < 1000) {
- try writer.print("{d:.0}", .{val});
- } else if (val < 1_000_000) {
- try writer.print("{d:.1}k", .{val / 1000.0});
- } else if (val < 1_000_000_000) {
- try writer.print("{d:.1}M", .{val / 1_000_000.0});
- } else {
- try writer.print("{d:.1}G", .{val / 1_000_000_000.0});
+ // Optional
+ if (col_bytes.active) {
+ if (m.mb_sec > 0.001) try printCell(w, try fmtBytes(&buf, m.mb_sec), col_bytes) else try printCell(w, "-", col_bytes);
+ }
+ if (col_ops.active) {
+ if (m.ops_sec > 0.001) {
+ // Must manually construct the string with suffix to match width measurement
+ const val = try fmtMetric(&buf, m.ops_sec);
+ var buf2: [64]u8 = undefined;
+ const final = try std.fmt.bufPrint(&buf2, "{s}/s", .{val});
+ try printCell(w, final, col_ops);
+ } else try printCell(w, "-", col_ops);
+ }
+ if (col_cycles.active) {
+ if (m.cycles) |v| try printCell(w, try fmtMetric(&buf, v), col_cycles) else try printCell(w, "-", col_cycles);
+ }
+ if (col_instr.active) {
+ if (m.instructions) |v| try printCell(w, try fmtMetric(&buf, v), col_instr) else try printCell(w, "-", col_instr);
+ }
+ if (col_ipc.active) {
+ if (m.ipc) |v| {
+ const s = try std.fmt.bufPrint(&buf, "{d:.2}", .{v});
+ try printCell(w, s, col_ipc);
+ } else try printCell(w, "-", col_ipc);
+ }
+ if (col_miss.active) {
+ if (m.cache_misses) |v| try printCell(w, try fmtMetric(&buf, v), col_miss) else try printCell(w, "-", col_miss);
+ }
+
+ try w.writeAll("\n");
}
}
-fn fmtTime(writer: *Writer, ns: f64) !void {
- var buf: [64]u8 = undefined;
- var slice: []u8 = undefined;
-
- if (ns < 1000) {
- slice = try std.fmt.bufPrint(&buf, "{d:.2}ns", .{ns});
- } else if (ns < 1_000_000) {
- slice = try std.fmt.bufPrint(&buf, "{d:.2}us", .{ns / 1000.0});
- } else if (ns < 1_000_000_000) {
- slice = try std.fmt.bufPrint(&buf, "{d:.2}ms", .{ns / 1_000_000.0});
+fn printCell(w: *Writer, text: []const u8, col: Column) !void {
+ const pad_len = if (col.width > text.len) col.width - text.len else 0;
+
+ if (col.align_right) {
+ _ = try w.splatByte(' ', pad_len);
+ try w.writeAll(text);
} else {
- slice = try std.fmt.bufPrint(&buf, "{d:.2}s", .{ns / 1_000_000_000.0});
+ try w.writeAll(text);
+ _ = try w.splatByte(' ', pad_len);
}
- try padLeft(writer, slice, 9);
+ try w.writeAll(" | ");
}
-fn fmtOps(writer: *Writer, ops: f64) !void {
- var buf: [64]u8 = undefined;
- var slice: []u8 = undefined;
-
- if (ops < 1000) {
- slice = try std.fmt.bufPrint(&buf, "{d:.0}/s", .{ops});
- } else if (ops < 1_000_000) {
- slice = try std.fmt.bufPrint(&buf, "{d:.2}K/s", .{ops / 1000.0});
- } else if (ops < 1_000_000_000) {
- slice = try std.fmt.bufPrint(&buf, "{d:.2}M/s", .{ops / 1_000_000.0});
+fn printDivider(w: *Writer, col: Column) !void {
+ if (col.align_right) {
+ // "-----------:"
+ _ = try w.splatByte('-', col.width - 1);
+ try w.writeAll(":");
} else {
- slice = try std.fmt.bufPrint(&buf, "{d:.2}G/s", .{ops / 1_000_000_000.0});
+ // ":-----------"
+ try w.writeAll(":");
+ _ = try w.splatByte('-', col.width - 1);
}
- try padLeft(writer, slice, 11);
+ try w.writeAll(" | ");
}
-fn fmtBandwidth(writer: *Writer, mb: f64) !void {
- var buf: [64]u8 = undefined;
- var slice: []u8 = undefined;
-
- if (mb >= 1000) {
- slice = try std.fmt.bufPrint(&buf, "{d:.2}GB/s", .{mb / 1000.0});
- } else {
- slice = try std.fmt.bufPrint(&buf, "{d:.2}MB/s", .{mb});
- }
- try padLeft(writer, slice, 11);
+fn fmtTime(buf: []u8, ns: f64) ![]const u8 {
+ if (ns < 1_000) return std.fmt.bufPrint(buf, "{d:.2} ns", .{ns});
+ if (ns < 1_000_000) return std.fmt.bufPrint(buf, "{d:.2} us", .{ns / 1_000.0});
+ if (ns < 1_000_000_000) return std.fmt.bufPrint(buf, "{d:.2} ms", .{ns / 1_000_000.0});
+ return std.fmt.bufPrint(buf, "{d:.2} s", .{ns / 1_000_000_000.0});
}
-// Pads with spaces on the left (for numbers)
-fn padLeft(writer: *Writer, text: []const u8, width: usize) !void {
- if (text.len < width) {
- _ = try writer.splatByte(' ', width - text.len);
- }
- try writer.writeAll(text);
+fn fmtBytes(buf: []u8, mb: f64) ![]const u8 {
+ if (mb > 1000) return std.fmt.bufPrint(buf, "{d:.2}GB/s", .{mb / 1024.0});
+ return std.fmt.bufPrint(buf, "{d:.2}MB/s", .{mb});
}
-// Pads with spaces on the right (for text/comparisons)
-fn padRight(writer: *Writer, text: []const u8, width: usize) !void {
- try writer.writeAll(text);
- if (text.len < width) {
- _ = try writer.splatByte(' ', width - text.len);
- }
+fn fmtMetric(buf: []u8, val: f64) ![]const u8 {
+ if (val < 1_000) return std.fmt.bufPrint(buf, "{d:.1}", .{val});
+ if (val < 1_000_000) return std.fmt.bufPrint(buf, "{d:.1}k", .{val / 1_000.0});
+ if (val < 1_000_000_000) return std.fmt.bufPrint(buf, "{d:.1}M", .{val / 1_000_000.0});
+ return std.fmt.bufPrint(buf, "{d:.1}G", .{val / 1_000_000_000.0});
}
diff --git a/src/Runner.zig b/src/Runner.zig
index f770756..63d6e8e 100644
--- a/src/Runner.zig
+++ b/src/Runner.zig
@@ -102,6 +102,7 @@ pub fn run(allocator: Allocator, name: []const u8, function: anytype, args: anyt
.median_ns = samples[options.sample_size / 2],
.std_dev_ns = math.sqrt(variance),
.samples = options.sample_size,
+ .iterations = batch_size,
.ops_sec = ops_sec,
.mb_sec = mb_sec,
};
diff --git a/src/root.zig b/src/root.zig
index 04e530e..72c0bc1 100644
--- a/src/root.zig
+++ b/src/root.zig
@@ -7,7 +7,7 @@ pub const Reporter = @import("Reporter.zig");
pub const Options = Runner.Options;
pub const run = Runner.run;
-pub const report = Reporter.report;
+pub const report = Reporter.print;
test {
if (builtin.os.tag == .linux) {