Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docs: add hooks documentation and example #89

Merged
merged 1 commit into from
Sep 4, 2024
Merged
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
1 change: 1 addition & 0 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
69 changes: 69 additions & 0 deletions docs/hooks.md
Original file line number Diff line number Diff line change
@@ -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`.
99 changes: 99 additions & 0 deletions examples/bubble_sort_hooks.zig
Original file line number Diff line number Diff line change
@@ -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();
}
};
Loading