summaryrefslogtreecommitdiff
path: root/src/benchmark/Benchmark.zig
blob: 0ca1544147b350decf8ef6a22af27fa012ae6a2b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
//! A single benchmark case.
const Benchmark = @This();

const std = @import("std");
const builtin = @import("builtin");
const assert = std.debug.assert;
const macos = @import("macos");
const build_config = @import("../build_config.zig");

ptr: *anyopaque,
vtable: VTable,

/// Create a new benchmark from a pointer and a vtable.
///
/// This usually is only called by benchmark implementations, not
/// benchmark users.
pub fn init(
    pointer: anytype,
    vtable: VTable,
) Benchmark {
    const Ptr = @TypeOf(pointer);
    assert(@typeInfo(Ptr) == .pointer); // Must be a pointer
    assert(@typeInfo(Ptr).pointer.size == .one); // Must be a single-item pointer
    assert(@typeInfo(@typeInfo(Ptr).pointer.child) == .@"struct"); // Must point to a struct
    return .{ .ptr = pointer, .vtable = vtable };
}

/// Run the benchmark.
pub fn run(
    self: Benchmark,
    mode: RunMode,
) Error!RunResult {
    // Run our setup function if it exists. We do this first because
    // we don't want this part of our benchmark and we want to fail fast.
    if (self.vtable.setupFn) |func| try func(self.ptr);
    defer if (self.vtable.teardownFn) |func| func(self.ptr);

    // Our result accumulator. This will be returned at the end of the run.
    var result: RunResult = .{};

    // If we're on macOS, we setup signposts so its easier to find
    // the results in Instruments. There's a lot of nasty comptime stuff
    // here but its just to ensure this does nothing on other platforms.
    const signpost_name = "ghostty";
    const signpost: if (builtin.target.os.tag.isDarwin()) struct {
        log: *macos.os.Log,
        id: macos.os.signpost.Id,
    } else void = if (builtin.target.os.tag.isDarwin()) darwin: {
        macos.os.signpost.init();
        const log = macos.os.Log.create(
            build_config.bundle_id,
            macos.os.signpost.Category.points_of_interest,
        );
        const id = macos.os.signpost.Id.forPointer(log, self.ptr);
        macos.os.signpost.intervalBegin(log, id, signpost_name);
        break :darwin .{ .log = log, .id = id };
    } else {};
    defer if (comptime builtin.target.os.tag.isDarwin()) {
        macos.os.signpost.intervalEnd(
            signpost.log,
            signpost.id,
            signpost_name,
        );
        signpost.log.release();
    };

    const start = std.time.Instant.now() catch return error.BenchmarkFailed;
    while (true) {
        // Run our step function. If it fails, we return the error.
        try self.vtable.stepFn(self.ptr);
        result.iterations += 1;

        // Get our current monotonic time and check our exit conditions.
        const now = std.time.Instant.now() catch return error.BenchmarkFailed;
        const exit = switch (mode) {
            .once => true,
            .duration => |ns| now.since(start) >= ns,
        };

        if (exit) {
            result.duration = now.since(start);
            return result;
        }
    }

    // We exit within the loop body.
    unreachable;
}

/// The type of benchmark run. This is used to determine how the benchmark
/// is executed.
pub const RunMode = union(enum) {
    /// Run the benchmark exactly once.
    once,

    /// Run the benchmark for a fixed duration in nanoseconds. This
    /// will not interrupt a running step so if the granularity of the
    /// duration is too low, benchmark results may be inaccurate.
    duration: u64,
};

/// The result of a benchmark run.
pub const RunResult = struct {
    /// The total iterations that step was executed. For "once" run
    /// modes this will always be 1.
    iterations: u32 = 0,

    /// The total time taken for the run. For "duration" run modes
    /// this will be relatively close to the requested duration.
    /// The units are nanoseconds.
    duration: u64 = 0,
};

/// The possible errors that can occur during various stages of the
/// benchmark. Right now its just "failure" which ends the benchmark.
pub const Error = error{BenchmarkFailed};

/// The vtable that must be provided to invoke the real implementation.
pub const VTable = struct {
    /// A single step to execute the benchmark. This should do the work
    /// that is under test. This may be called multiple times if we're
    /// testing throughput.
    stepFn: *const fn (ptr: *anyopaque) Error!void,

    /// Setup and teardown functions. These are called once before
    /// the first step and once after the last step. They are not part
    /// of the benchmark results (unless you're benchmarking the full
    /// binary).
    setupFn: ?*const fn (ptr: *anyopaque) Error!void = null,
    teardownFn: ?*const fn (ptr: *anyopaque) void = null,
};

test Benchmark {
    // This test fails on FreeBSD so skip:
    //
    // /home/runner/work/ghostty/ghostty/src/benchmark/Benchmark.zig:165:5: 0x3cd2de1 in decltest.Benchmark (ghostty-test)
    //     try testing.expect(result.duration > 0);
    //     ^
    if (builtin.os.tag == .freebsd) return error.SkipZigTest;

    const testing = std.testing;
    const Simple = struct {
        const Self = @This();

        setup_i: usize = 0,
        step_i: usize = 0,

        pub fn benchmark(self: *Self) Benchmark {
            return .init(self, .{
                .stepFn = step,
                .setupFn = setup,
            });
        }

        fn setup(ptr: *anyopaque) Error!void {
            const self: *Self = @ptrCast(@alignCast(ptr));
            self.setup_i += 1;
        }

        fn step(ptr: *anyopaque) Error!void {
            const self: *Self = @ptrCast(@alignCast(ptr));
            self.step_i += 1;
        }
    };

    var s: Simple = .{};
    const b = s.benchmark();
    const result = try b.run(.once);
    try testing.expectEqual(1, s.setup_i);
    try testing.expectEqual(1, s.step_i);
    try testing.expectEqual(1, result.iterations);
    try testing.expect(result.duration > 0);
}