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 }