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 }