lib/memtrace/src/census/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 Exhaustion = error{CensusStorageInUse};
  7 
  8 pub const Regions = struct {
  9     slots: []model.Slot,
 10     summary: []model.Entry,
 11     labels: []u8,
 12     joined_label: []u8,
 13 };
 14 
 15 pub const Status = struct {
 16     phase: alloc_phase.capacity.Phase,
 17     in_use: bool,
 18     storage_bytes: usize,
 19     category_capacity: usize,
 20     table_slots: usize,
 21     label_bytes_capacity: usize,
 22     joined_label_bytes_capacity: usize,
 23 };
 24 
 25 pub const Storage = struct {
 26     phase: alloc_phase.capacity.Phase,
 27     capacity: capacity_mod.Capacity,
 28     bytes: []align(capacity_mod.storage_alignment) u8,
 29     slots: []model.Slot,
 30     summary: []model.Entry,
 31     labels: []u8,
 32     joined_label: []u8,
 33     in_use: bool = false,
 34 
 35     pub const Limits: type = capacity_mod.Limits;
 36     pub const Capacity: type = capacity_mod.Capacity;
 37     pub const Exhaustion: type = @import("storage.zig").Exhaustion;
 38     pub const InitError = std.mem.Allocator.Error || capacity_mod.DeriveError;
 39 
 40     pub const claim: alloc_phase.capacity.Declaration = .{
 41         .source = .{
 42             .id = "memtrace.census_storage",
 43             .kind = .phase_static,
 44             .limit_source = .caller,
 45             .storage = .{
 46                 .covered = &.{
 47                     .{
 48                         .id = "bounded_census_category_hash_table",
 49                         .lifetime = .steady,
 50                         .detail = "bounded census category hash table",
 51                     },
 52                     .{
 53                         .id = "retained_category_label_bytes",
 54                         .lifetime = .steady,
 55                         .detail = "retained category label bytes",
 56                     },
 57                     .{
 58                         .id = "prefixed_label_join_scratch",
 59                         .lifetime = .steady,
 60                         .detail = "prefixed-label join scratch",
 61                     },
 62                     .{
 63                         .id = "sorted_summary_entry_scratch",
 64                         .lifetime = .steady,
 65                         .detail = "sorted summary entry scratch",
 66                     },
 67                 },
 68                 .excluded = &.{
 69                     "the caller-owned output writer and its storage",
 70                     "allocation events and tracer state outside the semantic census",
 71                 },
 72             },
 73             .capacity = .{
 74                 .inputs = &.{
 75                     alloc_phase.capacity.bindInput(Limits, "categories", "categories"),
 76                     alloc_phase.capacity.bindInput(Limits, "label_bytes", "label_bytes"),
 77                     alloc_phase.capacity.bindInput(Limits, "joined_label_bytes", "joined_label_bytes"),
 78                 },
 79                 .type_selectors = &.{
 80                     alloc_phase.capacity.bindType(model.Slot, "slot"),
 81                     alloc_phase.capacity.bindType(model.Entry, "entry"),
 82                 },
 83                 .nodes = &.{
 84                     .{ .input = 0 },
 85                     .{ .scale = .{ .node = 0, .coefficient = .{ .literal = 2 } } },
 86                     .{ .next_power_of_two = 1 },
 87                     .{ .scale = .{ .node = 2, .coefficient = .{ .size_of_concrete_type = 0 } } },
 88                     .{ .alignment = .{ .node = 3, .alignment = .{ .literal = 16 } } },
 89                     .{ .scale = .{ .node = 0, .coefficient = .{ .size_of_concrete_type = 1 } } },
 90                     .{ .alignment = .{ .node = 5, .alignment = .{ .literal = 16 } } },
 91                     .{ .input = 1 },
 92                     .{ .input = 2 },
 93                     .{ .add = .{ .left = 4, .right = 6 } },
 94                     .{ .add = .{ .left = 9, .right = 7 } },
 95                     .{ .add = .{ .left = 10, .right = 8 } },
 96                 },
 97                 .assertions = &.{.{
 98                     .scope = .closure_total,
 99                     .measure = .retained,
100                     .relation = .exact,
101                     .expression = 11,
102                 }},
103             },
104             .overload = .{
105                 .kind = .reject_before_mutation,
106                 .detail = "named category, retained-label, joined-label, and counter exhaustion leaves aggregate entries unchanged",
107             },
108             .risks = .{
109                 .transitive = .{
110                     .status = .witnessed,
111                     .detail = "recording, prefix joins, summary sorting, and JSONL emission use only the acquired census regions after activation",
112                 },
113                 .foreign = .{
114                     .status = .excluded,
115                     .detail = "writer effects and writer-owned buffering remain outside Census storage",
116                 },
117             },
118             .obligations = &.{
119                 .{ .key = "memtrace_census_capacity", .role = .capacity_model },
120                 .{ .key = "memtrace_census_acquisition", .role = .custom },
121                 .{ .key = "memtrace_census_oom", .role = .custom },
122                 .{ .key = "memtrace_census_boundary", .role = .overload },
123                 .{ .key = "memtrace_census_overflow", .role = .overload },
124                 .{ .key = "memtrace_census_reuse", .role = .overload },
125                 .{ .key = "memtrace_census_sealed", .role = .transitive_risk },
126                 .{ .key = "memtrace_census_output", .role = .foreign_risk },
127                 .{ .key = "memtrace_census_root", .role = .custom },
128             },
129         },
130         .bindings = .{
131             .owner = @This(),
132             .seal = .{
133                 .family = alloc_phase.capacity.selector(@This().activate),
134                 .premise = .{
135                     .class = .checked_semantic_fact,
136                     .authority = .checker,
137                 },
138             },
139             .teardown = .{
140                 .family = alloc_phase.capacity.selector(@This().deinit),
141                 .premise = .{
142                     .class = .checked_semantic_fact,
143                     .authority = .checker,
144                 },
145             },
146         },
147     };
148 
149     pub fn init(allocator: std.mem.Allocator, limits: Limits) InitError!Storage {
150         const capacity = try Capacity.derive(limits);
151         const bytes = try allocator.alignedAlloc(
152             u8,
153             .fromByteUnits(capacity_mod.storage_alignment),
154             capacity.storage_bytes,
155         );
156         return .{
157             .phase = .initialization,
158             .capacity = capacity,
159             .bytes = bytes,
160             .slots = typedSlice(model.Slot, bytes, capacity.table_offset, capacity.table_slots),
161             .summary = typedSlice(model.Entry, bytes, capacity.summary_offset, capacity.summary_entries),
162             .labels = bytes[capacity.label_offset..][0..capacity.label_bytes],
163             .joined_label = bytes[capacity.joined_label_offset..][0..capacity.joined_label_bytes],
164         };
165     }
166 
167     pub fn activate(self: *Storage) void {
168         std.debug.assert(self.phase == .initialization);
169         std.debug.assert(self.bytes.len == self.capacity.storage_bytes);
170         self.phase = .steady;
171     }
172 
173     pub fn acquire(self: *Storage) Storage.Exhaustion!Regions {
174         std.debug.assert(self.phase == .steady);
175         if (self.in_use) return error.CensusStorageInUse;
176         @memset(self.slots, .{});
177         self.in_use = true;
178         return .{
179             .slots = self.slots,
180             .summary = self.summary,
181             .labels = self.labels,
182             .joined_label = self.joined_label,
183         };
184     }
185 
186     pub fn release(self: *Storage) void {
187         std.debug.assert(self.phase == .steady);
188         std.debug.assert(self.in_use);
189         self.in_use = false;
190     }
191 
192     pub fn status(self: *const Storage) Status {
193         return .{
194             .phase = self.phase,
195             .in_use = self.in_use,
196             .storage_bytes = self.capacity.storage_bytes,
197             .category_capacity = self.capacity.categories,
198             .table_slots = self.capacity.table_slots,
199             .label_bytes_capacity = self.capacity.label_bytes,
200             .joined_label_bytes_capacity = self.capacity.joined_label_bytes,
201         };
202     }
203 
204     pub fn deinit(self: *Storage, allocator: std.mem.Allocator) void {
205         std.debug.assert(self.phase != .teardown);
206         std.debug.assert(!self.in_use);
207         std.debug.assert(self.bytes.len == self.capacity.storage_bytes);
208         self.phase = .teardown;
209         allocator.free(self.bytes);
210         self.bytes = &.{};
211         self.slots = &.{};
212         self.summary = &.{};
213         self.labels = &.{};
214         self.joined_label = &.{};
215     }
216 };
217 
218 fn typedSlice(
219     comptime T: type,
220     bytes: []align(capacity_mod.storage_alignment) u8,
221     offset: usize,
222     count: usize,
223 ) []T {
224     const byte_count = count * @sizeOf(T);
225     const region: []align(@alignOf(T)) u8 = @alignCast(bytes[offset..][0..byte_count]);
226     return std.mem.bytesAsSlice(T, region);
227 }
228 
229 fn checkInitFailures(allocator: std.mem.Allocator) !void {
230     var storage = try Storage.init(allocator, .{
231         .categories = 3,
232         .label_bytes = 64,
233         .joined_label_bytes = 31,
234     });
235     storage.deinit(allocator);
236 }
237 
238 test "Census storage acquires one exact aligned region" {
239     comptime {
240         @stardustClaim(
241             @import("alloc_phase").capacity.witness(Storage, "memtrace_census_acquisition"),
242             null,
243             null,
244             null,
245             null,
246             null,
247             null,
248         );
249     }
250 
251     var counting = std.testing.FailingAllocator.init(std.testing.allocator, .{});
252     const capacity = try capacity_mod.Capacity.derive(.{
253         .categories = 3,
254         .label_bytes = 64,
255         .joined_label_bytes = 31,
256     });
257     var storage = try Storage.init(counting.allocator(), .{
258         .categories = 3,
259         .label_bytes = 64,
260         .joined_label_bytes = 31,
261     });
262     defer storage.deinit(counting.allocator());
263 
264     try std.testing.expectEqual(@as(usize, 1), counting.alloc_index);
265     try std.testing.expectEqual(capacity.storage_bytes, counting.allocated_bytes);
266     try std.testing.expectEqual(capacity.storage_bytes, storage.status().storage_bytes);
267     try std.testing.expectEqual(alloc_phase.capacity.Phase.initialization, storage.status().phase);
268 
269     storage.activate();
270     const regions = try storage.acquire();
271     defer storage.release();
272     const base = @intFromPtr(storage.bytes.ptr);
273     try std.testing.expectEqual(base + capacity.table_offset, @intFromPtr(regions.slots.ptr));
274     try std.testing.expectEqual(base + capacity.summary_offset, @intFromPtr(regions.summary.ptr));
275     try std.testing.expectEqual(base + capacity.label_offset, @intFromPtr(regions.labels.ptr));
276     try std.testing.expectEqual(base + capacity.joined_label_offset, @intFromPtr(regions.joined_label.ptr));
277 }
278 
279 test "Census storage retries after every allocation failure" {
280     comptime {
281         @stardustClaim(
282             @import("alloc_phase").capacity.witness(Storage, "memtrace_census_oom"),
283             null,
284             null,
285             null,
286             null,
287             null,
288             null,
289         );
290     }
291 
292     try std.testing.checkAllAllocationFailures(std.testing.allocator, checkInitFailures, .{});
293 }
294 
295 comptime {
296     alloc_phase.capacity.requireAllocatorRejectingOwnerShape(Storage);
297 }