lib/bench/src/compare/storage.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const alloc_phase = @import("alloc_phase");
3 const capacity_mod = @import("capacity.zig");
4
5 const storage_exhaustion = error{
6 BaseSampleCapacityExceeded,
7 CandidateSampleCapacityExceeded,
8 SampleCapacityExceeded,
9 BootstrapIterationCapacityExceeded,
10 ComparisonStorageInUse,
11 };
12
13 pub const Exhaustion = storage_exhaustion;
14
15 pub const InputError = error{
16 EmptyBaseSeries,
17 EmptyCandidateSeries,
18 EmptySampleSet,
19 InvalidBootstrapIterations,
20 };
21
22 pub const Error = Exhaustion || InputError;
23
24 pub const F64Regions = struct {
25 effects: []f64,
26 samples: []f64,
27 counts: []u32,
28 };
29
30 pub const U64Regions = struct {
31 effects: []f64,
32 samples: []u64,
33 counts: []u32,
34 };
35
36 pub const Status = struct {
37 phase: alloc_phase.capacity.Phase,
38 in_use: bool,
39 storage_bytes: usize,
40 max_samples_per_series: usize,
41 max_bootstrap_iterations: u32,
42 };
43
44 pub const Storage = struct {
45 phase: alloc_phase.capacity.Phase,
46 capacity: capacity_mod.Capacity,
47 bytes: []align(capacity_mod.storage_alignment) u8,
48 in_use: bool = false,
49
50 pub const Limits: type = capacity_mod.Limits;
51 pub const Capacity: type = capacity_mod.Capacity;
52 pub const Exhaustion: type = storage_exhaustion;
53 pub const InitError = std.mem.Allocator.Error || capacity_mod.DeriveError;
54 pub const AcquireError: type = Error;
55
56 pub const claim: alloc_phase.capacity.Declaration = .{
57 .source = .{
58 .id = "bench.comparison_storage",
59 .kind = .phase_static,
60 .limit_source = .caller,
61 .storage = .{
62 .covered = &.{
63 .{
64 .id = "bootstrap_effect_distribution_scratch",
65 .lifetime = .steady,
66 .detail = "bootstrap effect distribution scratch",
67 },
68 .{
69 .id = "median_and_resampling_sample_scratch",
70 .lifetime = .steady,
71 .detail = "median and resampling sample scratch",
72 },
73 .{
74 .id = "bootstrap_multiplicity_scratch",
75 .lifetime = .steady,
76 .detail = "bootstrap multiplicity scratch",
77 },
78 },
79 .excluded = &.{
80 "caller-owned parsed comparison datasets",
81 "caller-owned comparison rows and report output",
82 },
83 },
84 .capacity = .{
85 .inputs = &.{
86 alloc_phase.capacity.bindInput(Limits, "max_bootstrap_iterations", "max_bootstrap_iterations"),
87 alloc_phase.capacity.bindInput(Limits, "max_samples_per_series", "max_samples_per_series"),
88 },
89 .type_selectors = &.{
90 alloc_phase.capacity.bindType(f64, "f64"),
91 alloc_phase.capacity.bindType(u64, "u64"),
92 alloc_phase.capacity.bindType(u32, "u32"),
93 },
94 .nodes = &.{
95 .{ .input = 0 },
96 .{ .scale = .{ .node = 0, .coefficient = .{ .size_of_concrete_type = 0 } } },
97 .{ .constant = 1 },
98 .{ .scale = .{ .node = 2, .coefficient = .{ .size_of_concrete_type = 0 } } },
99 .{ .scale = .{ .node = 2, .coefficient = .{ .size_of_concrete_type = 1 } } },
100 .{ .maximum = .{ .left = 3, .right = 4 } },
101 .{ .scale = .{ .node = 2, .coefficient = .{ .size_of_concrete_type = 2 } } },
102 .{ .add = .{ .left = 5, .right = 6 } },
103 .{ .input = 1 },
104 .{ .product = .{ .left = 8, .right = 7 } },
105 .{ .add = .{ .left = 1, .right = 9 } },
106 },
107 .assertions = &.{.{
108 .scope = .closure_total,
109 .measure = .retained,
110 .relation = .exact,
111 .expression = 10,
112 }},
113 },
114 .overload = .{
115 .kind = .reject_before_mutation,
116 .detail = "empty, zero-iteration, oversize, and concurrent requests fail before workspace mutation",
117 },
118 .risks = .{
119 .transitive = .{
120 .status = .witnessed,
121 .detail = "f64 and u64 median sorting and bootstrap multiplicities use only slices acquired from the sealed region",
122 },
123 .foreign = .{
124 .status = .excluded,
125 .detail = "comparison analysis crosses no operating-system or foreign callback boundary",
126 },
127 },
128 .obligations = &.{
129 .{ .key = "bench_comparison_capacity", .role = .capacity_model },
130 .{ .key = "bench_comparison_acquisition", .role = .custom },
131 .{ .key = "bench_comparison_oom", .role = .custom },
132 .{ .key = "bench_comparison_boundaries", .role = .overload },
133 .{ .key = "bench_comparison_reuse", .role = .overload },
134 .{ .key = "bench_comparison_sealed_f64_transitive_risk", .role = .transitive_risk },
135 .{ .key = "bench_comparison_sealed_f64_foreign_risk", .role = .foreign_risk },
136 .{ .key = "bench_comparison_rows", .role = .custom },
137 .{ .key = "bench_comparison_sealed_u64", .role = .transitive_risk },
138 .{ .key = "bench_comparison_command", .role = .custom },
139 },
140 },
141 .bindings = .{
142 .owner = @This(),
143 .seal = .{
144 .family = alloc_phase.capacity.selector(@This().activate),
145 .premise = .{
146 .class = .checked_semantic_fact,
147 .authority = .checker,
148 },
149 },
150 .teardown = .{
151 .family = alloc_phase.capacity.selector(@This().deinit),
152 .premise = .{
153 .class = .checked_semantic_fact,
154 .authority = .checker,
155 },
156 },
157 },
158 };
159
160 pub fn init(allocator: std.mem.Allocator, limits: Limits) InitError!Storage {
161 const capacity = try Capacity.derive(limits);
162 const bytes = try allocator.alignedAlloc(
163 u8,
164 .fromByteUnits(capacity_mod.storage_alignment),
165 capacity.storage_bytes,
166 );
167 return .{
168 .phase = .initialization,
169 .capacity = capacity,
170 .bytes = bytes,
171 };
172 }
173
174 pub fn activate(self: *Storage) void {
175 std.debug.assert(self.phase == .initialization);
176 std.debug.assert(self.bytes.len == self.capacity.storage_bytes);
177 self.phase = .steady;
178 }
179
180 pub fn acquireF64(
181 self: *Storage,
182 base_samples: usize,
183 candidate_samples: usize,
184 bootstrap_iterations: u32,
185 ) AcquireError!F64Regions {
186 const sample_count = try self.beginComparison(
187 base_samples,
188 candidate_samples,
189 bootstrap_iterations,
190 );
191 return .{
192 .effects = typedSlice(
193 f64,
194 self.bytes,
195 self.capacity.effects_offset,
196 bootstrap_iterations,
197 ),
198 .samples = typedSlice(
199 f64,
200 self.bytes,
201 self.capacity.samples_offset,
202 sample_count,
203 ),
204 .counts = typedSlice(
205 u32,
206 self.bytes,
207 self.capacity.counts_offset,
208 sample_count,
209 ),
210 };
211 }
212
213 pub fn acquireU64(
214 self: *Storage,
215 base_samples: usize,
216 candidate_samples: usize,
217 bootstrap_iterations: u32,
218 ) AcquireError!U64Regions {
219 const sample_count = try self.beginComparison(
220 base_samples,
221 candidate_samples,
222 bootstrap_iterations,
223 );
224 return .{
225 .effects = typedSlice(
226 f64,
227 self.bytes,
228 self.capacity.effects_offset,
229 bootstrap_iterations,
230 ),
231 .samples = typedSlice(
232 u64,
233 self.bytes,
234 self.capacity.samples_offset,
235 sample_count,
236 ),
237 .counts = typedSlice(
238 u32,
239 self.bytes,
240 self.capacity.counts_offset,
241 sample_count,
242 ),
243 };
244 }
245
246 pub fn acquireF64Samples(self: *Storage, sample_count: usize) AcquireError![]f64 {
247 std.debug.assert(self.phase == .steady);
248 if (self.in_use) return error.ComparisonStorageInUse;
249 if (sample_count == 0) return error.EmptySampleSet;
250 if (sample_count > self.capacity.limits.max_samples_per_series) {
251 return error.SampleCapacityExceeded;
252 }
253 self.in_use = true;
254 return typedSlice(
255 f64,
256 self.bytes,
257 self.capacity.samples_offset,
258 sample_count,
259 );
260 }
261
262 fn beginComparison(
263 self: *Storage,
264 base_samples: usize,
265 candidate_samples: usize,
266 bootstrap_iterations: u32,
267 ) AcquireError!usize {
268 std.debug.assert(self.phase == .steady);
269 if (self.in_use) return error.ComparisonStorageInUse;
270 if (base_samples == 0) return error.EmptyBaseSeries;
271 if (candidate_samples == 0) return error.EmptyCandidateSeries;
272 if (bootstrap_iterations == 0) return error.InvalidBootstrapIterations;
273 if (base_samples > self.capacity.limits.max_samples_per_series) {
274 return error.BaseSampleCapacityExceeded;
275 }
276 if (candidate_samples > self.capacity.limits.max_samples_per_series) {
277 return error.CandidateSampleCapacityExceeded;
278 }
279 if (bootstrap_iterations > self.capacity.limits.max_bootstrap_iterations) {
280 return error.BootstrapIterationCapacityExceeded;
281 }
282 self.in_use = true;
283 return @max(base_samples, candidate_samples);
284 }
285
286 pub fn reset(self: *Storage) void {
287 std.debug.assert(self.phase == .steady);
288 std.debug.assert(self.in_use);
289 self.in_use = false;
290 }
291
292 pub fn status(self: *const Storage) Status {
293 return .{
294 .phase = self.phase,
295 .in_use = self.in_use,
296 .storage_bytes = self.capacity.storage_bytes,
297 .max_samples_per_series = self.capacity.limits.max_samples_per_series,
298 .max_bootstrap_iterations = self.capacity.limits.max_bootstrap_iterations,
299 };
300 }
301
302 pub fn deinit(self: *Storage, allocator: std.mem.Allocator) void {
303 std.debug.assert(self.phase != .teardown);
304 std.debug.assert(!self.in_use);
305 std.debug.assert(self.bytes.len == self.capacity.storage_bytes);
306 self.phase = .teardown;
307 allocator.free(self.bytes);
308 self.bytes = &.{};
309 }
310 };
311
312 fn typedSlice(
313 comptime T: type,
314 bytes: []align(capacity_mod.storage_alignment) u8,
315 offset: usize,
316 count: usize,
317 ) []T {
318 const byte_count = count * @sizeOf(T);
319 const region: []align(@alignOf(T)) u8 = @alignCast(bytes[offset..][0..byte_count]);
320 return std.mem.bytesAsSlice(T, region);
321 }
322
323 fn checkInitFailures(allocator: std.mem.Allocator) !void {
324 var storage = try Storage.init(allocator, .{
325 .max_samples_per_series = 8,
326 .max_bootstrap_iterations = 500,
327 });
328 storage.deinit(allocator);
329 }
330
331 test "comparison storage acquires one exact region" {
332 comptime {
333 @stardustClaim(
334 @import("alloc_phase").capacity.witness(Storage, "bench_comparison_acquisition"),
335 null,
336 null,
337 null,
338 null,
339 null,
340 null,
341 );
342 }
343
344 var counting = std.testing.FailingAllocator.init(std.testing.allocator, .{});
345 const limits = capacity_mod.Limits{
346 .max_samples_per_series = 8,
347 .max_bootstrap_iterations = 500,
348 };
349 const capacity = try capacity_mod.Capacity.derive(limits);
350 var storage = try Storage.init(counting.allocator(), limits);
351 defer storage.deinit(counting.allocator());
352
353 try std.testing.expectEqual(@as(usize, 1), counting.alloc_index);
354 try std.testing.expectEqual(capacity.storage_bytes, counting.allocated_bytes);
355 try std.testing.expectEqual(alloc_phase.capacity.Phase.initialization, storage.status().phase);
356 storage.activate();
357 const f64_regions = try storage.acquireF64(8, 5, 500);
358 const base = @intFromPtr(storage.bytes.ptr);
359 try std.testing.expectEqual(
360 base + capacity.effects_offset,
361 @intFromPtr(f64_regions.effects.ptr),
362 );
363 try std.testing.expectEqual(
364 base + capacity.samples_offset,
365 @intFromPtr(f64_regions.samples.ptr),
366 );
367 try std.testing.expectEqual(
368 base + capacity.counts_offset,
369 @intFromPtr(f64_regions.counts.ptr),
370 );
371 storage.reset();
372 const u64_regions = try storage.acquireU64(5, 8, 500);
373 try std.testing.expectEqual(
374 base + capacity.effects_offset,
375 @intFromPtr(u64_regions.effects.ptr),
376 );
377 try std.testing.expectEqual(
378 base + capacity.samples_offset,
379 @intFromPtr(u64_regions.samples.ptr),
380 );
381 try std.testing.expectEqual(
382 base + capacity.counts_offset,
383 @intFromPtr(u64_regions.counts.ptr),
384 );
385 storage.reset();
386 }
387
388 test "comparison storage retries after every allocation failure" {
389 comptime {
390 @stardustClaim(
391 @import("alloc_phase").capacity.witness(Storage, "bench_comparison_oom"),
392 null,
393 null,
394 null,
395 null,
396 null,
397 null,
398 );
399 }
400
401 try std.testing.checkAllAllocationFailures(std.testing.allocator, checkInitFailures, .{});
402 }
403
404 comptime {
405 alloc_phase.capacity.requireAllocatorRejectingOwnerShape(Storage);
406 }