lib/memtrace/src/census/test.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const alloc_phase = @import("alloc_phase");
3 const namespace_owner = @import("root.zig");
4
5 const Capacity = namespace_owner.Capacity;
6 const Census = namespace_owner.Census;
7 const Limits = namespace_owner.Limits;
8 const Storage = namespace_owner.Storage;
9
10 fn testLimits() Limits {
11 return .{
12 .categories = 3,
13 .label_bytes = 64,
14 .joined_label_bytes = 64,
15 };
16 }
17
18 test "Bounded Census summary and JSONL output preserve aggregate semantics" {
19 comptime {
20 @stardustClaim(
21 @import("alloc_phase").capacity.witness(@import("./root.zig").Storage, "memtrace_census_output"),
22 null,
23 null,
24 null,
25 null,
26 null,
27 null,
28 );
29 }
30
31 var counting = std.testing.FailingAllocator.init(std.testing.allocator, .{});
32 const capacity = try Capacity.derive(testLimits());
33 var storage = try Storage.init(counting.allocator(), testLimits());
34 defer storage.deinit(counting.allocator());
35 storage.activate();
36 var census = try Census.init(&storage);
37 defer census.deinit();
38
39 try census.record("runtime.dictionary", 40);
40 try census.record("runtime.dictionary", 2);
41 try census.recordCount("runtime.string", 3, 9);
42 try census.recordPrefixed("global.page.", "runtime.record", 4);
43
44 var summary_bytes: [1024]u8 = undefined;
45 var summary = std.Io.Writer.fixed(&summary_bytes);
46 try census.writeSummary(&summary, .{ .top = 8 });
47 const text = summary.buffered();
48 const expected_summary =
49 "memtrace census categories=3\n" ++
50 "runtime.dictionary items=2 bytes=42\n" ++
51 "runtime.string items=3 bytes=9\n" ++
52 "global.page.runtime.record items=1 bytes=4\n";
53 try std.testing.expectEqualStrings(expected_summary, text);
54
55 var jsonl_bytes: [1024]u8 = undefined;
56 var jsonl = std.Io.Writer.fixed(&jsonl_bytes);
57 try census.writeJsonl(&jsonl);
58 const expected_jsonl =
59 "{\"v\":3,\"seq\":1,\"kind\":\"census\"," ++
60 "\"label\":\"global.page.runtime.record\",\"items\":1,\"bytes\":4}\n" ++
61 "{\"v\":3,\"seq\":2,\"kind\":\"census\"," ++
62 "\"label\":\"runtime.string\",\"items\":3,\"bytes\":9}\n" ++
63 "{\"v\":3,\"seq\":3,\"kind\":\"census\"," ++
64 "\"label\":\"runtime.dictionary\",\"items\":2,\"bytes\":42}\n";
65 try std.testing.expectEqualStrings(expected_jsonl, jsonl.buffered());
66 try std.testing.expectEqual(@as(usize, 3), census.status().categories);
67 try std.testing.expectEqual(@as(usize, 1), counting.alloc_index);
68 try std.testing.expectEqual(capacity.storage_bytes, counting.allocated_bytes);
69 }
70
71 test "Census rejects each storage dimension at max plus one before mutation" {
72 comptime {
73 @stardustClaim(
74 @import("alloc_phase").capacity.witness(@import("./root.zig").Storage, "memtrace_census_boundary"),
75 null,
76 null,
77 null,
78 null,
79 null,
80 null,
81 );
82 }
83
84 var storage = try Storage.init(std.testing.allocator, .{
85 .categories = 2,
86 .label_bytes = 5,
87 .joined_label_bytes = 3,
88 });
89 defer storage.deinit(std.testing.allocator);
90 storage.activate();
91 var census = try Census.init(&storage);
92 defer census.deinit();
93
94 try census.record("ab", 1);
95 try census.recordPrefixed("a", "bc", 2);
96 const first = census.status();
97 try std.testing.expectError(
98 error.JoinedLabelCapacityExceeded,
99 census.recordPrefixed("abc", "d", 1),
100 );
101 try std.testing.expectEqual(first, census.status());
102
103 const full = census.status();
104 var full_bytes: [256]u8 = undefined;
105 var full_summary = std.Io.Writer.fixed(&full_bytes);
106 try census.writeSummary(&full_summary, .{});
107 try std.testing.expectError(error.CategoryCapacityExceeded, census.record("f", 3));
108 try std.testing.expectEqual(full, census.status());
109 var rejected_bytes: [256]u8 = undefined;
110 var rejected_summary = std.Io.Writer.fixed(&rejected_bytes);
111 try census.writeSummary(&rejected_summary, .{});
112 try std.testing.expectEqualSlices(u8, full_summary.buffered(), rejected_summary.buffered());
113
114 var label_storage = try Storage.init(std.testing.allocator, .{
115 .categories = 2,
116 .label_bytes = 2,
117 .joined_label_bytes = 2,
118 });
119 defer label_storage.deinit(std.testing.allocator);
120 label_storage.activate();
121 var label_census = try Census.init(&label_storage);
122 defer label_census.deinit();
123 try label_census.record("ab", 1);
124 const label_full = label_census.status();
125 var label_full_bytes: [128]u8 = undefined;
126 var label_full_summary = std.Io.Writer.fixed(&label_full_bytes);
127 try label_census.writeSummary(&label_full_summary, .{});
128 try std.testing.expectError(error.LabelBytesCapacityExceeded, label_census.record("c", 1));
129 try std.testing.expectEqual(label_full, label_census.status());
130 var label_rejected_bytes: [128]u8 = undefined;
131 var label_rejected_summary = std.Io.Writer.fixed(&label_rejected_bytes);
132 try label_census.writeSummary(&label_rejected_summary, .{});
133 try std.testing.expectEqualSlices(
134 u8,
135 label_full_summary.buffered(),
136 label_rejected_summary.buffered(),
137 );
138 }
139
140 test "Census rejects aggregate counter overflow before mutation" {
141 comptime {
142 @stardustClaim(
143 @import("alloc_phase").capacity.witness(@import("./root.zig").Storage, "memtrace_census_overflow"),
144 null,
145 null,
146 null,
147 null,
148 null,
149 null,
150 );
151 }
152
153 var storage = try Storage.init(std.testing.allocator, .{
154 .categories = 1,
155 .label_bytes = 1,
156 .joined_label_bytes = 0,
157 });
158 defer storage.deinit(std.testing.allocator);
159 storage.activate();
160 var census = try Census.init(&storage);
161 defer census.deinit();
162
163 try census.recordCount("x", std.math.maxInt(usize), std.math.maxInt(usize));
164 const before = census.status();
165 var before_bytes: [128]u8 = undefined;
166 var before_summary = std.Io.Writer.fixed(&before_bytes);
167 try census.writeSummary(&before_summary, .{});
168 try std.testing.expectError(error.CensusCounterOverflow, census.record("x", 0));
169 try std.testing.expectEqual(before, census.status());
170 var after_bytes: [128]u8 = undefined;
171 var after_summary = std.Io.Writer.fixed(&after_bytes);
172 try census.writeSummary(&after_summary, .{});
173 try std.testing.expectEqualSlices(u8, before_summary.buffered(), after_summary.buffered());
174 }
175
176 test "Census open addressing fills every admitted category" {
177 const category_capacity = 32;
178 var storage = try Storage.init(std.testing.allocator, .{
179 .categories = category_capacity,
180 .label_bytes = category_capacity * 4,
181 .joined_label_bytes = 0,
182 });
183 defer storage.deinit(std.testing.allocator);
184 storage.activate();
185 var census = try Census.init(&storage);
186 defer census.deinit();
187
188 for (0..category_capacity) |index| {
189 var label_bytes: [4]u8 = undefined;
190 const label = try std.fmt.bufPrint(&label_bytes, "x{d:0>2}", .{index});
191 try census.record(label, index);
192 }
193 for (0..category_capacity) |index| {
194 var label_bytes: [4]u8 = undefined;
195 const label = try std.fmt.bufPrint(&label_bytes, "x{d:0>2}", .{index});
196 try census.record(label, 1);
197 }
198 try std.testing.expectEqual(@as(usize, category_capacity), census.status().categories);
199 try std.testing.expectEqual(
200 @as(usize, category_capacity * 3),
201 census.status().label_bytes_used,
202 );
203 }
204
205 test "Census storage rejects concurrent acquisition and resets for reuse" {
206 comptime {
207 @stardustClaim(
208 @import("alloc_phase").capacity.witness(@import("./root.zig").Storage, "memtrace_census_reuse"),
209 null,
210 null,
211 null,
212 null,
213 null,
214 null,
215 );
216 }
217
218 var storage = try Storage.init(std.testing.allocator, testLimits());
219 defer storage.deinit(std.testing.allocator);
220 storage.activate();
221 var first = try Census.init(&storage);
222 try first.record("old", 7);
223 try std.testing.expectError(error.CensusStorageInUse, Census.init(&storage));
224 first.deinit();
225
226 var reused = try Census.init(&storage);
227 defer reused.deinit();
228 try std.testing.expectEqual(@as(usize, 0), reused.status().categories);
229 try reused.record("new", 11);
230 try std.testing.expectEqual(@as(usize, 1), reused.status().categories);
231 }
232
233 test "Maximum Census recording and output make no backing allocation after sealing" {
234 comptime {
235 @stardustClaim(
236 @import("alloc_phase").capacity.witness(@import("./root.zig").Storage, "memtrace_census_sealed"),
237 null,
238 null,
239 null,
240 null,
241 null,
242 null,
243 );
244 }
245
246 var sealed = try alloc_phase.SealedPhaseAllocator.init(std.testing.allocator);
247 var storage = try Storage.init(sealed.initializationAllocator(), .{
248 .categories = 3,
249 .label_bytes = 6,
250 .joined_label_bytes = 3,
251 });
252 storage.activate();
253 var census = try Census.init(&storage);
254 sealed.seal();
255
256 try census.record("aa", 1);
257 try census.recordCount("bb", 2, 3);
258 try census.recordCountPrefixed("c", "c", 4, 5);
259 try census.record("aa", 7);
260 var summary_bytes: [512]u8 = undefined;
261 var summary = std.Io.Writer.fixed(&summary_bytes);
262 try census.writeSummary(&summary, .{});
263 var jsonl_bytes: [512]u8 = undefined;
264 var jsonl = std.Io.Writer.fixed(&jsonl_bytes);
265 try census.writeJsonl(&jsonl);
266 try std.testing.expectEqual(@as(usize, 3), census.status().categories);
267 try std.testing.expect(summary.buffered().len > 0);
268 try std.testing.expect(jsonl.buffered().len > 0);
269
270 census.deinit();
271 sealed.beginTeardown();
272 storage.deinit(sealed.teardownAllocator());
273 sealed.deinit();
274 }