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 }