lib/bench/src/stats/storage.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const alloc_phase = @import("alloc_phase");
3 const capacity_mod = @import("capacity.zig");
4 const model = @import("model.zig");
5
6 pub const Regions = struct {
7 sorted: []u64,
8 means: []f64,
9 medians: []f64,
10 p75s: []f64,
11 p95s: []f64,
12 p99s: []f64,
13 counts: []u32,
14 };
15
16 pub const Status = struct {
17 phase: alloc_phase.capacity.Phase,
18 in_use: bool,
19 storage_bytes: usize,
20 samples: usize,
21 bootstrap_iterations: u32,
22 };
23
24 pub const Storage = struct {
25 phase: alloc_phase.capacity.Phase,
26 capacity: capacity_mod.Capacity,
27 bytes: []align(capacity_mod.storage_alignment) u8,
28 sorted: []u64,
29 means: []f64,
30 medians: []f64,
31 p75s: []f64,
32 p95s: []f64,
33 p99s: []f64,
34 counts: []u32,
35 in_use: bool = false,
36
37 pub const Limits: type = capacity_mod.Limits;
38 pub const Capacity: type = capacity_mod.Capacity;
39 pub const Exhaustion: type = model.Exhaustion;
40 pub const InitError = std.mem.Allocator.Error || capacity_mod.DeriveError;
41 pub const AcquireError: type = model.ComputeError;
42
43 pub const claim: alloc_phase.capacity.Declaration = .{
44 .source = .{
45 .id = "bench.statistics_storage",
46 .kind = .phase_static,
47 .limit_source = .caller,
48 .storage = .{
49 .covered = &.{
50 .{
51 .id = "sorted_sample_scratch",
52 .lifetime = .steady,
53 .detail = "sorted sample scratch",
54 },
55 .{
56 .id = "bootstrap_distribution_scratch",
57 .lifetime = .steady,
58 .detail = "bootstrap distribution scratch",
59 },
60 .{
61 .id = "bootstrap_multiplicity_scratch",
62 .lifetime = .steady,
63 .detail = "bootstrap multiplicity scratch",
64 },
65 },
66 .excluded = &.{
67 "caller-owned input samples",
68 "caller-owned benchmark result retention",
69 },
70 },
71 .capacity = .{
72 .inputs = &.{
73 alloc_phase.capacity.bindInput(Limits, "samples", "samples"),
74 alloc_phase.capacity.bindInput(Limits, "bootstrap_iterations", "bootstrap_iterations"),
75 },
76 .type_selectors = &.{
77 alloc_phase.capacity.bindType(u64, "u64"),
78 alloc_phase.capacity.bindType(f64, "f64"),
79 alloc_phase.capacity.bindType(u32, "u32"),
80 },
81 .nodes = &.{
82 .{ .input = 0 },
83 .{ .scale = .{ .node = 0, .coefficient = .{ .size_of_concrete_type = 0 } } },
84 .{ .input = 1 },
85 .{ .scale = .{ .node = 2, .coefficient = .{ .literal = 5 } } },
86 .{ .scale = .{ .node = 3, .coefficient = .{ .size_of_concrete_type = 1 } } },
87 .{ .scale = .{ .node = 0, .coefficient = .{ .size_of_concrete_type = 2 } } },
88 .{ .add = .{ .left = 1, .right = 4 } },
89 .{ .add = .{ .left = 6, .right = 5 } },
90 },
91 .assertions = &.{.{
92 .scope = .closure_total,
93 .measure = .retained,
94 .relation = .exact,
95 .expression = 7,
96 }},
97 },
98 .overload = .{
99 .kind = .reject_before_mutation,
100 .detail = "empty, oversize, invalid confidence, and concurrent requests fail before workspace mutation",
101 },
102 .risks = .{
103 .transitive = .{
104 .status = .witnessed,
105 .detail = "sorting and bootstrap sampling use only slices acquired from the sealed region",
106 },
107 .foreign = .{
108 .status = .excluded,
109 .detail = "statistics calculation crosses no operating-system or foreign callback boundary",
110 },
111 },
112 .obligations = &.{
113 .{ .key = "bench_statistics_capacity", .role = .capacity_model },
114 .{ .key = "bench_statistics_acquisition", .role = .custom },
115 .{ .key = "bench_statistics_oom", .role = .custom },
116 .{ .key = "bench_statistics_boundaries", .role = .overload },
117 .{ .key = "bench_statistics_reuse", .role = .overload },
118 .{ .key = "bench_statistics_sealed_transitive_risk", .role = .transitive_risk },
119 .{ .key = "bench_statistics_sealed_foreign_risk", .role = .foreign_risk },
120 .{ .key = "bench_statistics_sets", .role = .custom },
121 },
122 },
123 .bindings = .{
124 .owner = @This(),
125 .seal = .{
126 .family = alloc_phase.capacity.selector(@This().activate),
127 .premise = .{
128 .class = .checked_semantic_fact,
129 .authority = .checker,
130 },
131 },
132 .teardown = .{
133 .family = alloc_phase.capacity.selector(@This().deinit),
134 .premise = .{
135 .class = .checked_semantic_fact,
136 .authority = .checker,
137 },
138 },
139 },
140 };
141
142 pub fn init(allocator: std.mem.Allocator, limits: Limits) InitError!Storage {
143 const capacity = try Capacity.derive(limits);
144 const bytes = try allocator.alignedAlloc(
145 u8,
146 .fromByteUnits(capacity_mod.storage_alignment),
147 capacity.storage_bytes,
148 );
149 return .{
150 .phase = .initialization,
151 .capacity = capacity,
152 .bytes = bytes,
153 .sorted = typedSlice(u64, bytes, capacity.sorted_offset, limits.samples),
154 .means = typedSlice(f64, bytes, capacity.means_offset, limits.bootstrap_iterations),
155 .medians = typedSlice(f64, bytes, capacity.medians_offset, limits.bootstrap_iterations),
156 .p75s = typedSlice(f64, bytes, capacity.p75s_offset, limits.bootstrap_iterations),
157 .p95s = typedSlice(f64, bytes, capacity.p95s_offset, limits.bootstrap_iterations),
158 .p99s = typedSlice(f64, bytes, capacity.p99s_offset, limits.bootstrap_iterations),
159 .counts = typedSlice(u32, bytes, capacity.counts_offset, limits.samples),
160 };
161 }
162
163 pub fn activate(self: *Storage) void {
164 std.debug.assert(self.phase == .initialization);
165 std.debug.assert(self.bytes.len == self.capacity.storage_bytes);
166 self.phase = .steady;
167 }
168
169 pub fn acquire(
170 self: *Storage,
171 samples: usize,
172 bootstrap: model.BootstrapConfig,
173 ) AcquireError!Regions {
174 std.debug.assert(self.phase == .steady);
175 if (self.in_use) return error.StatisticsStorageInUse;
176 if (samples == 0) return error.EmptySampleSet;
177 if (samples > self.capacity.limits.samples) return error.SampleCapacityExceeded;
178 if (bootstrap.iterations > self.capacity.limits.bootstrap_iterations) {
179 return error.BootstrapIterationCapacityExceeded;
180 }
181 if (bootstrap.iterations > 0 and
182 (bootstrap.confidence_per_mille == 0 or bootstrap.confidence_per_mille > 1000))
183 {
184 return error.InvalidBootstrapConfidence;
185 }
186 self.in_use = true;
187 return .{
188 .sorted = self.sorted[0..samples],
189 .means = self.means[0..bootstrap.iterations],
190 .medians = self.medians[0..bootstrap.iterations],
191 .p75s = self.p75s[0..bootstrap.iterations],
192 .p95s = self.p95s[0..bootstrap.iterations],
193 .p99s = self.p99s[0..bootstrap.iterations],
194 .counts = self.counts[0..samples],
195 };
196 }
197
198 pub fn reset(self: *Storage) void {
199 std.debug.assert(self.phase == .steady);
200 std.debug.assert(self.in_use);
201 self.in_use = false;
202 }
203
204 pub fn status(self: *const Storage) Status {
205 return .{
206 .phase = self.phase,
207 .in_use = self.in_use,
208 .storage_bytes = self.capacity.storage_bytes,
209 .samples = self.capacity.limits.samples,
210 .bootstrap_iterations = self.capacity.limits.bootstrap_iterations,
211 };
212 }
213
214 pub fn deinit(self: *Storage, allocator: std.mem.Allocator) void {
215 std.debug.assert(self.phase != .teardown);
216 std.debug.assert(!self.in_use);
217 std.debug.assert(self.bytes.len == self.capacity.storage_bytes);
218 self.phase = .teardown;
219 allocator.free(self.bytes);
220 self.bytes = &.{};
221 self.sorted = &.{};
222 self.means = &.{};
223 self.medians = &.{};
224 self.p75s = &.{};
225 self.p95s = &.{};
226 self.p99s = &.{};
227 self.counts = &.{};
228 }
229 };
230
231 fn typedSlice(
232 comptime T: type,
233 bytes: []align(capacity_mod.storage_alignment) u8,
234 offset: usize,
235 count: usize,
236 ) []T {
237 const byte_count = count * @sizeOf(T);
238 const region: []align(@alignOf(T)) u8 = @alignCast(bytes[offset..][0..byte_count]);
239 return std.mem.bytesAsSlice(T, region);
240 }
241
242 fn checkInitFailures(allocator: std.mem.Allocator) !void {
243 var storage = try Storage.init(allocator, .{ .samples = 13, .bootstrap_iterations = 1000 });
244 storage.deinit(allocator);
245 }
246
247 test "statistics storage acquires one exact region" {
248 comptime {
249 @stardustClaim(
250 @import("alloc_phase").capacity.witness(Storage, "bench_statistics_acquisition"),
251 null,
252 null,
253 null,
254 null,
255 null,
256 null,
257 );
258 }
259
260 var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{});
261 const limits = capacity_mod.Limits{ .samples = 13, .bootstrap_iterations = 1000 };
262 const capacity = try capacity_mod.Capacity.derive(limits);
263 var storage = try Storage.init(failing.allocator(), limits);
264 defer storage.deinit(failing.allocator());
265
266 try std.testing.expectEqual(@as(usize, 1), failing.alloc_index);
267 try std.testing.expectEqual(capacity.storage_bytes, failing.allocated_bytes);
268 try std.testing.expectEqual(alloc_phase.capacity.Phase.initialization, storage.status().phase);
269 storage.activate();
270 const regions = try storage.acquire(13, .{});
271 defer storage.reset();
272 const base = @intFromPtr(storage.bytes.ptr);
273 try std.testing.expectEqual(base + capacity.sorted_offset, @intFromPtr(regions.sorted.ptr));
274 try std.testing.expectEqual(base + capacity.means_offset, @intFromPtr(regions.means.ptr));
275 try std.testing.expectEqual(base + capacity.counts_offset, @intFromPtr(regions.counts.ptr));
276 }
277
278 test "statistics storage retries after every allocation failure" {
279 comptime {
280 @stardustClaim(
281 @import("alloc_phase").capacity.witness(Storage, "bench_statistics_oom"),
282 null,
283 null,
284 null,
285 null,
286 null,
287 null,
288 );
289 }
290
291 try std.testing.checkAllAllocationFailures(std.testing.allocator, checkInitFailures, .{});
292 }
293
294 comptime {
295 alloc_phase.capacity.requireAllocatorRejectingOwnerShape(Storage);
296 }