Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ fn setupExamples(b: *std.Build, target: std.Build.ResolvedTarget, optimize: std.
"shuffling_allocator",
"sleep",
"systeminfo",
"throughput",
};

for (example_names) |example_name| {
Expand All @@ -88,7 +89,7 @@ fn setupExamples(b: *std.Build, target: std.Build.ResolvedTarget, optimize: std.
}),
});
const install_example = b.addInstallArtifact(example, .{});
const zbench_mod = b.addModule("zbench", .{
const zbench_mod = b.createModule(.{
.root_source_file = .{ .src_path = .{ .owner = b, .sub_path = "src/zbench.zig" } },
});
example.root_module.addImport("zbench", zbench_mod);
Expand Down
11 changes: 11 additions & 0 deletions examples/json.zig
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,17 @@ pub fn main(init: std.process.Init) !void {
.iterations = 10,
.track_allocations = true,
});
try bench.add("My Benchmark 3", myBenchmark, .{
.iterations = 10,
.track_allocations = true,
.bytes_per_run = 2_000 * 1024,
});
try bench.add("My Benchmark 4", myBenchmark, .{
.iterations = 10,
.track_allocations = true,
.bytes_per_run = 2_000 * 1024,
.items_per_run = 2,
});

try writer.writeAll("[");
var iter = try bench.iterator();
Expand Down
24 changes: 24 additions & 0 deletions examples/throughput.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
const std = @import("std");
const zbench = @import("zbench");

fn myBenchmark(allocator: std.mem.Allocator) void {
for (0..1000) |_| {
const buf = allocator.alloc(u8, 512) catch @panic("Out of memory");
defer allocator.free(buf);
}
}

pub fn main(init: std.process.Init) !void {
const io = init.io;
const stdout: std.Io.File = .stdout();

var bench = zbench.Benchmark.init(init.gpa, .{});
defer bench.deinit();

try bench.add("My Benchmark Default", myBenchmark, .{
.bytes_per_run = 512,
.items_per_run = 1,
});

try bench.run(io, stdout);
}
8 changes: 8 additions & 0 deletions src/benchmark.zig
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,14 @@ pub const Config = struct {
/// This can be combined with track_allocations to wrap
/// the shuffling allocator in a tracking allocator.
use_shuffling_allocator: bool = false,

/// Number of bytes processed by one benchmark run.
/// When set, reports throughput as bytes per second.
bytes_per_run: ?usize = null,

/// Number of items processed by one benchmark run.
/// When set, reports throughput as items per second.
items_per_run: ?usize = null,
};

/// A function pointer type that represents a benchmark function.
Expand Down
58 changes: 26 additions & 32 deletions src/partial.zig
Original file line number Diff line number Diff line change
Expand Up @@ -2,48 +2,42 @@ const std = @import("std");

/// Make every field of the struct T nullable.
pub fn Partial(comptime T: type) type {
const T_info = switch (@typeInfo(T)) {
.@"struct" => |x| x,
else => @compileError("Partial only supports struct types for now"),
};

const fields = T_info.fields;

comptime var field_names: [fields.len][]const u8 = undefined;
comptime var field_types: [fields.len]type = undefined;
comptime var field_attrs: [fields.len]std.builtin.Type.StructField.Attributes = undefined;

inline for (fields, 0..) |field, i| {
field_names[i] = field.name;
field_types[i] = ?field.type;
field_attrs[i] = .{
.@"comptime" = field.is_comptime,
.@"align" = field.alignment,
.default_value_ptr = &@as(?field.type, null),
const names = @typeInfo(T).@"struct".field_names;

var types: [names.len]type = undefined;
var attrs: [names.len]std.lang.Type.Struct.FieldAttributes =
@splat(.{});

inline for (names, 0..) |name, i| {
const FieldType = @FieldType(T, name);

types[i] = ?FieldType;
attrs[i] = .{
.default_value_ptr = &@as(?FieldType, null),
};
}

return @Struct(
T_info.layout,
T_info.backing_integer,
&field_names,
&field_types,
&field_attrs,
.auto,
null,
names,
&types,
&attrs,
);
}

/// Take any non-null fields from x, and any null fields are taken from y
/// instead.
pub fn partial(comptime T: type, x: Partial(T), y: T) T {
const T_info = switch (@typeInfo(T)) {
.@"struct" => |info| info,
else => @compileError("Partial only supports struct types for now"),
};
var t: T = undefined;
inline for (T_info.fields) |f|
@field(t, f.name) =
if (@field(x, f.name)) |xx| xx else @field(y, f.name);
return t;
var result = y;

inline for (@typeInfo(T).@"struct".field_names) |name| {
if (@field(x, name)) |value| {
@field(result, name) = value;
}
}

return result;
}

test partial {
Expand Down
110 changes: 106 additions & 4 deletions src/result.zig
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,21 @@ const NAME_LEN_LIMIT = @import("zbench.zig").NAME_LEN_LIMIT;
pub const Result = struct {
name: []const u8,
readings: Readings,
bytes_per_run: ?usize,
items_per_run: ?usize,

pub fn init(name: []const u8, readings: Runner.Readings) Result {
return Result{ .name = name, .readings = readings };
pub fn init(
name: []const u8,
readings: Runner.Readings,
bytes_per_run: ?usize,
items_per_run: ?usize,
) Result {
return Result{
.name = name,
.readings = readings,
.bytes_per_run = bytes_per_run,
.items_per_run = items_per_run,
};
}

pub fn deinit(self: Result) void {
Expand Down Expand Up @@ -65,6 +77,30 @@ pub const Result = struct {
});
_ = try std.Io.Writer.alignBuffer(writer, tmp, 23, .left, ' ');

// Throughput bytes
if (self.bytes_per_run) |bytes| {
try terminal.setColor(Color.green);
tmp = try std.fmt.bufPrint(&buf, "{d:.3}", .{
statistics.throughputPerSecond(bytes, self.readings.timings_ns.len, s.total),
});

_ = try std.Io.Writer.alignBuffer(writer, tmp, 15, .left, ' ');
} else {
_ = try std.Io.Writer.alignBuffer(writer, "-", 15, .left, ' ');
}

// Throughput items
if (self.items_per_run) |items| {
try terminal.setColor(Color.green);
tmp = try std.fmt.bufPrint(&buf, "{d:.3}", .{
statistics.throughputPerSecond(items, self.readings.timings_ns.len, s.total),
});

_ = try std.Io.Writer.alignBuffer(writer, tmp, 15, .left, ' ');
} else {
_ = try std.Io.Writer.alignBuffer(writer, "-", 15, .left, ' ');
}

// Minimum and maximum
try terminal.setColor(Color.green);
tmp = try std.fmt.bufPrint(&buf, "({f} ... {f})", .{
Expand Down Expand Up @@ -137,33 +173,99 @@ pub const Result = struct {
) !void {
const timings_ns_stats =
try Statistics(u64).init(self.readings.timings_ns);
const throughput_json = fmtThroughputJSON(
self.bytes_per_run,
self.items_per_run,
self.readings.timings_ns.len,
timings_ns_stats,
);
if (self.readings.allocations) |allocs| {
const allocation_maxes_stats =
try Statistics(usize).init(allocs.maxes);
try writer.print(
\\{{ "name": "{f}",
\\ "timing_statistics": {f}, "timings": {f},
\\ "timing_statistics": {f}, "timings": {f}{f},
\\ "max_allocation_statistics": {f}, "max_allocations": {f} }}
,
.{
std.ascii.hexEscape(self.name, .lower),
statistics.fmtJSON(u64, "nanoseconds", timings_ns_stats),
fmt.formatJSONArray(u64, self.readings.timings_ns),
throughput_json,
statistics.fmtJSON(usize, "bytes", allocation_maxes_stats),
fmt.formatJSONArray(usize, allocs.maxes),
},
);
} else {
try writer.print(
\\{{ "name": "{f}",
\\ "timing_statistics": {f}, "timings": {f} }}
\\ "timing_statistics": {f}, "timings": {f}{f} }}
,
.{
std.ascii.hexEscape(self.name, .lower),
statistics.fmtJSON(u64, "nanoseconds", timings_ns_stats),
fmt.formatJSONArray(u64, self.readings.timings_ns),
throughput_json,
},
);
}
}
};

const ThroughputJSON = struct {
bytes_per_run: ?usize,
items_per_run: ?usize,
iterations: usize,
timing_stats: Statistics(u64),

fn format(
data: ThroughputJSON,
writer: *std.Io.Writer,
) !void {
if (data.bytes_per_run == null and data.items_per_run == null)
return;

try writer.writeAll(", \"throughput\": {");
var needs_comma = false;

if (data.bytes_per_run) |bytes| {
try writer.print("\"bytes_per_second\": {d:.3}", .{
statistics.throughputPerSecond(
bytes,
data.iterations,
data.timing_stats.total,
),
});
needs_comma = true;
}

if (data.items_per_run) |items| {
if (needs_comma)
try writer.writeAll(", ");

try writer.print("\"items_per_second\": {d:.3}", .{
statistics.throughputPerSecond(
items,
data.iterations,
data.timing_stats.total,
),
});
}

try writer.writeAll(" }");
}
};

fn fmtThroughputJSON(
bytes_per_run: ?usize,
items_per_run: ?usize,
iterations: usize,
timing_stats: Statistics(u64),
) std.fmt.Alt(ThroughputJSON, ThroughputJSON.format) {
return .{ .data = .{
.bytes_per_run = bytes_per_run,
.items_per_run = items_per_run,
.iterations = iterations,
.timing_stats = timing_stats,
} };
}
18 changes: 18 additions & 0 deletions src/statistics.zig
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,24 @@ pub fn Statistics(comptime T: type) type {
};
}

pub fn throughputPerSecond(
processed_per_run: usize,
iterations: usize,
total_ns: u64,
) f64 {
if (total_ns == 0) return 0;

const total_processed =
@as(f64, @floatFromInt(processed_per_run)) *
@as(f64, @floatFromInt(iterations));

const seconds =
@as(f64, @floatFromInt(total_ns)) /
@as(f64, @floatFromInt(std.time.ns_per_s));

return total_processed / seconds;
}

pub fn fmtJSON(
comptime T: type,
unit: []const u8,
Expand Down
11 changes: 8 additions & 3 deletions src/zbench.zig
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ pub const Benchmark = struct {
) !void {
// Check the benchmark parameter is the proper type.
const T: type = switch (@typeInfo(@TypeOf(benchmark))) {
.pointer => |ptr| if (ptr.is_const) ptr.child else @compileError(
.pointer => |ptr| if (ptr.attrs.@"const") ptr.child else @compileError(
"benchmark must be a const ptr to a struct with a 'run' method",
),
else => @compileError(
Expand Down Expand Up @@ -149,9 +149,12 @@ pub const Benchmark = struct {
defer self.runner = null;
defer self.remaining = self.remaining[1..];
if (self.remaining[0].config.hooks.after_all) |hook| hook();

return Step{ .result = Result.init(
self.remaining[0].name,
try runner.finish(),
self.remaining[0].config.bytes_per_run,
self.remaining[0].config.items_per_run,
) };
}
}
Expand Down Expand Up @@ -193,8 +196,8 @@ pub const Benchmark = struct {
/// Write the prettyPrint() header to a writer.
pub fn prettyPrintHeader(io: std.Io, file: std.Io.File, name_len: usize) !void {
const _name_len = if (name_len > NAME_LEN_LIMIT) NAME_LEN_LIMIT else name_len;
const header_fmt: []const u8 = "{s:<8} {s:<14} {s:<23} {s:<28} {s:<10} {s:<10} {s:<10}\n";
const dashes_repeat: usize = 111;
const header_fmt: []const u8 = "{s:<8} {s:<14} {s:<23} {s:<14} {s:<14} {s:<28} {s:<10} {s:<10} {s:<10}\n";
const dashes_repeat: usize = 139;

var w: std.Io.File.Writer = file.writerStreaming(io, &.{});
const writer: *std.Io.Writer = &w.interface;
Expand All @@ -209,6 +212,8 @@ pub fn prettyPrintHeader(io: std.Io, file: std.Io.File, name_len: usize) !void {
"runs",
"total time",
"time/run (avg ± σ)",
"bytes/sec",
"items/sec",
"(min ... max)",
"p75",
"p99",
Expand Down
Loading