diff --git a/build.zig b/build.zig index ffbb0be..41fbbd1 100644 --- a/build.zig +++ b/build.zig @@ -61,6 +61,7 @@ fn setupExamples(b: *std.Build, target: std.Build.ResolvedTarget, optimize: std. const example_names = [_][]const u8{ "basic", "bubble_sort", + "bubble_sort_hooks", "hooks", "json", "memory_tracking", diff --git a/docs/hooks.md b/docs/hooks.md new file mode 100644 index 0000000..2160067 --- /dev/null +++ b/docs/hooks.md @@ -0,0 +1,69 @@ +# Hooks + +This guide explains what lifecycle hooks are and how to use them. + +## Concepts + +Lifecycle hooks provide control over a benchmark environment. A hook is a function with the following signature: `fn () void`. Its execution is not included in the benchmark reports. + +There are 4 kinds of hooks summarised in the following table: + +| Hook | When is it called ? | Goal/Actions | Example(s) | +|---------------|----------------------------------------|--------------------|---------------------------------------------------------| +| `before_all` | Executed at the start of the benchmark | Global setup | Allocate memory, initialize variables for the benchmark | +| `before_each` | Executed before each iteration | Iteration setup | Setup/Allocate benchmark data | +| `after_each` | Executed after each iteration | Iteration teardown | Reset/Free benchmark data | +| `after_all` | Executed at the end of the benchmark | Global teardown | Free memory, deinit variables | + +## Usage + +zBench provides two ways to register hooks: globally or for a given benchmark. + +--- + +Global registration adds hooks to each added benchmark. + +```zig +test "bench test hooks" { + const stdout = std.io.getStdOut().writer(); + var bench = zbench.Benchmark.init(std.testing.allocator, .{ .hooks = .{ + .before_all = beforeAllHook, + .after_all = afterAllHook, + } }); + defer bench.deinit(); + + try bench.add("Benchmark 1 ", myBenchmark, .{}); + try bench.add("Benchmark 2 ", myBenchmark, .{}); + + try stdout.writeAll("\n"); + try bench.run(stdout); +} +``` + +In this example, both Benchmark 1 and Benchmark 2 will execute `beforeAllHook` and `afterAllHook`. Note that `before_each` and `after_each` can be omitted because hooks are optional. + +--- + +Hooks can also be included with the `add` and `addParam` methods. + +```zig +test "bench test hooks" { + const stdout = std.io.getStdOut().writer(); + var bench = zbench.Benchmark.init(std.testing.allocator, .{}); + defer bench.deinit(); + + try bench.add("Benchmark 1", myBenchmark, .{ + .hooks = .{ + .before_all = beforeAllHook, + .after_all = afterAllHook, + }, + }); + + try bench.add("Benchmark 2", myBenchmark, .{}); + + try stdout.writeAll("\n"); + try bench.run(stdout); +} +``` + +In this example, only Benchmark 1 will execute `beforeAllHook` and `afterAllHook`. \ No newline at end of file diff --git a/examples/bubble_sort_hooks.zig b/examples/bubble_sort_hooks.zig new file mode 100644 index 0000000..3bd14f1 --- /dev/null +++ b/examples/bubble_sort_hooks.zig @@ -0,0 +1,99 @@ +// This example shows how to use hooks to provide more control over a benchmark. +// The bubble_sort.zig example is enhanced with randomly generated numbers. +// Global strategy: +// * At the start of the benchmark, i.e., before the first iteration, we allocate an ArrayList and setup a random number generator. +// * Before each iteration, we fill the ArrayList with random numbers. +// * After each iteration, we reset the ArrayList while keeping the allocated memory. +// * At the end of the benchmark, we deinit the ArrayList. +const std = @import("std"); +const inc = @import("include"); +const zbench = @import("zbench"); + +// Global variables modified/accessed by the hooks. +const test_allocator = std.testing.allocator; +const array_size: usize = 100; +// BenchmarkData contains the data generation logic. +var benchmark_data: BenchmarkData = undefined; + +// Hooks do not accept any parameters and cannot return anything. +fn beforeAll() void { + benchmark_data.init(test_allocator, array_size) catch unreachable; +} + +fn beforeEach() void { + benchmark_data.fill(); +} + +fn myBenchmark(_: std.mem.Allocator) void { + bubbleSort(benchmark_data.numbers.items); +} + +fn bubbleSort(nums: []i32) void { + var i: usize = nums.len - 1; + while (i > 0) : (i -= 1) { + var j: usize = 0; + while (j < i) : (j += 1) { + if (nums[j] > nums[j + 1]) { + std.mem.swap(i32, &nums[j], &nums[j + 1]); + } + } + } +} + +fn afterEach() void { + benchmark_data.reset(); +} + +fn afterAll() void { + benchmark_data.deinit(); +} + +test "bench test bubbleSort with hooks" { + const stdout = std.io.getStdOut().writer(); + + var bench = zbench.Benchmark.init(test_allocator, .{}); + defer bench.deinit(); + + try bench.add("Bubble Sort Benchmark", myBenchmark, .{ + .track_allocations = true, // Option used to show that hooks are not included in the tracking. + .hooks = .{ // Fields are optional and can be omitted. + .before_all = beforeAll, + .after_all = afterAll, + .before_each = beforeEach, + .after_each = afterEach, + }, + }); + + try stdout.writeAll("\n"); + try bench.run(stdout); +} + +const BenchmarkData = struct { + rand: std.Random, + numbers: std.ArrayList(i32), + prng: std.Random.DefaultPrng, + + pub fn init(self: *BenchmarkData, allocator: std.mem.Allocator, num: usize) !void { + self.prng = std.rand.DefaultPrng.init(blk: { + var seed: u64 = undefined; + std.posix.getrandom(std.mem.asBytes(&seed)) catch unreachable; + break :blk seed; + }); + self.rand = self.prng.random(); + self.numbers = try std.ArrayList(i32).initCapacity(allocator, num); + } + + pub fn deinit(self: BenchmarkData) void { + self.numbers.deinit(); + } + + pub fn fill(self: *BenchmarkData) void { + for (0..self.numbers.capacity) |_| { + self.numbers.appendAssumeCapacity(self.rand.intRangeAtMost(i32, 0, 100)); + } + } + + pub fn reset(self: *BenchmarkData) void { + self.numbers.clearRetainingCapacity(); + } +};