From ea07edcd59e1aa22521898c794d8f65a7e6b7e1b Mon Sep 17 00:00:00 2001 From: pyk <2213646+pyk@users.noreply.github.com> Date: Sat, 13 Dec 2025 02:15:51 +0700 Subject: [PATCH 1/9] feat(Metrics): add iterations --- src/Metrics.zig | 2 ++ src/Runner.zig | 1 + 2 files changed, 3 insertions(+) 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/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, }; From a780bfb243e32fe2b87fdb2892af03518fa93b72 Mon Sep 17 00:00:00 2001 From: pyk <2213646+pyk@users.noreply.github.com> Date: Sat, 13 Dec 2025 02:16:55 +0700 Subject: [PATCH 2/9] feat: add MarkdownReporter --- src/reporters/MarkdownReporter.test.zig | 112 ++++++++++++ src/reporters/MarkdownReporter.zig | 217 ++++++++++++++++++++++++ src/root.zig | 1 + 3 files changed, 330 insertions(+) create mode 100644 src/reporters/MarkdownReporter.test.zig create mode 100644 src/reporters/MarkdownReporter.zig diff --git a/src/reporters/MarkdownReporter.test.zig b/src/reporters/MarkdownReporter.test.zig new file mode 100644 index 0000000..7cafb23 --- /dev/null +++ b/src/reporters/MarkdownReporter.test.zig @@ -0,0 +1,112 @@ +const std = @import("std"); +const testing = std.testing; +const Writer = std.Io.Writer; + +const Metrics = @import("../Metrics.zig"); +const MarkdownReporter = @import("MarkdownReporter.zig"); + +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, + }; +} + +test "MarkdownReporter: 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 MarkdownReporter.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 "MarkdownReporter: 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 MarkdownReporter.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 "MarkdownReporter: 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 MarkdownReporter.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 "MarkdownReporter: Basic table / 1 Metric" { + const metrics = createMetrics("myFun", 100.0); + + var buffer: [16 * 1024]u8 = undefined; + var w: Writer = .fixed(&buffer); + try MarkdownReporter.write(&w, .{ .metrics = &.{metrics} }); + const expected = + "| Benchmark | Time | Iterations | Ops/s | \n" ++ + "| :-------- | --------: | ---------: | ------: | \n" ++ + "| `myFun` | 100.00 ns | 1000000 | 10.0M/s | \n"; + try testing.expectEqualStrings(expected, w.buffered()); +} diff --git a/src/reporters/MarkdownReporter.zig b/src/reporters/MarkdownReporter.zig new file mode 100644 index 0000000..2703d8c --- /dev/null +++ b/src/reporters/MarkdownReporter.zig @@ -0,0 +1,217 @@ +const std = @import("std"); +const Writer = std.Io.Writer; +const Metrics = @import("../Metrics.zig"); + +pub const Options = struct { + metrics: []const Metrics, + baseline_index: ?usize = null, +}; + +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 fbs = std.io.fixedBufferStream(&buffer); + try write(fbs.writer(), options); + std.debug.print("{s}", .{fbs.getWritten()}); +} + +pub fn write(w: *Writer, options: Options) !void { + if (options.metrics.len == 0) return; + + // 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_iter = Column{ .title = "Iterations", .width = 0, .align_right = true, .active = true }; + + 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 }; + + // We must format every number to a temporary buffer to know its length. + var buf: [64]u8 = undefined; + + // Check headers first + col_name.width = col_name.title.len; + col_time.width = col_time.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); + + // 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) { + col_bytes.active = true; + const s = try fmtBytes(&buf, m.mb_sec); + col_bytes.width = @max(col_bytes.width, s.len); + } + 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); + } + 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) |v| { + col_instr.active = true; + const s = try fmtMetric(&buf, v); + col_instr.width = @max(col_instr.width, s.len); + } + 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) |v| { + col_miss.active = true; + const s = try fmtMetric(&buf, v); + col_miss.width = @max(col_miss.width, s.len); + } + } + + // Header Row + try w.writeAll("| "); + try printCell(w, col_name.title, col_name); + try printCell(w, col_time.title, col_time); + 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); + 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/CPU + try printCell(w, try fmtTime(&buf, m.mean_ns), col_time); + + // Iterations + const iter_s = try std.fmt.bufPrint(&buf, "{d}", .{m.iterations}); + try printCell(w, iter_s, col_iter); + + // 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 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 { + try w.writeAll(text); + _ = try w.splatByte(' ', pad_len); + } + try w.writeAll(" | "); +} + +fn printDivider(w: *Writer, col: Column) !void { + if (col.align_right) { + // "-----------:" + _ = try w.splatByte('-', col.width - 1); + try w.writeAll(":"); + } else { + // ":-----------" + try w.writeAll(":"); + _ = try w.splatByte('-', col.width - 1); + } + try w.writeAll(" | "); +} + +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}); +} + +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}); +} + +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/root.zig b/src/root.zig index 04e530e..f870e97 100644 --- a/src/root.zig +++ b/src/root.zig @@ -15,4 +15,5 @@ test { } _ = @import("Runner.test.zig"); _ = @import("Reporter.test.zig"); + _ = @import("reporters/MarkdownReporter.test.zig"); } From 60b202f1cfef3bc0b5f0bed230ae211a74456ecb Mon Sep 17 00:00:00 2001 From: pyk <2213646+pyk@users.noreply.github.com> Date: Sat, 13 Dec 2025 02:38:13 +0700 Subject: [PATCH 3/9] chore: replace default reporter with markdown --- src/Reporter.test.zig | 132 +++++++-- src/Reporter.zig | 348 +++++++++++++----------- src/reporters/MarkdownReporter.test.zig | 112 -------- src/reporters/MarkdownReporter.zig | 217 --------------- src/root.zig | 3 +- 5 files changed, 306 insertions(+), 506 deletions(-) delete mode 100644 src/reporters/MarkdownReporter.test.zig delete mode 100644 src/reporters/MarkdownReporter.zig diff --git a/src/Reporter.test.zig b/src/Reporter.test.zig index d40c49c..75bc680 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 "MarkdownReporter: 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 "MarkdownReporter: 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 "MarkdownReporter: 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 "MarkdownReporter: 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/reporters/MarkdownReporter.test.zig b/src/reporters/MarkdownReporter.test.zig deleted file mode 100644 index 7cafb23..0000000 --- a/src/reporters/MarkdownReporter.test.zig +++ /dev/null @@ -1,112 +0,0 @@ -const std = @import("std"); -const testing = std.testing; -const Writer = std.Io.Writer; - -const Metrics = @import("../Metrics.zig"); -const MarkdownReporter = @import("MarkdownReporter.zig"); - -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, - }; -} - -test "MarkdownReporter: 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 MarkdownReporter.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 "MarkdownReporter: 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 MarkdownReporter.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 "MarkdownReporter: 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 MarkdownReporter.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 "MarkdownReporter: Basic table / 1 Metric" { - const metrics = createMetrics("myFun", 100.0); - - var buffer: [16 * 1024]u8 = undefined; - var w: Writer = .fixed(&buffer); - try MarkdownReporter.write(&w, .{ .metrics = &.{metrics} }); - const expected = - "| Benchmark | Time | Iterations | Ops/s | \n" ++ - "| :-------- | --------: | ---------: | ------: | \n" ++ - "| `myFun` | 100.00 ns | 1000000 | 10.0M/s | \n"; - try testing.expectEqualStrings(expected, w.buffered()); -} diff --git a/src/reporters/MarkdownReporter.zig b/src/reporters/MarkdownReporter.zig deleted file mode 100644 index 2703d8c..0000000 --- a/src/reporters/MarkdownReporter.zig +++ /dev/null @@ -1,217 +0,0 @@ -const std = @import("std"); -const Writer = std.Io.Writer; -const Metrics = @import("../Metrics.zig"); - -pub const Options = struct { - metrics: []const Metrics, - baseline_index: ?usize = null, -}; - -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 fbs = std.io.fixedBufferStream(&buffer); - try write(fbs.writer(), options); - std.debug.print("{s}", .{fbs.getWritten()}); -} - -pub fn write(w: *Writer, options: Options) !void { - if (options.metrics.len == 0) return; - - // 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_iter = Column{ .title = "Iterations", .width = 0, .align_right = true, .active = true }; - - 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 }; - - // We must format every number to a temporary buffer to know its length. - var buf: [64]u8 = undefined; - - // Check headers first - col_name.width = col_name.title.len; - col_time.width = col_time.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); - - // 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) { - col_bytes.active = true; - const s = try fmtBytes(&buf, m.mb_sec); - col_bytes.width = @max(col_bytes.width, s.len); - } - 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); - } - 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) |v| { - col_instr.active = true; - const s = try fmtMetric(&buf, v); - col_instr.width = @max(col_instr.width, s.len); - } - 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) |v| { - col_miss.active = true; - const s = try fmtMetric(&buf, v); - col_miss.width = @max(col_miss.width, s.len); - } - } - - // Header Row - try w.writeAll("| "); - try printCell(w, col_name.title, col_name); - try printCell(w, col_time.title, col_time); - 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); - 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/CPU - try printCell(w, try fmtTime(&buf, m.mean_ns), col_time); - - // Iterations - const iter_s = try std.fmt.bufPrint(&buf, "{d}", .{m.iterations}); - try printCell(w, iter_s, col_iter); - - // 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 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 { - try w.writeAll(text); - _ = try w.splatByte(' ', pad_len); - } - try w.writeAll(" | "); -} - -fn printDivider(w: *Writer, col: Column) !void { - if (col.align_right) { - // "-----------:" - _ = try w.splatByte('-', col.width - 1); - try w.writeAll(":"); - } else { - // ":-----------" - try w.writeAll(":"); - _ = try w.splatByte('-', col.width - 1); - } - try w.writeAll(" | "); -} - -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}); -} - -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}); -} - -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/root.zig b/src/root.zig index f870e97..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) { @@ -15,5 +15,4 @@ test { } _ = @import("Runner.test.zig"); _ = @import("Reporter.test.zig"); - _ = @import("reporters/MarkdownReporter.test.zig"); } From 6b43a7e2f8b844987cf3f35520c9e6e3bdb2114f Mon Sep 17 00:00:00 2001 From: pyk <2213646+pyk@users.noreply.github.com> Date: Sat, 13 Dec 2025 02:38:26 +0700 Subject: [PATCH 4/9] chore(examples): update fibonacci examples --- examples/fibonacci.zig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/fibonacci.zig b/examples/fibonacci.zig index 9434580..a3c28f9 100644 --- a/examples/fibonacci.zig +++ b/examples/fibonacci.zig @@ -26,8 +26,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 }, From e3ea910d12ee5055ad4a49d3ef16308257a0eb77 Mon Sep 17 00:00:00 2001 From: pyk <2213646+pyk@users.noreply.github.com> Date: Sat, 13 Dec 2025 02:39:04 +0700 Subject: [PATCH 5/9] chore(examples): remove quicksort --- examples/quicksort.zig | 128 ----------------------------------------- 1 file changed, 128 deletions(-) delete mode 100644 examples/quicksort.zig 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, - }); -} From 36d59053446cb746f06308267ec84ddec47a7383 Mon Sep 17 00:00:00 2001 From: pyk <2213646+pyk@users.noreply.github.com> Date: Sat, 13 Dec 2025 02:39:14 +0700 Subject: [PATCH 6/9] chore(build): remove quicksort --- build.zig | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) 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(.{ From 39cc8e3e0d791682b7c407bbba0f97b24b52852f Mon Sep 17 00:00:00 2001 From: pyk <2213646+pyk@users.noreply.github.com> Date: Sat, 13 Dec 2025 02:42:04 +0700 Subject: [PATCH 7/9] chore(examples): remove extra whitespace --- examples/fibonacci.zig | 2 -- 1 file changed, 2 deletions(-) diff --git a/examples/fibonacci.zig b/examples/fibonacci.zig index a3c28f9..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; } From 4e21fca4c64b92fca9bc4a0601696eb718d28fbe Mon Sep 17 00:00:00 2001 From: pyk <2213646+pyk@users.noreply.github.com> Date: Sat, 13 Dec 2025 02:43:49 +0700 Subject: [PATCH 8/9] chore(Reporter): rename test prefix --- src/Reporter.test.zig | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Reporter.test.zig b/src/Reporter.test.zig index 75bc680..dd5ae6c 100644 --- a/src/Reporter.test.zig +++ b/src/Reporter.test.zig @@ -24,7 +24,7 @@ fn createMetrics(name: []const u8, ns: f64) Metrics { }; } -test "MarkdownReporter: Time Unit Scaling (ns, us, ms, s)" { +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); @@ -46,7 +46,7 @@ test "MarkdownReporter: Time Unit Scaling (ns, us, ms, s)" { try testing.expectEqualStrings(expected, w.buffered()); } -test "MarkdownReporter: Throughput Mixing (Bytes vs Ops)" { +test "Reporter: Throughput Mixing (Bytes vs Ops)" { // Case A: Only Ops/s (Default) const m_ops = createMetrics("OpsOnly", 100.0); @@ -73,7 +73,7 @@ test "MarkdownReporter: Throughput Mixing (Bytes vs Ops)" { try testing.expectEqualStrings(expected, w.buffered()); } -test "MarkdownReporter: Hardware Counters (Sparse Data)" { +test "Reporter: Hardware Counters (Sparse Data)" { const m_base = createMetrics("Baseline", 100.0); var m_full = createMetrics("WithHW", 100.0); @@ -98,7 +98,7 @@ test "MarkdownReporter: Hardware Counters (Sparse Data)" { try testing.expectEqualStrings(expected, w.buffered()); } -test "MarkdownReporter: Baseline Comparison" { +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) From dd8fdf4fa0ff20e1a4a1542873749263b1db5ead Mon Sep 17 00:00:00 2001 From: pyk <2213646+pyk@users.noreply.github.com> Date: Sat, 13 Dec 2025 02:46:49 +0700 Subject: [PATCH 9/9] chore(docs): add demo --- README.md | 71 ++++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 57 insertions(+), 14 deletions(-) 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 @@