Skip to content

Commit

Permalink
refactor: move Statistics into module & various minor changes (#70)
Browse files Browse the repository at this point in the history
- Move Statistics into its own module and parameterise it by type, so it can hold u64s for nanoseconds and usizes for bytes.
- Compute statistics on demand rather than pre-computing them in Result.
- Add Reading and Readings types, so it can carry more than just nanoseconds without changing most code.
- Add JSON examples
  • Loading branch information
bens authored Mar 20, 2024
1 parent 3ca44c2 commit ac1b084
Show file tree
Hide file tree
Showing 14 changed files with 377 additions and 329 deletions.
6 changes: 2 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,10 +105,8 @@ You can then run your benchmarks in a test:
test "bench test" {
var bench = zbench.Benchmark.init(std.testing.allocator, .{});
defer bench.deinit();
try bench.add("My Benchmark", myBenchmark, .{ .iterations = 10 });
const results = try bench.run();
defer results.deinit();
try results.prettyPrint(stdout, true);
try bench.add("My Benchmark", myBenchmark, .{});
try bench.run(std.io.getStdOut().writer());
}
```

Expand Down
2 changes: 2 additions & 0 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ fn setupTesting(b: *std.Build, target: std.zig.CrossTarget, optimize: std.builti
.{ .name = "optional", .path = "util/optional.zig" },
.{ .name = "platform", .path = "util/platform.zig" },
.{ .name = "runner", .path = "util/runner.zig" },
.{ .name = "statistics", .path = "util/statistics.zig" },
.{ .name = "zbench", .path = "zbench.zig" },
};

Expand All @@ -61,6 +62,7 @@ fn setupExamples(b: *std.Build, target: std.zig.CrossTarget, optimize: std.built
"basic",
"bubble_sort",
"hooks",
"json",
"parameterised",
"progress",
"sleep",
Expand Down
2 changes: 2 additions & 0 deletions examples/basic.zig
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,7 @@ test "bench test basic" {
defer bench.deinit();

try bench.add("My Benchmark", myBenchmark, .{});

try stdout.writeAll("\n");
try bench.run(stdout);
}
1 change: 1 addition & 0 deletions examples/bubble_sort.zig
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,6 @@ test "bench test bubbleSort" {

try bench.add("Bubble Sort Benchmark", myBenchmark, .{});

try stdout.writeAll("\n");
try bench.run(stdout);
}
2 changes: 2 additions & 0 deletions examples/hooks.zig
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,7 @@ test "bench test hooks" {
.after_all = afterAllHook,
},
});

try stdout.writeAll("\n");
try bench.run(stdout);
}
35 changes: 35 additions & 0 deletions examples/json.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
const std = @import("std");
const zbench = @import("zbench");
const test_allocator = std.testing.allocator;

fn myBenchmark(alloc: std.mem.Allocator) void {
var result: usize = 0;
for (0..2_000) |i| {
std.mem.doNotOptimizeAway(i);
result += i * i;
const buf = alloc.alloc(u8, 1024) catch unreachable;
defer alloc.free(buf);
}
}

test "bench test json" {
const stdout = std.io.getStdOut().writer();
var bench = zbench.Benchmark.init(test_allocator, .{});
defer bench.deinit();

try bench.add("My Benchmark 1", myBenchmark, .{ .iterations = 10 });

try stdout.writeAll("[");
var iter = try bench.iterator();
var i: usize = 0;
while (try iter.next()) |step| switch (step) {
.progress => |_| {},
.result => |x| {
defer x.deinit();
defer i += 1;
if (0 < i) try stdout.writeAll(", ");
try x.writeJSON(stdout);
},
};
try stdout.writeAll("]\n");
}
2 changes: 1 addition & 1 deletion examples/progress.zig
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ test "bench test progress" {
defer progress_node.end();

try stdout.writeAll("\n");
try bench.prettyPrintHeader(stdout);
try zbench.prettyPrintHeader(stdout);
var iter = try bench.iterator();
while (try iter.next()) |step| switch (step) {
.progress => |p| {
Expand Down
19 changes: 2 additions & 17 deletions examples/systeminfo.zig
Original file line number Diff line number Diff line change
@@ -1,22 +1,7 @@
const std = @import("std");
const zbench = @import("zbench");

fn myBenchmark(_: std.mem.Allocator) void {
var result: usize = 0;
for (0..1_000_000) |i| {
std.mem.doNotOptimizeAway(i);
result += i * i;
}
}

test "bench test system info" {
test "system info" {
const stdout = std.io.getStdOut().writer();
var bench = zbench.Benchmark.init(std.testing.allocator, .{});
defer bench.deinit();

const sysinfo = try bench.getSystemInfo();
try std.fmt.format(stdout, "\n{}\n", .{sysinfo});

try bench.add("My Benchmark", myBenchmark, .{});
try bench.run(stdout);
try stdout.print("\n\n{}\n", .{try zbench.getSystemInfo()});
}
119 changes: 27 additions & 92 deletions util/format.zig
Original file line number Diff line number Diff line change
@@ -1,98 +1,33 @@
const std = @import("std");
const log = std.log.scoped(.zbench_format);

pub fn memorySize(bytes: u64, allocator: std.mem.Allocator) ![]const u8 {
const units = .{ "B", "KB", "MB", "GB", "TB" };
var size: f64 = @floatFromInt(bytes);
var unit_index: usize = 0;

while (size >= 1024 and unit_index < units.len - 1) : (unit_index += 1) {
size /= 1024;
}

const unit = switch (unit_index) {
0 => "B",
1 => "KB",
2 => "MB",
3 => "GB",
4 => "TB",
5 => "PB",
6 => "EB",
else => unreachable,
fn FormatJSONArrayData(comptime T: type) type {
return struct {
values: []const T,

const Self = @This();

fn format(
data: Self,
comptime fmt: []const u8,
options: std.fmt.FormatOptions,
writer: anytype,
) !void {
_ = fmt;
_ = options;
try writer.writeAll("[");
for (data.values, 0..) |x, i| {
if (0 < i) try writer.writeAll(", ");
try writer.print("{}", .{x});
}
try writer.writeAll("]");
}
};

// Format the result with two decimal places if needed
var buf: [64]u8 = undefined; // Buffer for formatting
const formattedSize = try std.fmt.bufPrint(&buf, "{d:.2} {s}", .{ size, unit });
return allocator.dupe(u8, formattedSize);
}

/// Pretty-prints the header for the result pretty-print table
/// writer: Type that has the associated method print (for example std.io.getStdOut.writer())
pub fn prettyPrintHeader(writer: anytype) !void {
try writer.print(
"{s:<22} {s:<8} {s:<14} {s:<22} {s:<28} {s:<10} {s:<10} {s:<10}\n",
.{
"benchmark",
"runs",
"total time",
"time/run (avg ± σ)",
"(min ... max)",
"p75",
"p99",
"p995",
},
);
const dashes = "-------------------------";
try writer.print(dashes ++ dashes ++ dashes ++ dashes ++ dashes ++ "\n", .{});
}

/// Pretty-prints the name of the benchmark
/// writer: Type that has the associated method print (for example std.io.getStdOut.writer())
pub fn prettyPrintName(name: []const u8, writer: anytype) !void {
try writer.print("{s:<22} ", .{name});
}

/// Pretty-prints the number of total operations (or runs) of the benchmark performed
/// writer: Type that has the associated method print (for example std.io.getStdOut.writer())
pub fn prettyPrintTotalOperations(total_operations: u64, writer: anytype) !void {
try writer.print("{d:<8} ", .{total_operations});
}

/// Pretty-prints the total time it took to perform all the runs
/// writer: Type that has the associated method print (for example std.io.getStdOut.writer())
pub fn prettyPrintTotalTime(total_time: u64, writer: anytype) !void {
try writer.print("{s:<14} ", .{std.fmt.fmtDuration(total_time)});
}

/// Pretty-prints the average (arithmetic mean) and the standard deviation of the durations
/// writer: Type that has the associated method print (for example std.io.getStdOut.writer())
pub fn prettyPrintAvgStd(avg: u64, stddev: u64, writer: anytype) !void {
var buffer: [128]u8 = undefined;
const str = try std.fmt.bufPrint(&buffer, "{} ± {}", .{
std.fmt.fmtDuration(avg),
std.fmt.fmtDuration(stddev),
});
try writer.print("{s:<22} ", .{str});
}

/// Pretty-prints the minumim and maximum duration
/// writer: Type that has the associated method print (for example std.io.getStdOut.writer())
pub fn prettyPrintMinMax(min: u64, max: u64, writer: anytype) !void {
var buffer: [128]u8 = undefined;
const str = try std.fmt.bufPrint(&buffer, "({} ... {})", .{
std.fmt.fmtDuration(min),
std.fmt.fmtDuration(max),
});
try writer.print("{s:<28} ", .{str});
}

/// Pretty-prints the 75th, 99th and 99.5th percentile of the durations
/// writer: Type that has the associated method print (for example std.io.getStdOut.writer())
pub fn prettyPrintPercentiles(p75: u64, p99: u64, p995: u64, writer: anytype) !void {
try writer.print("{s:<10} {s:<10} {s:<10}", .{
std.fmt.fmtDuration(p75),
std.fmt.fmtDuration(p99),
std.fmt.fmtDuration(p995),
});
pub fn fmtJSONArray(
comptime T: type,
values: []const T,
) std.fmt.Formatter(FormatJSONArrayData(T).format) {
const data = FormatJSONArrayData(T){ .values = values };
return .{ .data = data };
}
2 changes: 0 additions & 2 deletions util/platform.zig
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
const std = @import("std");
const builtin = @import("builtin");

const format = @import("format.zig");

const lnx = @import("os/linux.zig");
const mac = @import("os/osx.zig");
const win = @import("os/windows.zig");
Expand Down
66 changes: 37 additions & 29 deletions util/runner.zig
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ const expectEqSlices = std.testing.expectEqualSlices;

pub const Error = @import("./runner/types.zig").Error;
pub const Step = @import("./runner/types.zig").Step;
pub const Reading = @import("./runner/types.zig").Reading;
pub const Readings = @import("./runner/types.zig").Readings;

const Runner = @This();

const State = union(enum) {
Expand Down Expand Up @@ -55,7 +58,7 @@ pub fn init(
iterations: u16,
max_iterations: u16,
time_budget_ns: u64,
) !Runner {
) Error!Runner {
return if (iterations == 0) .{
.allocator = allocator,
.state = .{ .preparing = .{
Expand All @@ -72,12 +75,12 @@ pub fn init(
};
}

pub fn next(self: *Runner, ns: u64) Runner.Error!?Runner.Step {
pub fn next(self: *Runner, reading: Reading) Error!?Step {
const MAX_N = 65536;
switch (self.state) {
.preparing => |*st| {
if (st.elapsed_ns < st.time_budget_ns and st.iteration_loops < st.max_iterations) {
st.elapsed_ns += ns;
st.elapsed_ns += reading.timing_ns;
if (st.iterations_remaining == 0) {
// double N for next iteration
st.N = @min(st.N * 2, MAX_N);
Expand Down Expand Up @@ -105,7 +108,8 @@ pub fn next(self: *Runner, ns: u64) Runner.Error!?Runner.Step {
},
.running => |*st| {
if (0 < st.iterations_remaining) {
st.timings_ns[st.timings_ns.len - st.iterations_remaining] = ns;
const i = st.timings_ns.len - st.iterations_remaining;
st.timings_ns[i] = reading.timing_ns;
st.iterations_remaining -= 1;
}
return if (st.iterations_remaining == 0) null else .more;
Expand All @@ -115,10 +119,14 @@ pub fn next(self: *Runner, ns: u64) Runner.Error!?Runner.Step {

/// The next() function has returned null and there are no more steps to
/// complete, so get the timing results.
pub fn finish(self: *Runner) Runner.Error![]u64 {
pub fn finish(self: *Runner) Error!Readings {
return switch (self.state) {
.preparing => &.{},
.running => |st| st.timings_ns,
.preparing => .{
.timings_ns = &.{},
},
.running => |st| .{
.timings_ns = st.timings_ns,
},
};
}

Expand Down Expand Up @@ -152,30 +160,30 @@ test "Runner" {
var r = try Runner.init(std.testing.allocator, 0, 16384, 2e9);
{
errdefer r.abort();
try expectEq(Runner.Step.more, try r.next(200_000_000));
try expectEq(Runner.Step.more, try r.next(200_000_000));
try expectEq(Runner.Step.more, try r.next(200_000_000));
try expectEq(Runner.Step.more, try r.next(200_000_000));
try expectEq(Runner.Step.more, try r.next(200_000_000));
try expectEq(Runner.Step.more, try r.next(200_000_000));
try expectEq(Runner.Step.more, try r.next(200_000_000));
try expectEq(Runner.Step.more, try r.next(200_000_000));
try expectEq(Runner.Step.more, try r.next(200_000_000));
try expectEq(Runner.Step.more, try r.next(200_000_000));
try expectEq(Runner.Step.more, try r.next(200_000_000));
try expectEq(Runner.Step.more, try r.next(200_000_000));
try expectEq(Runner.Step.more, try r.next(200_000_000));
try expectEq(Runner.Step.more, try r.next(200_000_000));
try expectEq(Runner.Step.more, try r.next(200_000_000));
try expectEq(Runner.Step.more, try r.next(200_000_000));
try expectEq(Runner.Step.more, try r.next(200_000_000));
try expectEq(Runner.Step.more, try r.next(200_000_000));
try expectEq(@as(?Runner.Step, null), try r.next(200_000_000));
try expectEq(Step.more, try r.next(Reading.init(200_000_000)));
try expectEq(Step.more, try r.next(Reading.init(200_000_000)));
try expectEq(Step.more, try r.next(Reading.init(200_000_000)));
try expectEq(Step.more, try r.next(Reading.init(200_000_000)));
try expectEq(Step.more, try r.next(Reading.init(200_000_000)));
try expectEq(Step.more, try r.next(Reading.init(200_000_000)));
try expectEq(Step.more, try r.next(Reading.init(200_000_000)));
try expectEq(Step.more, try r.next(Reading.init(200_000_000)));
try expectEq(Step.more, try r.next(Reading.init(200_000_000)));
try expectEq(Step.more, try r.next(Reading.init(200_000_000)));
try expectEq(Step.more, try r.next(Reading.init(200_000_000)));
try expectEq(Step.more, try r.next(Reading.init(200_000_000)));
try expectEq(Step.more, try r.next(Reading.init(200_000_000)));
try expectEq(Step.more, try r.next(Reading.init(200_000_000)));
try expectEq(Step.more, try r.next(Reading.init(200_000_000)));
try expectEq(Step.more, try r.next(Reading.init(200_000_000)));
try expectEq(Step.more, try r.next(Reading.init(200_000_000)));
try expectEq(Step.more, try r.next(Reading.init(200_000_000)));
try expectEq(@as(?Step, null), try r.next(Reading.init(200_000_000)));
}
const timings_ns = try r.finish();
defer std.testing.allocator.free(timings_ns);
const result = try r.finish();
defer std.testing.allocator.free(result.timings_ns);
try expectEqSlices(u64, &.{
200_000_000, 200_000_000, 200_000_000, 200_000_000,
200_000_000, 200_000_000, 200_000_000, 200_000_000,
}, timings_ns);
}, result.timings_ns);
}
14 changes: 14 additions & 0 deletions util/runner/types.zig
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,17 @@ const std = @import("std");
pub const Error = std.mem.Allocator.Error;

pub const Step = enum { more };

pub const Reading = struct {
timing_ns: u64,

pub fn init(timing_ns: u64) Reading {
return .{
.timing_ns = timing_ns,
};
}
};

pub const Readings = struct {
timings_ns: []u64,
};
Loading

0 comments on commit ac1b084

Please sign in to comment.