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

Add memory allocation tracking #69

Merged
merged 1 commit into from
Apr 10, 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 @@ -63,6 +63,7 @@ fn setupExamples(b: *std.Build, target: std.zig.CrossTarget, optimize: std.built
"bubble_sort",
"hooks",
"json",
"memory_tracking",
"parameterised",
"progress",
"sleep",
Expand Down
9 changes: 8 additions & 1 deletion examples/json.zig
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,14 @@ test "bench test json" {
var bench = zbench.Benchmark.init(test_allocator, .{});
defer bench.deinit();

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

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

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

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

try bench.add("My Benchmark 1", myBenchmark, .{});
try bench.add("My Benchmark 2", myBenchmark, .{
.track_allocations = true,
});

try stdout.writeAll("\n");
try bench.run(stdout);
}
126 changes: 92 additions & 34 deletions util/runner.zig
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ const expectEqSlices = std.testing.expectEqualSlices;

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

Expand Down Expand Up @@ -45,32 +47,40 @@ const State = union(enum) {
/// Number of timings still to be performed in the benchmark.
iterations_remaining: usize,

/// Array of timings collected.
timings_ns: []u64,
/// Readings collected during the run.
readings: Readings,
};
};

allocator: std.mem.Allocator,
track_allocations: bool,
state: State,

pub fn init(
allocator: std.mem.Allocator,
iterations: u16,
max_iterations: u16,
time_budget_ns: u64,
track_allocations: bool,
) Error!Runner {
return if (iterations == 0) .{
.allocator = allocator,
.track_allocations = track_allocations,
.state = .{ .preparing = .{
.max_iterations = max_iterations,
.time_budget_ns = time_budget_ns,
} },
} else .{
.allocator = allocator,
.track_allocations = track_allocations,
.state = .{ .running = .{
.iterations_count = iterations,
.iterations_remaining = iterations,
.timings_ns = try allocator.alloc(u64, iterations),
.readings = try Readings.init(
allocator,
iterations,
track_allocations,
),
} },
};
}
Expand Down Expand Up @@ -101,15 +111,19 @@ pub fn next(self: *Runner, reading: Reading) Error!?Step {
self.state = .{ .running = .{
.iterations_count = N,
.iterations_remaining = N,
.timings_ns = try self.allocator.alloc(u64, N),
.readings = try Readings.init(
self.allocator,
N,
self.track_allocations,
),
} };
}
return .more;
},
.running => |*st| {
if (0 < st.iterations_remaining) {
const i = st.timings_ns.len - st.iterations_remaining;
st.timings_ns[i] = reading.timing_ns;
const i = st.readings.iterations - st.iterations_remaining;
st.readings.set(i, reading);
st.iterations_remaining -= 1;
}
return if (st.iterations_remaining == 0) null else .more;
Expand All @@ -121,20 +135,21 @@ pub fn next(self: *Runner, reading: Reading) Error!?Step {
/// complete, so get the timing results.
pub fn finish(self: *Runner) Error!Readings {
return switch (self.state) {
.preparing => .{
.preparing => Readings{
.allocator = self.allocator,
.iterations = 0,
.timings_ns = &.{},
.allocations = null,
},
.running => |st| .{
.timings_ns = st.timings_ns,
},
.running => |st| st.readings,
};
}

/// Clean up after an error.
pub fn abort(self: *Runner) void {
return switch (self.state) {
.preparing => {},
.running => |st| self.allocator.free(st.timings_ns),
.running => |st| st.readings.deinit(),
};
}

Expand All @@ -157,33 +172,76 @@ pub fn status(self: Runner) Status {
}

test "Runner" {
var r = try Runner.init(std.testing.allocator, 0, 16384, 2e9);
var r = try Runner.init(std.testing.allocator, 0, 16384, 2e9, false);
{
errdefer r.abort();
try expectEq(Step.more, try r.next(Reading.init(100_000_000, null)));
try expectEq(Step.more, try r.next(Reading.init(200_000_000, null)));
try expectEq(Step.more, try r.next(Reading.init(300_000_000, null)));
try expectEq(Step.more, try r.next(Reading.init(100_000_000, null)));
try expectEq(Step.more, try r.next(Reading.init(200_000_000, null)));
try expectEq(Step.more, try r.next(Reading.init(300_000_000, null)));
try expectEq(Step.more, try r.next(Reading.init(100_000_000, null)));
try expectEq(Step.more, try r.next(Reading.init(200_000_000, null)));
try expectEq(Step.more, try r.next(Reading.init(300_000_000, null)));
try expectEq(Step.more, try r.next(Reading.init(200_000_000, null)));
try expectEq(Step.more, try r.next(Reading.init(200_000_000, null)));

try expectEq(Step.more, try r.next(Reading.init(100_000_000, null)));
try expectEq(Step.more, try r.next(Reading.init(200_000_000, null)));
try expectEq(Step.more, try r.next(Reading.init(300_000_000, null)));
try expectEq(Step.more, try r.next(Reading.init(400_000_000, null)));
try expectEq(Step.more, try r.next(Reading.init(100_000_000, null)));
try expectEq(Step.more, try r.next(Reading.init(200_000_000, null)));
try expectEq(Step.more, try r.next(Reading.init(300_000_000, null)));
try expectEq(@as(?Step, null), try r.next(Reading.init(400_000_000, null)));
}
const result = try r.finish();
defer result.deinit();
try expectEqSlices(u64, &.{
100_000_000, 200_000_000, 300_000_000, 400_000_000,
100_000_000, 200_000_000, 300_000_000, 400_000_000,
}, result.timings_ns);
}

test "Runner - memory tracking" {
var r = try Runner.init(std.testing.allocator, 0, 16384, 2e9, true);
{
errdefer r.abort();
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)));
try expectEq(Step.more, try r.next(Reading.init(100_000_000, .{ .max = 256, .count = 1 })));
try expectEq(Step.more, try r.next(Reading.init(200_000_000, .{ .max = 256, .count = 1 })));
try expectEq(Step.more, try r.next(Reading.init(300_000_000, .{ .max = 512, .count = 2 })));
try expectEq(Step.more, try r.next(Reading.init(100_000_000, .{ .max = 512, .count = 2 })));
try expectEq(Step.more, try r.next(Reading.init(200_000_000, .{ .max = 1024, .count = 4 })));
try expectEq(Step.more, try r.next(Reading.init(300_000_000, .{ .max = 1024, .count = 4 })));
try expectEq(Step.more, try r.next(Reading.init(100_000_000, .{ .max = 2048, .count = 8 })));
try expectEq(Step.more, try r.next(Reading.init(200_000_000, .{ .max = 2045, .count = 8 })));
try expectEq(Step.more, try r.next(Reading.init(300_000_000, .{ .max = 4096, .count = 16 })));
try expectEq(Step.more, try r.next(Reading.init(200_000_000, .{ .max = 4096, .count = 16 })));
try expectEq(Step.more, try r.next(Reading.init(200_000_000, .{ .max = 8192, .count = 32 })));

try expectEq(Step.more, try r.next(Reading.init(100, .{ .max = 1, .count = 2 })));
try expectEq(Step.more, try r.next(Reading.init(200, .{ .max = 2, .count = 4 })));
try expectEq(Step.more, try r.next(Reading.init(300, .{ .max = 4, .count = 8 })));
try expectEq(Step.more, try r.next(Reading.init(400, .{ .max = 8, .count = 16 })));
try expectEq(Step.more, try r.next(Reading.init(100, .{ .max = 16, .count = 32 })));
try expectEq(Step.more, try r.next(Reading.init(200, .{ .max = 32, .count = 64 })));
try expectEq(Step.more, try r.next(Reading.init(300, .{ .max = 64, .count = 128 })));
try expectEq(@as(?Step, null), try r.next(Reading.init(400, .{ .max = 128, .count = 256 })));
}
const result = try r.finish();
defer std.testing.allocator.free(result.timings_ns);
defer result.deinit();
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,
100, 200, 300, 400, 100, 200, 300, 400,
}, result.timings_ns);
try expectEqSlices(
usize,
&.{ 1, 2, 4, 8, 16, 32, 64, 128 },
result.allocations.?.maxes,
);
try expectEqSlices(
usize,
&.{ 2, 4, 8, 16, 32, 64, 128, 256 },
result.allocations.?.counts,
);
}
74 changes: 73 additions & 1 deletion util/runner/types.zig
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,86 @@ pub const Step = enum { more };

pub const Reading = struct {
timing_ns: u64,
allocation: ?AllocationReading,

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

pub const Readings = struct {
allocator: std.mem.Allocator,
iterations: usize,
timings_ns: []u64,
allocations: ?AllocationReadings,

pub fn init(
allocator: std.mem.Allocator,
n: usize,
track_allocations: bool,
) !Readings {
return Readings{
.allocator = allocator,
.iterations = n,
.timings_ns = try allocator.alloc(u64, n),
.allocations = if (track_allocations)
try AllocationReadings.init(allocator, n)
else
null,
};
}

pub fn deinit(self: Readings) void {
self.allocator.free(self.timings_ns);
if (self.allocations) |allocs| allocs.deinit(self.allocator);
}

pub fn set(self: *Readings, i: usize, reading: Reading) void {
self.timings_ns[i] = reading.timing_ns;
if (self.allocations) |allocs| {
if (reading.allocation) |x| {
allocs.maxes[i] = x.max;
allocs.counts[i] = x.count;
} else {
allocs.deinit(self.allocator);
self.allocations = null;
}
}
}

pub fn sort(self: *Readings) void {
std.sort.heap(u64, self.timings_ns, {}, std.sort.asc(u64));
if (self.allocations) |allocs| {
std.sort.heap(usize, allocs.maxes, {}, std.sort.asc(usize));
std.sort.heap(usize, allocs.counts, {}, std.sort.asc(usize));
}
}
};

pub const AllocationReading = struct {
max: usize,
count: usize,
};

pub const AllocationReadings = struct {
maxes: []usize,
counts: []usize,

pub fn init(allocator: std.mem.Allocator, n: usize) !AllocationReadings {
return AllocationReadings{
.maxes = try allocator.alloc(usize, n),
.counts = try allocator.alloc(usize, n),
};
}

pub fn deinit(self: AllocationReadings, allocator: std.mem.Allocator) void {
allocator.free(self.maxes);
allocator.free(self.counts);
}
};
1 change: 1 addition & 0 deletions util/statistics.zig
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const std = @import("std");

/// Collect common statistical calculations together.
pub fn Statistics(comptime T: type) type {
return struct {
total: T,
Expand Down
Loading
Loading