lib/memtrace/src/analysis.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const observe = @import("alloc_observe");
3 const pretty_json = @import("pretty").json;
4 const sys = @import("sys");
5 const allocations = @import("allocations.zig");
6 const coverage_mod = @import("coverage.zig");
7 const event_mod = @import("event.zig");
8 const mappings = @import("mappings.zig");
9 const stack_mod = @import("stack/root.zig");
10 const tracer_mod = @import("tracer.zig");
11
12 const Allocator = std.mem.Allocator;
13
14 pub const Sort = enum {
15 retained,
16 traffic,
17 lifetime,
18
19 pub fn parse(value: []const u8) ?Sort {
20 if (std.mem.eql(u8, value, "retained")) return .retained;
21 if (std.mem.eql(u8, value, "traffic")) return .traffic;
22 if (std.mem.eql(u8, value, "lifetime")) return .lifetime;
23 return null;
24 }
25
26 pub fn name(self: Sort) []const u8 {
27 return @tagName(self);
28 }
29 };
30
31 pub const SummaryOptions = struct {
32 top: usize = 24,
33 min_bytes: usize = 0,
34 include_zero_live: bool = false,
35 include_sites: bool = false,
36 site_detail_return_address: ?u64 = null,
37 site_symbol_binary: ?[]const u8 = null,
38 layer: event_mod.LayerFilter = .backing,
39 sort: Sort = .retained,
40 };
41
42 pub const SnapshotOptions = struct {
43 top: usize = 24,
44 min_bytes: usize = 0,
45 include_zero_live: bool = false,
46 site_symbol_binary: ?[]const u8 = null,
47 layer: event_mod.LayerFilter = .backing,
48 };
49
50 pub const Summary = struct {
51 events: u64 = 0,
52 allocations: u64 = 0,
53 frees: u64 = 0,
54 resizes: u64 = 0,
55 remaps: u64 = 0,
56 live_allocations: u64 = 0,
57 allocated_bytes: u64 = 0,
58 freed_bytes: u64 = 0,
59 live_bytes: u64 = 0,
60 high_water_live_bytes: u64 = 0,
61 retained_bytes: u64 = 0,
62 high_water_retained_bytes: u64 = 0,
63 completed_lifetimes: u64 = 0,
64 lifetime_total_events: u64 = 0,
65 lifetime_mean_events: u64 = 0,
66 lifetime_max_events: u64 = 0,
67 lifetime_total_byte_events: u64 = 0,
68 lifetime_mean_byte_events: u64 = 0,
69 lifetime_max_byte_events: u64 = 0,
70 failed_allocations: u64 = 0,
71 failed_resizes: u64 = 0,
72 failed_remaps: u64 = 0,
73 unmatched_frees: u64 = 0,
74 unmatched_resizes: u64 = 0,
75 };
76
77 pub const Scope = struct {
78 scope: []const u8,
79 retained_bytes: u64,
80 high_water_retained_bytes: u64,
81 live_bytes: u64,
82 high_water_live_bytes: u64,
83 allocated_bytes: u64,
84 freed_bytes: u64,
85 allocations: u64,
86 frees: u64,
87 live_allocations: u64,
88 completed_lifetimes: u64,
89 lifetime_total_events: u64,
90 lifetime_mean_events: u64,
91 lifetime_max_events: u64,
92 lifetime_total_byte_events: u64 = 0,
93 lifetime_mean_byte_events: u64 = 0,
94 lifetime_max_byte_events: u64 = 0,
95 };
96
97 pub const Source = struct {
98 return_address: u64,
99 function: ?[]const u8,
100 location: ?[]const u8,
101 retained_bytes: u64,
102 high_water_retained_bytes: u64,
103 live_bytes: u64,
104 high_water_live_bytes: u64,
105 allocated_bytes: u64,
106 freed_bytes: u64,
107 allocations: u64,
108 frees: u64,
109 live_allocations: u64,
110 completed_lifetimes: u64,
111 lifetime_total_events: u64,
112 lifetime_mean_events: u64,
113 lifetime_max_events: u64,
114 lifetime_total_byte_events: u64 = 0,
115 lifetime_mean_byte_events: u64 = 0,
116 lifetime_max_byte_events: u64 = 0,
117 };
118
119 pub const Ranking = struct {
120 scopes: []Scope = &.{},
121 sources: []Source = &.{},
122 };
123
124 pub const Rankings = struct {
125 retained: Ranking = .{},
126 traffic: Ranking = .{},
127 lifetime: Ranking = .{},
128
129 pub fn get(self: Rankings, sort: Sort) Ranking {
130 return switch (sort) {
131 .retained => self.retained,
132 .traffic => self.traffic,
133 .lifetime => self.lifetime,
134 };
135 }
136
137 fn getPtr(self: *Rankings, sort: Sort) *Ranking {
138 return switch (sort) {
139 .retained => &self.retained,
140 .traffic => &self.traffic,
141 .lifetime => &self.lifetime,
142 };
143 }
144 };
145
146 pub const Snapshot = struct {
147 allocator: Allocator,
148 integrity: CaptureIntegrity,
149 summary: Summary,
150 rankings: Rankings = .{},
151 symbol_storage: ?[]u8 = null,
152
153 pub fn deinit(self: *Snapshot) void {
154 inline for (std.meta.tags(Sort)) |sort| {
155 const ranking = self.rankings.getPtr(sort);
156 for (ranking.scopes) |scope| self.allocator.free(scope.scope);
157 if (ranking.scopes.len != 0) self.allocator.free(ranking.scopes);
158 if (ranking.sources.len != 0) self.allocator.free(ranking.sources);
159 }
160 if (self.symbol_storage) |storage| self.allocator.free(storage);
161 self.* = undefined;
162 }
163 };
164
165 const Retention = enum {
166 releases_freed_memory,
167 retains_freed_memory,
168 };
169
170 const Counters = struct {
171 allocations: u64 = 0,
172 frees: u64 = 0,
173 resizes: u64 = 0,
174 remaps: u64 = 0,
175 failed_allocations: u64 = 0,
176 failed_resizes: u64 = 0,
177 failed_remaps: u64 = 0,
178 unmatched_frees: u64 = 0,
179 unmatched_resizes: u64 = 0,
180 unmatched_remaps: u64 = 0,
181 bulk_invalidated_requests: u64 = 0,
182 bulk_invalidated_bytes: usize = 0,
183 untracked_requests: u64 = 0,
184 untracked_request_bytes: usize = 0,
185 lifecycle_events: u64 = 0,
186 live_allocations: usize = 0,
187 allocated_bytes: usize = 0,
188 freed_bytes: usize = 0,
189 live_bytes: usize = 0,
190 high_water_live_bytes: usize = 0,
191 retained_bytes: usize = 0,
192 high_water_retained_bytes: usize = 0,
193 completed_lifetimes: u64 = 0,
194 lifetime_total_events: u64 = 0,
195 lifetime_max_events: u64 = 0,
196 lifetime_total_byte_events: u64 = 0,
197 lifetime_max_byte_events: u64 = 0,
198 };
199
200 const PhysicalCounters = struct {
201 maps: u64 = 0,
202 unmaps: u64 = 0,
203 protects: u64 = 0,
204 discards: u64 = 0,
205 decommits: u64 = 0,
206 advises: u64 = 0,
207 failed_maps: u64 = 0,
208 failed_unmaps: u64 = 0,
209 failed_protects: u64 = 0,
210 failed_discards: u64 = 0,
211 failed_decommits: u64 = 0,
212 failed_advises: u64 = 0,
213 mapped_bytes: usize = 0,
214 unmapped_bytes: usize = 0,
215 displaced_bytes: usize = 0,
216 untracked_unmap_bytes: usize = 0,
217 live_mapped_bytes: usize = 0,
218 high_water_mapped_bytes: usize = 0,
219 };
220
221 const IntegrityCounters = struct {
222 sequenced_events: u64 = 0,
223 unsequenced_events: u64 = 0,
224 sequence_gaps: u64 = 0,
225 missing_sequence_events: u64 = 0,
226 sequence_regressions: u64 = 0,
227 start_events: u64 = 0,
228 stop_events: u64 = 0,
229 unmatched_scope_exits: u64 = 0,
230 active_scope_events: u64 = 0,
231 };
232
233 pub const capture_integrity_method = "memtrace_event_sequence_and_lifecycle_v1";
234
235 pub const CaptureIntegrity = struct {
236 status: []const u8,
237 action: []const u8,
238 message: ?[]const u8,
239 event_count: u64,
240 sequenced_event_count: u64,
241 unsequenced_event_count: u64,
242 first_sequence: ?u64,
243 last_sequence: ?u64,
244 sequence_gap_count: u64,
245 missing_sequence_event_count: u64,
246 sequence_regression_count: u64,
247 start_event_count: u64,
248 stop_event_count: u64,
249 start_sequence: ?u64,
250 stop_sequence: ?u64,
251 unbalanced_event_count: u64,
252 };
253
254 const ScopeCounters = struct {
255 allocations: u64 = 0,
256 frees: u64 = 0,
257 allocated_bytes: usize = 0,
258 freed_bytes: usize = 0,
259 bulk_invalidated_requests: u64 = 0,
260 bulk_invalidated_bytes: usize = 0,
261 untracked_requests: u64 = 0,
262 untracked_request_bytes: usize = 0,
263 live_allocations: usize = 0,
264 live_bytes: usize = 0,
265 high_water_live_bytes: usize = 0,
266 retained_bytes: usize = 0,
267 high_water_retained_bytes: usize = 0,
268 completed_lifetimes: u64 = 0,
269 lifetime_total_events: u64 = 0,
270 lifetime_max_events: u64 = 0,
271 lifetime_total_byte_events: u64 = 0,
272 lifetime_max_byte_events: u64 = 0,
273 };
274
275 const AllocatorState = struct {
276 retention: Retention = .releases_freed_memory,
277 layer: event_mod.Layer = .backing_boundary,
278 lifecycle_instrumented: bool = false,
279 prefix_complete: bool = true,
280 };
281
282 const LifecycleCoverage = struct {
283 instrumented: u64 = 0,
284 uninstrumented: u64 = 0,
285 not_required: u64 = 0,
286 prefix_complete: u64 = 0,
287 prefix_partial: u64 = 0,
288
289 fn status(self: LifecycleCoverage) []const u8 {
290 if (self.instrumented == 0 and
291 self.uninstrumented == 0 and
292 self.not_required == 0)
293 {
294 return "none";
295 }
296 if (self.uninstrumented == 0 and self.prefix_partial == 0) {
297 return "complete";
298 }
299 return "partial";
300 }
301 };
302
303 const AllocationRecord = struct {
304 allocator_id: u32,
305 len: usize,
306 scope: []const u8,
307 return_address: u64,
308 allocation_event: u64,
309 segment_event: u64,
310 byte_events: u64,
311 };
312
313 const ScopeSummary = struct {
314 path: []const u8,
315 counters: ScopeCounters,
316 };
317
318 const ParsedEvent = event_mod.ReplayEvent;
319
320 const SiteCounters = struct {
321 allocations: u64 = 0,
322 frees: u64 = 0,
323 allocated_bytes: usize = 0,
324 freed_bytes: usize = 0,
325 bulk_invalidated_requests: u64 = 0,
326 bulk_invalidated_bytes: usize = 0,
327 untracked_requests: u64 = 0,
328 untracked_request_bytes: usize = 0,
329 live_allocations: usize = 0,
330 live_bytes: usize = 0,
331 high_water_live_bytes: usize = 0,
332 retained_bytes: usize = 0,
333 high_water_retained_bytes: usize = 0,
334 completed_lifetimes: u64 = 0,
335 lifetime_total_events: u64 = 0,
336 lifetime_max_events: u64 = 0,
337 lifetime_total_byte_events: u64 = 0,
338 lifetime_max_byte_events: u64 = 0,
339 };
340
341 const SiteSummary = struct {
342 return_address: u64,
343 counters: SiteCounters,
344 };
345
346 const SiteSizeSummary = struct {
347 len: usize,
348 counters: SiteCounters,
349 };
350
351 const SiteScopeSummary = struct {
352 scope: []const u8,
353 counters: SiteCounters,
354 };
355
356 pub const Analyzer = struct {
357 allocator: Allocator,
358 allocators: std.AutoHashMapUnmanaged(u32, AllocatorState) = .{},
359 allocations: allocations.Map(AllocationRecord) = .{},
360 scopes: std.StringHashMapUnmanaged(ScopeCounters) = .{},
361 sites: std.AutoHashMapUnmanaged(u64, SiteCounters) = .{},
362 site_detail_sizes: std.AutoHashMapUnmanaged(usize, SiteCounters) = .{},
363 site_detail_scopes: std.StringHashMapUnmanaged(SiteCounters) = .{},
364 site_detail_return_address: ?u64 = null,
365 active_scopes: std.AutoHashMapUnmanaged(u32, u64) = .{},
366 scope_paths: std.AutoHashMapUnmanaged(u32, []const u8) = .{},
367 track_sites: bool = true,
368 layer_filter: event_mod.LayerFilter = .backing,
369 counters: Counters = .{},
370 physical: PhysicalCounters = .{},
371 mappings: mappings.Ledger = .{},
372 integrity_counters: IntegrityCounters = .{},
373 events: u64 = 0,
374 first_sequence: ?u64 = null,
375 last_sequence: ?u64 = null,
376 start_sequence: ?u64 = null,
377 stop_sequence: ?u64 = null,
378
379 pub fn init(allocator: Allocator) Analyzer {
380 return .{ .allocator = allocator };
381 }
382
383 pub fn initForLayer(
384 allocator: Allocator,
385 layer_filter: event_mod.LayerFilter,
386 ) Analyzer {
387 return .{
388 .allocator = allocator,
389 .layer_filter = layer_filter,
390 };
391 }
392
393 pub fn deinit(self: *Analyzer) void {
394 var iterator = self.scopes.iterator();
395 while (iterator.next()) |entry| self.allocator.free(entry.key_ptr.*);
396 self.scopes.deinit(self.allocator);
397 self.sites.deinit(self.allocator);
398 self.site_detail_sizes.deinit(self.allocator);
399 self.site_detail_scopes.deinit(self.allocator);
400 self.active_scopes.deinit(self.allocator);
401 self.scope_paths.deinit(self.allocator);
402 self.allocators.deinit(self.allocator);
403 self.allocations.deinit(self.allocator);
404 self.mappings.deinit(self.allocator);
405 self.* = undefined;
406 }
407
408 pub fn ingestJsonlBytes(self: *Analyzer, bytes: []const u8) !void {
409 var lines = std.mem.splitScalar(u8, bytes, '\n');
410 while (lines.next()) |line| try self.ingestJsonLine(line);
411 }
412
413 pub fn ingestJsonLine(self: *Analyzer, line: []const u8) !void {
414 const text = std.mem.trim(u8, line, " \t\r\n");
415 if (text.len == 0) return;
416 if (stack_mod.isMetadataLine(text) or coverage_mod.isMetadataLine(text)) return;
417
418 if (event_mod.parseReplayFast(text)) |parsed| {
419 try self.ingestParsedEvent(parsed);
420 return;
421 } else |err| switch (err) {
422 error.UnsupportedFastEvent => {},
423 else => return err,
424 }
425
426 try self.ingestGenericJsonLine(text);
427 }
428
429 fn ingestParsedEvent(self: *Analyzer, parsed: ParsedEvent) !void {
430 const scope = try self.resolveScope(parsed);
431 const key = switch (parsed.kind) {
432 .remap => allocationKey(parsed.allocation_id, parsed.old_address),
433 else => allocationKey(parsed.allocation_id, parsed.address),
434 };
435
436 self.events +|= 1;
437 self.recordSequence(parsed.seq);
438 switch (parsed.kind) {
439 .trace_start => {
440 self.integrity_counters.start_events +|= 1;
441 if (self.start_sequence == null) self.start_sequence = parsed.seq;
442 },
443 .trace_stop => {
444 self.integrity_counters.stop_events +|= 1;
445 self.stop_sequence = parsed.seq;
446 },
447 .allocator => if (self.layer_filter.includes(parsed.layer)) {
448 try self.recordAllocator(
449 parsed.allocator_id,
450 parsed.retains_freed_memory,
451 parsed.layer,
452 parsed.lifecycle_instrumented,
453 parsed.observation_prefix_complete,
454 );
455 },
456 .scope_enter => try self.recordScopeEnter(parsed.scope_id, scope),
457 .scope_exit => self.recordScopeExit(parsed.scope_id),
458 .alloc => if (self.layer_filter.includes(parsed.layer)) {
459 if (parsed.succeeded) {
460 if (parsed.tracked) {
461 try self.recordAllocation(
462 parsed.allocator_id,
463 key,
464 scope,
465 parsed.return_address,
466 parsed.len,
467 );
468 } else {
469 try self.recordUntrackedAllocation(
470 scope,
471 parsed.return_address,
472 parsed.len,
473 );
474 }
475 } else {
476 self.counters.failed_allocations +|= 1;
477 }
478 },
479 .free => if (self.layer_filter.includes(parsed.layer)) {
480 if (parsed.tracked) {
481 try self.recordFree(
482 key,
483 parsed.allocator_id,
484 scope,
485 parsed.len,
486 );
487 } else {
488 self.counters.unmatched_frees +|= 1;
489 }
490 },
491 .release => if (self.layer_filter.includes(parsed.layer)) {
492 try self.recordRelease(key);
493 },
494 .resize => if (self.layer_filter.includes(parsed.layer)) {
495 if (parsed.succeeded) {
496 if (parsed.tracked) {
497 try self.recordResize(
498 key,
499 parsed.allocator_id,
500 scope,
501 parsed.old_len,
502 parsed.len,
503 );
504 } else {
505 self.counters.unmatched_resizes +|= 1;
506 }
507 } else {
508 self.counters.failed_resizes +|= 1;
509 if (!parsed.tracked) {
510 self.counters.unmatched_resizes +|= 1;
511 }
512 }
513 },
514 .remap => if (self.layer_filter.includes(parsed.layer)) {
515 if (parsed.succeeded) {
516 if (parsed.tracked) {
517 try self.recordRemap(
518 key,
519 allocationKey(
520 parsed.allocation_id,
521 parsed.address,
522 ),
523 parsed.allocator_id,
524 scope,
525 parsed.old_len,
526 parsed.len,
527 );
528 } else if (parsed.allocation_id != 0) {
529 try self.recordLostTrackingRemap(
530 key,
531 parsed.len,
532 );
533 } else {
534 self.counters.unmatched_remaps +|= 1;
535 }
536 } else {
537 self.counters.failed_remaps +|= 1;
538 if (!parsed.tracked) {
539 self.counters.unmatched_remaps +|= 1;
540 }
541 }
542 },
543 .lifecycle => if (self.layer_filter.includes(parsed.layer)) {
544 self.counters.lifecycle_events +|= 1;
545 if (self.allocators.getPtr(parsed.allocator_id)) |allocator| {
546 allocator.prefix_complete = true;
547 }
548 },
549 .map => if (self.layer_filter.includes(parsed.layer)) {
550 try self.recordPhysicalMap(parsed);
551 },
552 .unmap => if (self.layer_filter.includes(parsed.layer)) {
553 try self.recordPhysicalUnmap(parsed);
554 },
555 .protect => if (self.layer_filter.includes(parsed.layer)) {
556 self.recordPhysicalEffect(parsed.succeeded, .protect);
557 },
558 .discard => if (self.layer_filter.includes(parsed.layer)) {
559 self.recordPhysicalEffect(parsed.succeeded, .discard);
560 },
561 .decommit => if (self.layer_filter.includes(parsed.layer)) {
562 self.recordPhysicalEffect(parsed.succeeded, .decommit);
563 },
564 .advise => if (self.layer_filter.includes(parsed.layer)) {
565 self.recordPhysicalEffect(parsed.succeeded, .advise);
566 },
567 .snapshot, .census => {},
568 }
569 }
570
571 fn recordPhysicalMap(self: *Analyzer, parsed: ParsedEvent) !void {
572 if (!parsed.succeeded) {
573 self.physical.failed_maps +|= 1;
574 return;
575 }
576 const address = std.math.cast(usize, parsed.address) orelse
577 return error.InvalidMappingRange;
578 const displaced = try self.mappings.map(
579 self.allocator,
580 address,
581 parsed.len,
582 );
583 self.physical.maps +|= 1;
584 self.physical.mapped_bytes +|= parsed.len;
585 self.physical.displaced_bytes +|= displaced;
586 self.updatePhysicalLiveBytes();
587 }
588
589 fn recordPhysicalUnmap(self: *Analyzer, parsed: ParsedEvent) !void {
590 if (!parsed.succeeded) {
591 self.physical.failed_unmaps +|= 1;
592 return;
593 }
594 const address = std.math.cast(usize, parsed.address) orelse
595 return error.InvalidMappingRange;
596 const removal = try self.mappings.unmap(
597 self.allocator,
598 address,
599 parsed.len,
600 );
601 self.physical.unmaps +|= 1;
602 self.physical.unmapped_bytes +|= parsed.len;
603 self.physical.untracked_unmap_bytes +|= removal.untrackedBytes();
604 self.updatePhysicalLiveBytes();
605 }
606
607 const PhysicalEffect = enum {
608 protect,
609 discard,
610 decommit,
611 advise,
612 };
613
614 fn recordPhysicalEffect(
615 self: *Analyzer,
616 succeeded: bool,
617 effect: PhysicalEffect,
618 ) void {
619 const counter = switch (effect) {
620 .protect => if (succeeded)
621 &self.physical.protects
622 else
623 &self.physical.failed_protects,
624 .discard => if (succeeded)
625 &self.physical.discards
626 else
627 &self.physical.failed_discards,
628 .decommit => if (succeeded)
629 &self.physical.decommits
630 else
631 &self.physical.failed_decommits,
632 .advise => if (succeeded)
633 &self.physical.advises
634 else
635 &self.physical.failed_advises,
636 };
637 counter.* +|= 1;
638 }
639
640 fn updatePhysicalLiveBytes(self: *Analyzer) void {
641 self.physical.live_mapped_bytes = self.mappings.mapped_bytes;
642 self.physical.high_water_mapped_bytes = @max(
643 self.physical.high_water_mapped_bytes,
644 self.physical.live_mapped_bytes,
645 );
646 }
647
648 fn ingestGenericJsonLine(self: *Analyzer, text: []const u8) !void {
649 var parsed = std.json.parseFromSlice(
650 std.json.Value,
651 self.allocator,
652 text,
653 .{},
654 ) catch |err| switch (err) {
655 error.OutOfMemory => return err,
656 else => return error.InvalidEventJson,
657 };
658 defer parsed.deinit();
659
660 const object = switch (parsed.value) {
661 .object => |object| object,
662 else => return error.InvalidEventJson,
663 };
664 try self.ingestParsedEvent(try event_mod.replayFromJsonObject(object));
665 }
666
667 pub fn captureIntegrity(self: *const Analyzer) CaptureIntegrity {
668 var result: CaptureIntegrity = .{
669 .status = "complete",
670 .action = "none",
671 .message = null,
672 .event_count = self.events,
673 .sequenced_event_count = self.integrity_counters.sequenced_events,
674 .unsequenced_event_count = self.integrity_counters.unsequenced_events,
675 .first_sequence = self.first_sequence,
676 .last_sequence = self.last_sequence,
677 .sequence_gap_count = self.integrity_counters.sequence_gaps,
678 .missing_sequence_event_count = self.integrity_counters.missing_sequence_events,
679 .sequence_regression_count = self.integrity_counters.sequence_regressions,
680 .start_event_count = self.integrity_counters.start_events,
681 .stop_event_count = self.integrity_counters.stop_events,
682 .start_sequence = self.start_sequence,
683 .stop_sequence = self.stop_sequence,
684 .unbalanced_event_count = self.unbalancedEventCount(),
685 };
686 classifyCaptureIntegrity(&result);
687 return result;
688 }
689
690 fn recordSequence(self: *Analyzer, maybe_sequence: ?u64) void {
691 const sequence = maybe_sequence orelse {
692 self.integrity_counters.unsequenced_events +|= 1;
693 return;
694 };
695 if (sequence == 0) {
696 self.integrity_counters.unsequenced_events +|= 1;
697 return;
698 }
699 self.integrity_counters.sequenced_events +|= 1;
700 const previous = self.last_sequence orelse {
701 self.first_sequence = sequence;
702 self.last_sequence = sequence;
703 if (sequence > 1) {
704 self.integrity_counters.sequence_gaps +|= 1;
705 self.integrity_counters.missing_sequence_events +|= sequence - 1;
706 }
707 return;
708 };
709 if (sequence <= previous) {
710 self.integrity_counters.sequence_regressions +|= 1;
711 } else if (sequence - previous > 1) {
712 self.integrity_counters.sequence_gaps +|= 1;
713 self.integrity_counters.missing_sequence_events +|= sequence - previous - 1;
714 }
715 self.last_sequence = sequence;
716 }
717
718 fn recordScopeEnter(
719 self: *Analyzer,
720 scope_id: u32,
721 scope: []const u8,
722 ) !void {
723 const owned_scope = try self.internScope(scope);
724 const path = try self.scope_paths.getOrPut(self.allocator, scope_id);
725 if (!path.found_existing) {
726 path.value_ptr.* = owned_scope;
727 } else if (!std.mem.eql(u8, path.value_ptr.*, owned_scope)) {
728 return error.ScopeIdentityConflict;
729 }
730 const result = try self.active_scopes.getOrPut(self.allocator, scope_id);
731 if (!result.found_existing) result.value_ptr.* = 0;
732 result.value_ptr.* +|= 1;
733 self.integrity_counters.active_scope_events +|= 1;
734 }
735
736 fn recordScopeExit(self: *Analyzer, scope_id: u32) void {
737 const count = self.active_scopes.getPtr(scope_id) orelse {
738 self.integrity_counters.unmatched_scope_exits +|= 1;
739 return;
740 };
741 if (count.* == 0) {
742 self.integrity_counters.unmatched_scope_exits +|= 1;
743 return;
744 }
745 count.* -= 1;
746 self.integrity_counters.active_scope_events -= 1;
747 if (count.* == 0) _ = self.active_scopes.remove(scope_id);
748 }
749
750 fn unbalancedEventCount(self: *const Analyzer) u64 {
751 var count = self.counters.unmatched_frees +|
752 self.counters.unmatched_resizes +|
753 self.counters.unmatched_remaps +|
754 self.integrity_counters.unmatched_scope_exits;
755 count +|= self.integrity_counters.active_scope_events;
756 return count;
757 }
758
759 pub fn snapshot(
760 self: *Analyzer,
761 allocator: Allocator,
762 options: SnapshotOptions,
763 ) !Snapshot {
764 if (options.layer != self.layer_filter) {
765 return error.LayerSelectionMismatch;
766 }
767 var scope_summaries: [3]std.ArrayListUnmanaged(ScopeSummary) = .{
768 .empty,
769 .empty,
770 .empty,
771 };
772 defer for (&scope_summaries) |*summaries| {
773 summaries.deinit(self.allocator);
774 };
775 var site_summaries: [3]std.ArrayListUnmanaged(SiteSummary) = .{
776 .empty,
777 .empty,
778 .empty,
779 };
780 defer for (&site_summaries) |*summaries| {
781 summaries.deinit(self.allocator);
782 };
783
784 inline for (std.meta.tags(Sort)) |sort| {
785 const summary_options = SummaryOptions{
786 .top = options.top,
787 .min_bytes = options.min_bytes,
788 .include_zero_live = options.include_zero_live,
789 .include_sites = true,
790 .site_symbol_binary = options.site_symbol_binary,
791 .layer = options.layer,
792 .sort = sort,
793 };
794 scope_summaries[@backingInt(sort)] =
795 try self.collectScopeSummaries(summary_options);
796 site_summaries[@backingInt(sort)] =
797 try self.collectSiteSummaries(summary_options);
798 }
799
800 var addresses = std.ArrayListUnmanaged(u64).empty;
801 defer addresses.deinit(allocator);
802 inline for (std.meta.tags(Sort)) |sort| {
803 const sites = site_summaries[@backingInt(sort)].items;
804 const limit = @min(options.top, sites.len);
805 for (sites[0..limit]) |site| {
806 if (std.mem.indexOfScalar(
807 u64,
808 addresses.items,
809 site.return_address,
810 ) == null) {
811 try addresses.append(allocator, site.return_address);
812 }
813 }
814 }
815
816 var maybe_symbols = if (options.site_symbol_binary) |binary|
817 try resolveSiteAddressesAlloc(allocator, binary, addresses.items)
818 else
819 null;
820 defer if (maybe_symbols) |*symbols| symbols.deinit(allocator);
821
822 var result = Snapshot{
823 .allocator = allocator,
824 .integrity = self.captureIntegrity(),
825 .summary = summarySnapshot(self),
826 };
827 errdefer result.deinit();
828 inline for (std.meta.tags(Sort)) |sort| {
829 const scopes = scope_summaries[@backingInt(sort)].items;
830 const sites = site_summaries[@backingInt(sort)].items;
831 const ranking = result.rankings.getPtr(sort);
832 ranking.scopes = try snapshotScopes(
833 allocator,
834 scopes[0..@min(options.top, scopes.len)],
835 );
836 ranking.sources = try snapshotSources(
837 allocator,
838 sites[0..@min(options.top, sites.len)],
839 if (maybe_symbols) |*symbols| symbols else null,
840 );
841 }
842 if (maybe_symbols) |*symbols| {
843 result.symbol_storage = symbols.stdout;
844 symbols.ranges.deinit(allocator);
845 symbols.frames.deinit(allocator);
846 symbols.* = undefined;
847 maybe_symbols = null;
848 }
849 return result;
850 }
851
852 pub fn writeSummary(self: *Analyzer, writer: *std.Io.Writer, options: SummaryOptions) !void {
853 if (options.layer != self.layer_filter) {
854 return error.LayerSelectionMismatch;
855 }
856 var summaries = try self.collectScopeSummaries(options);
857 defer summaries.deinit(self.allocator);
858
859 try writeCaptureIntegrityText(writer, self.captureIntegrity());
860 if (options.layer == .logical) {
861 try writer.print(
862 "memtrace layer={s} sort={s} events={d} allocations={d} " ++
863 "frees={d} " ++
864 "open_requests={d} requested_bytes={d} " ++
865 "explicitly_closed_bytes={d} open_request_bytes={d} " ++
866 "high_water_open_request_bytes={d} " ++
867 "bulk_invalidated_requests={d} bulk_invalidated_bytes={d} " ++
868 "untracked_requests={d} untracked_request_bytes={d}\n",
869 .{
870 options.layer.tag(),
871 options.sort.name(),
872 self.events,
873 self.counters.allocations,
874 self.counters.frees,
875 self.counters.live_allocations,
876 self.counters.allocated_bytes,
877 self.counters.freed_bytes,
878 self.counters.live_bytes,
879 self.counters.high_water_live_bytes,
880 self.counters.bulk_invalidated_requests,
881 self.counters.bulk_invalidated_bytes,
882 self.counters.untracked_requests,
883 self.counters.untracked_request_bytes,
884 },
885 );
886 const coverage = self.lifecycleCoverage();
887 try writer.print(
888 "memtrace lifecycle_coverage={s} instrumented_producers={d} " ++
889 "uninstrumented_producers={d} " ++
890 "lifecycle_not_required_producers={d} " ++
891 "prefix_complete_producers={d} prefix_partial_producers={d} " ++
892 "lifecycle_events={d}\n",
893 .{
894 coverage.status(),
895 coverage.instrumented,
896 coverage.uninstrumented,
897 coverage.not_required,
898 coverage.prefix_complete,
899 coverage.prefix_partial,
900 self.counters.lifecycle_events,
901 },
902 );
903 } else {
904 try writer.print(
905 "memtrace layer={s} sort={s} events={d} allocations={d} " ++
906 "frees={d} live_allocations={d} allocated_bytes={d} " ++
907 "freed_bytes={d} live_bytes={d} " ++
908 "high_water_live_bytes={d}\n",
909 .{
910 options.layer.tag(),
911 options.sort.name(),
912 self.events,
913 self.counters.allocations,
914 self.counters.frees,
915 self.counters.live_allocations,
916 self.counters.allocated_bytes,
917 self.counters.freed_bytes,
918 self.counters.live_bytes,
919 self.counters.high_water_live_bytes,
920 },
921 );
922 try writer.print(
923 "memtrace retained_bytes={d} high_water_retained_bytes={d}\n",
924 .{
925 self.counters.retained_bytes,
926 self.counters.high_water_retained_bytes,
927 },
928 );
929 }
930 try writer.print(
931 "memtrace lifetimes completed={d} total_events={d} mean_events={d} " ++
932 "max_events={d} total_byte_events={d} " ++
933 "mean_byte_events={d} max_byte_events={d}\n",
934 .{
935 self.counters.completed_lifetimes,
936 self.counters.lifetime_total_events,
937 meanLifetimeEvents(self.counters),
938 self.counters.lifetime_max_events,
939 self.counters.lifetime_total_byte_events,
940 meanLifetimeByteEvents(self.counters),
941 self.counters.lifetime_max_byte_events,
942 },
943 );
944 if (self.counters.failed_allocations != 0 or
945 self.counters.failed_resizes != 0 or
946 self.counters.failed_remaps != 0 or
947 self.counters.unmatched_frees != 0 or
948 self.counters.unmatched_resizes != 0 or
949 self.counters.unmatched_remaps != 0)
950 {
951 try writer.print(
952 "memtrace anomalies failed_allocations={d} failed_resizes={d} " ++
953 "failed_remaps={d} unmatched_frees={d} unmatched_resizes={d} " ++
954 "unmatched_remaps={d}\n",
955 .{
956 self.counters.failed_allocations,
957 self.counters.failed_resizes,
958 self.counters.failed_remaps,
959 self.counters.unmatched_frees,
960 self.counters.unmatched_resizes,
961 self.counters.unmatched_remaps,
962 },
963 );
964 }
965 if (options.layer.includes(.physical_page)) {
966 try self.writePhysicalSummary(writer);
967 }
968 const limit = @min(options.top, summaries.items.len);
969 for (summaries.items[0..limit]) |summary| {
970 if (options.layer == .logical) {
971 try writer.print(
972 "{s} open_request_bytes={d} " ++
973 "high_water_open_request_bytes={d} requested_bytes={d} " ++
974 "explicitly_closed_bytes={d} allocations={d} frees={d} " ++
975 "open_requests={d} bulk_invalidated_requests={d} " ++
976 "bulk_invalidated_bytes={d} untracked_requests={d} " ++
977 "untracked_request_bytes={d}\n",
978 .{
979 summary.path,
980 summary.counters.live_bytes,
981 summary.counters.high_water_live_bytes,
982 summary.counters.allocated_bytes,
983 summary.counters.freed_bytes,
984 summary.counters.allocations,
985 summary.counters.frees,
986 summary.counters.live_allocations,
987 summary.counters.bulk_invalidated_requests,
988 summary.counters.bulk_invalidated_bytes,
989 summary.counters.untracked_requests,
990 summary.counters.untracked_request_bytes,
991 },
992 );
993 } else {
994 try writer.print(
995 "{s} retained_bytes={d} high_water_retained_bytes={d} " ++
996 "live_bytes={d} high_water_live_bytes={d} " ++
997 "allocated_bytes={d} freed_bytes={d} allocations={d} " ++
998 "frees={d} live_allocations={d} completed_lifetimes={d} " ++
999 "lifetime_total_events={d} lifetime_mean_events={d} " ++
1000 "lifetime_max_events={d} " ++
1001 "lifetime_total_byte_events={d} " ++
1002 "lifetime_mean_byte_events={d} " ++
1003 "lifetime_max_byte_events={d}\n",
1004 .{
1005 summary.path,
1006 summary.counters.retained_bytes,
1007 summary.counters.high_water_retained_bytes,
1008 summary.counters.live_bytes,
1009 summary.counters.high_water_live_bytes,
1010 summary.counters.allocated_bytes,
1011 summary.counters.freed_bytes,
1012 summary.counters.allocations,
1013 summary.counters.frees,
1014 summary.counters.live_allocations,
1015 summary.counters.completed_lifetimes,
1016 summary.counters.lifetime_total_events,
1017 meanLifetimeEvents(summary.counters),
1018 summary.counters.lifetime_max_events,
1019 summary.counters.lifetime_total_byte_events,
1020 meanLifetimeByteEvents(summary.counters),
1021 summary.counters.lifetime_max_byte_events,
1022 },
1023 );
1024 }
1025 }
1026 if (options.include_sites) try self.writeSiteSummary(writer, options);
1027 if (options.site_detail_return_address) |return_address| try self.writeSiteDetailSummary(writer, options, return_address);
1028 }
1029
1030 pub fn writeSummaryJsonl(self: *Analyzer, writer: *std.Io.Writer, options: SummaryOptions) !void {
1031 if (options.layer != self.layer_filter) {
1032 return error.LayerSelectionMismatch;
1033 }
1034 var summaries = try self.collectScopeSummaries(options);
1035 defer summaries.deinit(self.allocator);
1036
1037 var stream = pretty_json.Writer.init(writer, .minified);
1038 const object = try stream.object();
1039 try object.field("kind", "summary");
1040 try object.field("layer", options.layer.tag());
1041 try object.field("sort", options.sort.name());
1042 try object.field("events", self.events);
1043 try object.field("allocations", self.counters.allocations);
1044 try object.field("frees", self.counters.frees);
1045 try object.field("resizes", self.counters.resizes);
1046 try object.field("remaps", self.counters.remaps);
1047 try object.field("allocated_bytes", self.counters.allocated_bytes);
1048 if (options.layer == .logical) {
1049 try object.field("open_requests", self.counters.live_allocations);
1050 try object.field("requested_bytes", self.counters.allocated_bytes);
1051 try object.field(
1052 "explicitly_closed_bytes",
1053 self.counters.freed_bytes,
1054 );
1055 try object.field("open_request_bytes", self.counters.live_bytes);
1056 try object.field(
1057 "high_water_open_request_bytes",
1058 self.counters.high_water_live_bytes,
1059 );
1060 try object.field(
1061 "bulk_invalidated_requests",
1062 self.counters.bulk_invalidated_requests,
1063 );
1064 try object.field(
1065 "bulk_invalidated_bytes",
1066 self.counters.bulk_invalidated_bytes,
1067 );
1068 try object.field(
1069 "untracked_requests",
1070 self.counters.untracked_requests,
1071 );
1072 try object.field(
1073 "untracked_request_bytes",
1074 self.counters.untracked_request_bytes,
1075 );
1076 try object.field(
1077 "lifecycle_events",
1078 self.counters.lifecycle_events,
1079 );
1080 const coverage = self.lifecycleCoverage();
1081 try object.field("lifecycle_coverage", coverage.status());
1082 try object.field(
1083 "lifecycle_instrumented_producers",
1084 coverage.instrumented,
1085 );
1086 try object.field(
1087 "lifecycle_uninstrumented_producers",
1088 coverage.uninstrumented,
1089 );
1090 try object.field(
1091 "lifecycle_not_required_producers",
1092 coverage.not_required,
1093 );
1094 try object.field(
1095 "prefix_complete_producers",
1096 coverage.prefix_complete,
1097 );
1098 try object.field(
1099 "prefix_partial_producers",
1100 coverage.prefix_partial,
1101 );
1102 } else {
1103 try object.field("live_allocations", self.counters.live_allocations);
1104 try object.field("freed_bytes", self.counters.freed_bytes);
1105 try object.field("live_bytes", self.counters.live_bytes);
1106 try object.field(
1107 "high_water_live_bytes",
1108 self.counters.high_water_live_bytes,
1109 );
1110 try object.field("retained_bytes", self.counters.retained_bytes);
1111 try object.field(
1112 "high_water_retained_bytes",
1113 self.counters.high_water_retained_bytes,
1114 );
1115 }
1116 try object.field("completed_lifetimes", self.counters.completed_lifetimes);
1117 try object.field("lifetime_total_events", self.counters.lifetime_total_events);
1118 try object.field("lifetime_mean_events", meanLifetimeEvents(self.counters));
1119 try object.field("lifetime_max_events", self.counters.lifetime_max_events);
1120 try object.field(
1121 "lifetime_total_byte_events",
1122 self.counters.lifetime_total_byte_events,
1123 );
1124 try object.field(
1125 "lifetime_mean_byte_events",
1126 meanLifetimeByteEvents(self.counters),
1127 );
1128 try object.field(
1129 "lifetime_max_byte_events",
1130 self.counters.lifetime_max_byte_events,
1131 );
1132 try object.field("failed_allocations", self.counters.failed_allocations);
1133 try object.field("failed_resizes", self.counters.failed_resizes);
1134 try object.field("failed_remaps", self.counters.failed_remaps);
1135 try object.field("unmatched_frees", self.counters.unmatched_frees);
1136 try object.field("unmatched_resizes", self.counters.unmatched_resizes);
1137 try object.field("unmatched_remaps", self.counters.unmatched_remaps);
1138 if (options.layer.includes(.physical_page)) {
1139 try self.writePhysicalSummaryJson(object);
1140 }
1141 try writeCaptureIntegrityJson(object, self.captureIntegrity());
1142 try object.endLine();
1143 const limit = @min(options.top, summaries.items.len);
1144 for (summaries.items[0..limit]) |summary| {
1145 var row_stream = pretty_json.Writer.init(writer, .minified);
1146 const row = try row_stream.object();
1147 try row.field("kind", "scope");
1148 try row.field("sort", options.sort.name());
1149 try row.field("scope", summary.path);
1150 try writeCounterFields(
1151 row,
1152 summary.counters,
1153 options.layer == .logical,
1154 );
1155 try row.endLine();
1156 }
1157 if (options.include_sites) try self.writeSiteSummaryJsonl(writer, options);
1158 if (options.site_detail_return_address) |return_address| try self.writeSiteDetailSummaryJsonl(writer, options, return_address);
1159 }
1160
1161 fn writePhysicalSummary(
1162 self: *const Analyzer,
1163 writer: *std.Io.Writer,
1164 ) !void {
1165 try writer.print(
1166 "memtrace physical maps={d} unmaps={d} protects={d} discards={d} " ++
1167 "decommits={d} advises={d} mapped_bytes={d} unmapped_bytes={d} " ++
1168 "displaced_bytes={d} untracked_unmap_bytes={d} " ++
1169 "live_mapped_bytes={d} high_water_mapped_bytes={d} " ++
1170 "live_ranges={d}\n",
1171 .{
1172 self.physical.maps,
1173 self.physical.unmaps,
1174 self.physical.protects,
1175 self.physical.discards,
1176 self.physical.decommits,
1177 self.physical.advises,
1178 self.physical.mapped_bytes,
1179 self.physical.unmapped_bytes,
1180 self.physical.displaced_bytes,
1181 self.physical.untracked_unmap_bytes,
1182 self.physical.live_mapped_bytes,
1183 self.physical.high_water_mapped_bytes,
1184 self.mappings.count(),
1185 },
1186 );
1187 if (self.physical.failed_maps != 0 or
1188 self.physical.failed_unmaps != 0 or
1189 self.physical.failed_protects != 0 or
1190 self.physical.failed_discards != 0 or
1191 self.physical.failed_decommits != 0 or
1192 self.physical.failed_advises != 0)
1193 {
1194 try writer.print(
1195 "memtrace physical_failures maps={d} unmaps={d} protects={d} " ++
1196 "discards={d} decommits={d} advises={d}\n",
1197 .{
1198 self.physical.failed_maps,
1199 self.physical.failed_unmaps,
1200 self.physical.failed_protects,
1201 self.physical.failed_discards,
1202 self.physical.failed_decommits,
1203 self.physical.failed_advises,
1204 },
1205 );
1206 }
1207 }
1208
1209 fn writePhysicalSummaryJson(
1210 self: *const Analyzer,
1211 object: pretty_json.Object,
1212 ) !void {
1213 const physical = try object.object("physical");
1214 try physical.field("maps", self.physical.maps);
1215 try physical.field("unmaps", self.physical.unmaps);
1216 try physical.field("protects", self.physical.protects);
1217 try physical.field("discards", self.physical.discards);
1218 try physical.field("decommits", self.physical.decommits);
1219 try physical.field("advises", self.physical.advises);
1220 try physical.field("failed_maps", self.physical.failed_maps);
1221 try physical.field("failed_unmaps", self.physical.failed_unmaps);
1222 try physical.field("failed_protects", self.physical.failed_protects);
1223 try physical.field("failed_discards", self.physical.failed_discards);
1224 try physical.field("failed_decommits", self.physical.failed_decommits);
1225 try physical.field("failed_advises", self.physical.failed_advises);
1226 try physical.field("mapped_bytes", self.physical.mapped_bytes);
1227 try physical.field("unmapped_bytes", self.physical.unmapped_bytes);
1228 try physical.field("displaced_bytes", self.physical.displaced_bytes);
1229 try physical.field("untracked_unmap_bytes", self.physical.untracked_unmap_bytes);
1230 try physical.field("live_mapped_bytes", self.physical.live_mapped_bytes);
1231 try physical.field("high_water_mapped_bytes", self.physical.high_water_mapped_bytes);
1232 try physical.field("live_ranges", self.mappings.count());
1233 try physical.end();
1234 }
1235
1236 fn collectScopeSummaries(
1237 self: *Analyzer,
1238 options: SummaryOptions,
1239 ) !std.ArrayListUnmanaged(ScopeSummary) {
1240 var summaries = std.ArrayListUnmanaged(ScopeSummary).empty;
1241 errdefer summaries.deinit(self.allocator);
1242 var iterator = self.scopes.iterator();
1243 while (iterator.next()) |entry| {
1244 const counters = entry.value_ptr.*;
1245 const invisible = !options.include_zero_live and counters.live_bytes == 0 and
1246 counters.high_water_live_bytes == 0 and counters.retained_bytes == 0;
1247 if (invisible) continue;
1248 const below_floor = counters.live_bytes < options.min_bytes and
1249 counters.high_water_live_bytes < options.min_bytes and
1250 counters.retained_bytes < options.min_bytes;
1251 if (below_floor) continue;
1252 try summaries.append(self.allocator, .{
1253 .path = entry.key_ptr.*,
1254 .counters = counters,
1255 });
1256 }
1257 std.mem.sort(
1258 ScopeSummary,
1259 summaries.items,
1260 options.sort,
1261 scopeSummaryGreaterThan,
1262 );
1263 return summaries;
1264 }
1265
1266 fn recordAllocator(
1267 self: *Analyzer,
1268 allocator_id: u32,
1269 retains_freed_memory: bool,
1270 layer: event_mod.Layer,
1271 lifecycle_instrumented: bool,
1272 prefix_complete: bool,
1273 ) !void {
1274 try self.allocators.put(self.allocator, allocator_id, .{
1275 .retention = if (retains_freed_memory) .retains_freed_memory else .releases_freed_memory,
1276 .layer = layer,
1277 .lifecycle_instrumented = lifecycle_instrumented,
1278 .prefix_complete = prefix_complete,
1279 });
1280 }
1281
1282 fn recordAllocation(self: *Analyzer, allocator_id: u32, key: u64, scope: []const u8, return_address: u64, len: usize) !void {
1283 const owned_scope = try self.internScope(scope);
1284 try self.allocations.put(self.allocator, key, .{
1285 .allocator_id = allocator_id,
1286 .len = len,
1287 .scope = owned_scope,
1288 .return_address = return_address,
1289 .allocation_event = self.events,
1290 .segment_event = self.events,
1291 .byte_events = 0,
1292 });
1293 self.applyAllocation(allocator_id, owned_scope, len);
1294 if (self.track_sites) {
1295 try self.applySiteAllocation(
1296 allocator_id,
1297 return_address,
1298 len,
1299 );
1300 }
1301 if (self.site_detail_return_address == return_address) {
1302 try self.applySiteDetailAllocation(
1303 allocator_id,
1304 owned_scope,
1305 len,
1306 );
1307 }
1308 }
1309
1310 fn recordUntrackedAllocation(
1311 self: *Analyzer,
1312 scope: []const u8,
1313 return_address: u64,
1314 len: usize,
1315 ) !void {
1316 const owned_scope = try self.internScope(scope);
1317 self.counters.allocations +|= 1;
1318 self.counters.allocated_bytes +|= len;
1319 self.counters.untracked_requests +|= 1;
1320 self.counters.untracked_request_bytes +|= len;
1321 const scope_counters = self.scopes.getPtr(owned_scope).?;
1322 scope_counters.allocations +|= 1;
1323 scope_counters.allocated_bytes +|= len;
1324 scope_counters.untracked_requests +|= 1;
1325 scope_counters.untracked_request_bytes +|= len;
1326 if (self.track_sites) {
1327 const site = try self.siteCounters(return_address);
1328 site.allocations +|= 1;
1329 site.allocated_bytes +|= len;
1330 site.untracked_requests +|= 1;
1331 site.untracked_request_bytes +|= len;
1332 }
1333 }
1334
1335 fn recordFree(self: *Analyzer, key: u64, allocator_id: u32, scope: []const u8, len: usize) !void {
1336 if (self.allocations.fetchRemove(key)) |removed| {
1337 self.clearDrainedAllocations();
1338 self.applyLifetime(
1339 removed.value.scope,
1340 removed.value.return_address,
1341 removed.value.len,
1342 completedLifetimeEvents(removed.value.allocation_event, self.events),
1343 removed.value.byte_events +|
1344 completedLifetimeByteEvents(
1345 removed.value.len,
1346 removed.value.segment_event,
1347 self.events,
1348 ),
1349 );
1350 self.applyFree(removed.value.allocator_id, removed.value.scope, removed.value.len);
1351 if (self.track_sites) self.applySiteFree(removed.value.allocator_id, removed.value.return_address, removed.value.len);
1352 if (self.site_detail_return_address == removed.value.return_address) self.applySiteDetailFree(removed.value.allocator_id, removed.value.scope, removed.value.len);
1353 return;
1354 }
1355 self.counters.unmatched_frees += 1;
1356 const owned_scope = try self.internScope(scope);
1357 self.applyFree(allocator_id, owned_scope, len);
1358 }
1359
1360 fn recordRelease(self: *Analyzer, key: u64) !void {
1361 const removed = self.allocations.fetchRemove(key) orelse {
1362 self.counters.unmatched_frees +|= 1;
1363 return;
1364 };
1365 self.clearDrainedAllocations();
1366 self.applyLifetime(
1367 removed.value.scope,
1368 removed.value.return_address,
1369 removed.value.len,
1370 completedLifetimeEvents(
1371 removed.value.allocation_event,
1372 self.events,
1373 ),
1374 removed.value.byte_events +|
1375 completedLifetimeByteEvents(
1376 removed.value.len,
1377 removed.value.segment_event,
1378 self.events,
1379 ),
1380 );
1381 self.applyBulkInvalidation(
1382 removed.value.allocator_id,
1383 removed.value.scope,
1384 removed.value.len,
1385 );
1386 if (self.track_sites) {
1387 self.applySiteBulkInvalidation(
1388 removed.value.return_address,
1389 removed.value.len,
1390 );
1391 }
1392 if (self.site_detail_return_address == removed.value.return_address) {
1393 self.applySiteDetailBulkInvalidation(
1394 removed.value.scope,
1395 removed.value.len,
1396 );
1397 }
1398 }
1399
1400 fn recordResize(self: *Analyzer, key: u64, allocator_id: u32, scope: []const u8, old_len: usize, new_len: usize) !void {
1401 self.counters.resizes += 1;
1402 try self.resizeAllocation(
1403 key,
1404 allocator_id,
1405 scope,
1406 old_len,
1407 new_len,
1408 false,
1409 );
1410 }
1411
1412 fn resizeAllocation(
1413 self: *Analyzer,
1414 key: u64,
1415 allocator_id: u32,
1416 scope: []const u8,
1417 old_len: usize,
1418 new_len: usize,
1419 is_remap: bool,
1420 ) !void {
1421 if (self.allocations.getPtr(key)) |record| {
1422 accrueLifetimeByteEvents(record, self.events);
1423 self.applyResize(record.allocator_id, record.scope, record.len, new_len);
1424 if (self.track_sites) self.applySiteResize(record.allocator_id, record.return_address, record.len, new_len);
1425 if (self.site_detail_return_address == record.return_address) try self.applySiteDetailResize(record.allocator_id, record.scope, record.len, new_len);
1426 record.len = new_len;
1427 return;
1428 }
1429 _ = allocator_id;
1430 _ = scope;
1431 _ = old_len;
1432 if (is_remap) {
1433 self.counters.unmatched_remaps += 1;
1434 } else {
1435 self.counters.unmatched_resizes += 1;
1436 }
1437 }
1438
1439 fn recordRemap(self: *Analyzer, old_key: u64, new_key: u64, allocator_id: u32, scope: []const u8, old_len: usize, new_len: usize) !void {
1440 self.counters.remaps += 1;
1441 if (old_key == new_key) {
1442 try self.resizeAllocation(
1443 old_key,
1444 allocator_id,
1445 scope,
1446 old_len,
1447 new_len,
1448 true,
1449 );
1450 return;
1451 }
1452 if (self.allocations.fetchRemove(old_key)) |removed| {
1453 var record = removed.value;
1454 self.clearDrainedAllocations();
1455 accrueLifetimeByteEvents(&record, self.events);
1456 self.applyResize(record.allocator_id, record.scope, record.len, new_len);
1457 if (self.track_sites) self.applySiteResize(record.allocator_id, record.return_address, record.len, new_len);
1458 if (self.site_detail_return_address == record.return_address) try self.applySiteDetailResize(record.allocator_id, record.scope, record.len, new_len);
1459 record.len = new_len;
1460 try self.allocations.put(self.allocator, new_key, record);
1461 return;
1462 }
1463 self.counters.unmatched_remaps += 1;
1464 }
1465
1466 fn recordLostTrackingRemap(
1467 self: *Analyzer,
1468 key: u64,
1469 new_len: usize,
1470 ) !void {
1471 const removed = self.allocations.fetchRemove(key) orelse {
1472 self.counters.unmatched_remaps +|= 1;
1473 return;
1474 };
1475 self.clearDrainedAllocations();
1476 const record = removed.value;
1477 self.counters.resizes +|= 1;
1478 self.applyResize(
1479 record.allocator_id,
1480 record.scope,
1481 record.len,
1482 new_len,
1483 );
1484 self.applyLostTracking(record.scope, new_len);
1485 if (self.track_sites) {
1486 self.applySiteResize(
1487 record.allocator_id,
1488 record.return_address,
1489 record.len,
1490 new_len,
1491 );
1492 self.applySiteLostTracking(record.return_address, new_len);
1493 }
1494 if (self.site_detail_return_address == record.return_address) {
1495 try self.applySiteDetailResize(
1496 record.allocator_id,
1497 record.scope,
1498 record.len,
1499 new_len,
1500 );
1501 self.applySiteDetailLostTracking(record.scope, new_len);
1502 }
1503 }
1504
1505 fn applyAllocation(self: *Analyzer, allocator_id: u32, scope: []const u8, len: usize) void {
1506 self.counters.allocations += 1;
1507 self.counters.live_allocations += 1;
1508 self.counters.allocated_bytes += len;
1509 self.counters.live_bytes += len;
1510 self.counters.high_water_live_bytes = @max(self.counters.high_water_live_bytes, self.counters.live_bytes);
1511 const tracks_retained = self.allocatorLayer(allocator_id) !=
1512 .logical_allocator;
1513 if (tracks_retained) {
1514 self.counters.retained_bytes += len;
1515 self.counters.high_water_retained_bytes = @max(
1516 self.counters.high_water_retained_bytes,
1517 self.counters.retained_bytes,
1518 );
1519 }
1520
1521 const counters = self.scopes.getPtr(scope) orelse return;
1522 counters.allocations += 1;
1523 counters.live_allocations += 1;
1524 counters.allocated_bytes += len;
1525 counters.live_bytes += len;
1526 counters.high_water_live_bytes = @max(counters.high_water_live_bytes, counters.live_bytes);
1527 if (tracks_retained) {
1528 counters.retained_bytes += len;
1529 counters.high_water_retained_bytes = @max(
1530 counters.high_water_retained_bytes,
1531 counters.retained_bytes,
1532 );
1533 }
1534 }
1535
1536 fn applyFree(self: *Analyzer, allocator_id: u32, scope: []const u8, len: usize) void {
1537 self.counters.frees += 1;
1538 if (self.counters.live_allocations > 0) self.counters.live_allocations -= 1;
1539 self.counters.freed_bytes += len;
1540 if (self.counters.live_bytes >= len) self.counters.live_bytes -= len else self.counters.live_bytes = 0;
1541 const releases = self.allocatorReleases(allocator_id);
1542 if (releases and self.counters.retained_bytes >= len) self.counters.retained_bytes -= len;
1543
1544 const counters = self.scopes.getPtr(scope) orelse return;
1545 counters.frees += 1;
1546 if (counters.live_allocations > 0) counters.live_allocations -= 1;
1547 counters.freed_bytes += len;
1548 if (counters.live_bytes >= len) counters.live_bytes -= len else counters.live_bytes = 0;
1549 if (releases and counters.retained_bytes >= len) counters.retained_bytes -= len;
1550 }
1551
1552 fn applyBulkInvalidation(
1553 self: *Analyzer,
1554 allocator_id: u32,
1555 scope: []const u8,
1556 len: usize,
1557 ) void {
1558 _ = allocator_id;
1559 self.counters.bulk_invalidated_requests +|= 1;
1560 self.counters.bulk_invalidated_bytes +|= len;
1561 if (self.counters.live_allocations > 0) {
1562 self.counters.live_allocations -= 1;
1563 }
1564 if (self.counters.live_bytes >= len) {
1565 self.counters.live_bytes -= len;
1566 } else {
1567 self.counters.live_bytes = 0;
1568 }
1569 const counters = self.scopes.getPtr(scope) orelse return;
1570 counters.bulk_invalidated_requests +|= 1;
1571 counters.bulk_invalidated_bytes +|= len;
1572 if (counters.live_allocations > 0) counters.live_allocations -= 1;
1573 if (counters.live_bytes >= len) {
1574 counters.live_bytes -= len;
1575 } else {
1576 counters.live_bytes = 0;
1577 }
1578 }
1579
1580 fn applyLostTracking(
1581 self: *Analyzer,
1582 scope: []const u8,
1583 len: usize,
1584 ) void {
1585 applyCountersLostTracking(&self.counters, len);
1586 if (self.scopes.getPtr(scope)) |counters| {
1587 applyCountersLostTracking(counters, len);
1588 }
1589 }
1590
1591 fn applyLifetime(
1592 self: *Analyzer,
1593 scope: []const u8,
1594 return_address: u64,
1595 len: usize,
1596 lifetime_events: u64,
1597 lifetime_byte_events: u64,
1598 ) void {
1599 applyCompletedLifetime(
1600 &self.counters,
1601 lifetime_events,
1602 lifetime_byte_events,
1603 );
1604 if (self.scopes.getPtr(scope)) |counters| {
1605 applyCompletedLifetime(
1606 counters,
1607 lifetime_events,
1608 lifetime_byte_events,
1609 );
1610 }
1611 if (self.track_sites) {
1612 if (self.sites.getPtr(return_address)) |counters| {
1613 applyCompletedLifetime(
1614 counters,
1615 lifetime_events,
1616 lifetime_byte_events,
1617 );
1618 }
1619 }
1620 if (self.site_detail_return_address == return_address) {
1621 if (self.site_detail_sizes.getPtr(len)) |counters| {
1622 applyCompletedLifetime(
1623 counters,
1624 lifetime_events,
1625 lifetime_byte_events,
1626 );
1627 }
1628 if (self.site_detail_scopes.getPtr(scope)) |counters| {
1629 applyCompletedLifetime(
1630 counters,
1631 lifetime_events,
1632 lifetime_byte_events,
1633 );
1634 }
1635 }
1636 }
1637
1638 fn applyResize(self: *Analyzer, allocator_id: u32, scope: []const u8, old_len: usize, new_len: usize) void {
1639 if (new_len >= old_len) {
1640 const delta = new_len - old_len;
1641 self.counters.allocated_bytes += delta;
1642 self.counters.live_bytes += delta;
1643 self.counters.high_water_live_bytes = @max(self.counters.high_water_live_bytes, self.counters.live_bytes);
1644 const tracks_retained = self.allocatorLayer(allocator_id) !=
1645 .logical_allocator;
1646 if (tracks_retained) {
1647 self.counters.retained_bytes += delta;
1648 self.counters.high_water_retained_bytes = @max(
1649 self.counters.high_water_retained_bytes,
1650 self.counters.retained_bytes,
1651 );
1652 }
1653 if (self.scopes.getPtr(scope)) |counters| {
1654 counters.allocated_bytes += delta;
1655 counters.live_bytes += delta;
1656 counters.high_water_live_bytes = @max(counters.high_water_live_bytes, counters.live_bytes);
1657 if (tracks_retained) {
1658 counters.retained_bytes += delta;
1659 counters.high_water_retained_bytes = @max(
1660 counters.high_water_retained_bytes,
1661 counters.retained_bytes,
1662 );
1663 }
1664 }
1665 } else {
1666 const delta = old_len - new_len;
1667 self.counters.freed_bytes += delta;
1668 if (self.counters.live_bytes >= delta) self.counters.live_bytes -= delta else self.counters.live_bytes = 0;
1669 const releases = self.allocatorReleases(allocator_id);
1670 if (releases and self.counters.retained_bytes >= delta) self.counters.retained_bytes -= delta;
1671 if (self.scopes.getPtr(scope)) |counters| {
1672 counters.freed_bytes += delta;
1673 if (counters.live_bytes >= delta) counters.live_bytes -= delta else counters.live_bytes = 0;
1674 if (releases and counters.retained_bytes >= delta) counters.retained_bytes -= delta;
1675 }
1676 }
1677 }
1678
1679 fn applySiteAllocation(
1680 self: *Analyzer,
1681 allocator_id: u32,
1682 return_address: u64,
1683 len: usize,
1684 ) !void {
1685 const counters = try self.siteCounters(return_address);
1686 counters.allocations += 1;
1687 counters.live_allocations += 1;
1688 counters.allocated_bytes += len;
1689 counters.live_bytes += len;
1690 counters.high_water_live_bytes = @max(counters.high_water_live_bytes, counters.live_bytes);
1691 if (self.allocatorLayer(allocator_id) != .logical_allocator) {
1692 counters.retained_bytes += len;
1693 counters.high_water_retained_bytes = @max(
1694 counters.high_water_retained_bytes,
1695 counters.retained_bytes,
1696 );
1697 }
1698 }
1699
1700 fn applySiteFree(self: *Analyzer, allocator_id: u32, return_address: u64, len: usize) void {
1701 const counters = self.sites.getPtr(return_address) orelse return;
1702 counters.frees += 1;
1703 if (counters.live_allocations > 0) counters.live_allocations -= 1;
1704 counters.freed_bytes += len;
1705 if (counters.live_bytes >= len) counters.live_bytes -= len else counters.live_bytes = 0;
1706 if (self.allocatorReleases(allocator_id) and counters.retained_bytes >= len) counters.retained_bytes -= len;
1707 }
1708
1709 fn applySiteBulkInvalidation(
1710 self: *Analyzer,
1711 return_address: u64,
1712 len: usize,
1713 ) void {
1714 const counters = self.sites.getPtr(return_address) orelse return;
1715 counters.bulk_invalidated_requests +|= 1;
1716 counters.bulk_invalidated_bytes +|= len;
1717 if (counters.live_allocations > 0) counters.live_allocations -= 1;
1718 if (counters.live_bytes >= len) {
1719 counters.live_bytes -= len;
1720 } else {
1721 counters.live_bytes = 0;
1722 }
1723 }
1724
1725 fn applySiteLostTracking(
1726 self: *Analyzer,
1727 return_address: u64,
1728 len: usize,
1729 ) void {
1730 const counters = self.sites.getPtr(return_address) orelse return;
1731 applyCountersLostTracking(counters, len);
1732 }
1733
1734 fn applySiteResize(self: *Analyzer, allocator_id: u32, return_address: u64, old_len: usize, new_len: usize) void {
1735 const counters = self.sites.getPtr(return_address) orelse return;
1736 if (new_len >= old_len) {
1737 const delta = new_len - old_len;
1738 counters.allocated_bytes += delta;
1739 counters.live_bytes += delta;
1740 counters.high_water_live_bytes = @max(counters.high_water_live_bytes, counters.live_bytes);
1741 counters.retained_bytes += delta;
1742 counters.high_water_retained_bytes = @max(counters.high_water_retained_bytes, counters.retained_bytes);
1743 } else {
1744 const delta = old_len - new_len;
1745 counters.freed_bytes += delta;
1746 if (counters.live_bytes >= delta) counters.live_bytes -= delta else counters.live_bytes = 0;
1747 if (self.allocatorReleases(allocator_id) and counters.retained_bytes >= delta) counters.retained_bytes -= delta;
1748 }
1749 }
1750
1751 fn siteCounters(self: *Analyzer, return_address: u64) !*SiteCounters {
1752 const entry = try self.sites.getOrPut(self.allocator, return_address);
1753 if (!entry.found_existing) entry.value_ptr.* = .{};
1754 return entry.value_ptr;
1755 }
1756
1757 fn applySiteDetailAllocation(
1758 self: *Analyzer,
1759 allocator_id: u32,
1760 scope: []const u8,
1761 len: usize,
1762 ) !void {
1763 const tracks_retained = self.allocatorLayer(allocator_id) !=
1764 .logical_allocator;
1765 applyDetailAllocation(
1766 try self.siteDetailSizeCounters(len),
1767 len,
1768 tracks_retained,
1769 );
1770 applyDetailAllocation(
1771 try self.siteDetailScopeCounters(scope),
1772 len,
1773 tracks_retained,
1774 );
1775 }
1776
1777 fn applySiteDetailFree(self: *Analyzer, allocator_id: u32, scope: []const u8, len: usize) void {
1778 const releases = self.allocatorReleases(allocator_id);
1779 if (self.site_detail_sizes.getPtr(len)) |counters| applyDetailFree(counters, len, releases);
1780 if (self.site_detail_scopes.getPtr(scope)) |counters| applyDetailFree(counters, len, releases);
1781 }
1782
1783 fn applySiteDetailResize(self: *Analyzer, allocator_id: u32, scope: []const u8, old_len: usize, new_len: usize) !void {
1784 if (old_len == new_len) return;
1785 self.applySiteDetailFree(allocator_id, scope, old_len);
1786 try self.applySiteDetailAllocation(allocator_id, scope, new_len);
1787 }
1788
1789 fn applySiteDetailBulkInvalidation(
1790 self: *Analyzer,
1791 scope: []const u8,
1792 len: usize,
1793 ) void {
1794 if (self.site_detail_sizes.getPtr(len)) |counters| {
1795 applyDetailBulkInvalidation(counters, len);
1796 }
1797 if (self.site_detail_scopes.getPtr(scope)) |counters| {
1798 applyDetailBulkInvalidation(counters, len);
1799 }
1800 }
1801
1802 fn applySiteDetailLostTracking(
1803 self: *Analyzer,
1804 scope: []const u8,
1805 len: usize,
1806 ) void {
1807 if (self.site_detail_sizes.getPtr(len)) |counters| {
1808 applyCountersLostTracking(counters, len);
1809 }
1810 if (self.site_detail_scopes.getPtr(scope)) |counters| {
1811 applyCountersLostTracking(counters, len);
1812 }
1813 }
1814
1815 fn siteDetailSizeCounters(self: *Analyzer, len: usize) !*SiteCounters {
1816 const entry = try self.site_detail_sizes.getOrPut(self.allocator, len);
1817 if (!entry.found_existing) entry.value_ptr.* = .{};
1818 return entry.value_ptr;
1819 }
1820
1821 fn siteDetailScopeCounters(self: *Analyzer, scope: []const u8) !*SiteCounters {
1822 const entry = try self.site_detail_scopes.getOrPut(self.allocator, scope);
1823 if (!entry.found_existing) entry.value_ptr.* = .{};
1824 return entry.value_ptr;
1825 }
1826
1827 fn applyDetailAllocation(
1828 counters: *SiteCounters,
1829 len: usize,
1830 tracks_retained: bool,
1831 ) void {
1832 counters.allocations += 1;
1833 counters.live_allocations += 1;
1834 counters.allocated_bytes += len;
1835 counters.live_bytes += len;
1836 counters.high_water_live_bytes = @max(counters.high_water_live_bytes, counters.live_bytes);
1837 if (tracks_retained) {
1838 counters.retained_bytes += len;
1839 counters.high_water_retained_bytes = @max(
1840 counters.high_water_retained_bytes,
1841 counters.retained_bytes,
1842 );
1843 }
1844 }
1845
1846 fn applyDetailFree(counters: *SiteCounters, len: usize, releases: bool) void {
1847 counters.frees += 1;
1848 if (counters.live_allocations > 0) counters.live_allocations -= 1;
1849 counters.freed_bytes += len;
1850 if (counters.live_bytes >= len) counters.live_bytes -= len else counters.live_bytes = 0;
1851 if (releases and counters.retained_bytes >= len) counters.retained_bytes -= len;
1852 }
1853
1854 fn applyDetailBulkInvalidation(
1855 counters: *SiteCounters,
1856 len: usize,
1857 ) void {
1858 counters.bulk_invalidated_requests +|= 1;
1859 counters.bulk_invalidated_bytes +|= len;
1860 if (counters.live_allocations > 0) counters.live_allocations -= 1;
1861 if (counters.live_bytes >= len) {
1862 counters.live_bytes -= len;
1863 } else {
1864 counters.live_bytes = 0;
1865 }
1866 }
1867
1868 fn writeSiteSummary(self: *Analyzer, writer: *std.Io.Writer, options: SummaryOptions) !void {
1869 var summaries = try self.collectSiteSummaries(options);
1870 defer summaries.deinit(self.allocator);
1871 const limit = @min(options.top, summaries.items.len);
1872 var maybe_symbols = if (options.site_symbol_binary) |binary|
1873 try resolveSiteSummarySymbolsAlloc(
1874 self.allocator,
1875 binary,
1876 summaries.items[0..limit],
1877 )
1878 else
1879 null;
1880 defer if (maybe_symbols) |*symbols| symbols.deinit(self.allocator);
1881 for (summaries.items[0..limit]) |summary| {
1882 const symbol = symbol: {
1883 const symbols = if (maybe_symbols) |*value|
1884 value
1885 else
1886 break :symbol null;
1887 const frames = symbols.find(summary.return_address);
1888 break :symbol if (frames.len == 0) null else frames[0];
1889 };
1890 try writer.print(
1891 "site return_address=0x{x}",
1892 .{summary.return_address},
1893 );
1894 if (symbol) |resolved| {
1895 try writer.writeAll(" symbol=");
1896 var symbol_stream = pretty_json.Writer.init(writer, .minified);
1897 try symbol_stream.write(resolved.function);
1898 try writer.writeAll(" location=");
1899 var location_stream = pretty_json.Writer.init(writer, .minified);
1900 try location_stream.write(resolved.location);
1901 }
1902 try writer.print(
1903 " retained_bytes={d} high_water_retained_bytes={d} live_bytes={d} high_water_live_bytes={d} allocated_bytes={d} freed_bytes={d} allocations={d} frees={d} live_allocations={d} completed_lifetimes={d} lifetime_total_events={d} lifetime_mean_events={d} lifetime_max_events={d} lifetime_total_byte_events={d} lifetime_mean_byte_events={d} lifetime_max_byte_events={d}\n",
1904 .{
1905 summary.counters.retained_bytes,
1906 summary.counters.high_water_retained_bytes,
1907 summary.counters.live_bytes,
1908 summary.counters.high_water_live_bytes,
1909 summary.counters.allocated_bytes,
1910 summary.counters.freed_bytes,
1911 summary.counters.allocations,
1912 summary.counters.frees,
1913 summary.counters.live_allocations,
1914 summary.counters.completed_lifetimes,
1915 summary.counters.lifetime_total_events,
1916 meanLifetimeEvents(summary.counters),
1917 summary.counters.lifetime_max_events,
1918 summary.counters.lifetime_total_byte_events,
1919 meanLifetimeByteEvents(summary.counters),
1920 summary.counters.lifetime_max_byte_events,
1921 },
1922 );
1923 }
1924 }
1925
1926 fn writeSiteDetailSummary(self: *Analyzer, writer: *std.Io.Writer, options: SummaryOptions, return_address: u64) !void {
1927 try writer.print("site_detail return_address=0x{x}\n", .{return_address});
1928
1929 var sizes = try self.collectSiteSizeSummaries(options.sort);
1930 defer sizes.deinit(self.allocator);
1931 const size_limit = @min(options.top, sizes.items.len);
1932 for (sizes.items[0..size_limit]) |summary| {
1933 try writer.print(
1934 "site_size len={d} retained_bytes={d} high_water_retained_bytes={d} live_bytes={d} high_water_live_bytes={d} allocated_bytes={d} freed_bytes={d} allocations={d} frees={d} live_allocations={d} completed_lifetimes={d} lifetime_total_events={d} lifetime_mean_events={d} lifetime_max_events={d} lifetime_total_byte_events={d} lifetime_mean_byte_events={d} lifetime_max_byte_events={d}\n",
1935 .{
1936 summary.len,
1937 summary.counters.retained_bytes,
1938 summary.counters.high_water_retained_bytes,
1939 summary.counters.live_bytes,
1940 summary.counters.high_water_live_bytes,
1941 summary.counters.allocated_bytes,
1942 summary.counters.freed_bytes,
1943 summary.counters.allocations,
1944 summary.counters.frees,
1945 summary.counters.live_allocations,
1946 summary.counters.completed_lifetimes,
1947 summary.counters.lifetime_total_events,
1948 meanLifetimeEvents(summary.counters),
1949 summary.counters.lifetime_max_events,
1950 summary.counters.lifetime_total_byte_events,
1951 meanLifetimeByteEvents(summary.counters),
1952 summary.counters.lifetime_max_byte_events,
1953 },
1954 );
1955 }
1956
1957 var scopes = try self.collectSiteScopeSummaries(options.sort);
1958 defer scopes.deinit(self.allocator);
1959 const scope_limit = @min(options.top, scopes.items.len);
1960 for (scopes.items[0..scope_limit]) |summary| {
1961 try writer.writeAll("site_scope scope=");
1962 var stream = pretty_json.Writer.init(writer, .minified);
1963 try stream.write(summary.scope);
1964 try writer.print(
1965 " retained_bytes={d} high_water_retained_bytes={d} live_bytes={d} high_water_live_bytes={d} allocated_bytes={d} freed_bytes={d} allocations={d} frees={d} live_allocations={d} completed_lifetimes={d} lifetime_total_events={d} lifetime_mean_events={d} lifetime_max_events={d} lifetime_total_byte_events={d} lifetime_mean_byte_events={d} lifetime_max_byte_events={d}\n",
1966 .{
1967 summary.counters.retained_bytes,
1968 summary.counters.high_water_retained_bytes,
1969 summary.counters.live_bytes,
1970 summary.counters.high_water_live_bytes,
1971 summary.counters.allocated_bytes,
1972 summary.counters.freed_bytes,
1973 summary.counters.allocations,
1974 summary.counters.frees,
1975 summary.counters.live_allocations,
1976 summary.counters.completed_lifetimes,
1977 summary.counters.lifetime_total_events,
1978 meanLifetimeEvents(summary.counters),
1979 summary.counters.lifetime_max_events,
1980 summary.counters.lifetime_total_byte_events,
1981 meanLifetimeByteEvents(summary.counters),
1982 summary.counters.lifetime_max_byte_events,
1983 },
1984 );
1985 }
1986 }
1987
1988 fn writeSiteSummaryJsonl(self: *Analyzer, writer: *std.Io.Writer, options: SummaryOptions) !void {
1989 var summaries = try self.collectSiteSummaries(options);
1990 defer summaries.deinit(self.allocator);
1991 const limit = @min(options.top, summaries.items.len);
1992 var maybe_symbols = if (options.site_symbol_binary) |binary|
1993 try resolveSiteSummarySymbolsAlloc(
1994 self.allocator,
1995 binary,
1996 summaries.items[0..limit],
1997 )
1998 else
1999 null;
2000 defer if (maybe_symbols) |*symbols| symbols.deinit(self.allocator);
2001 for (summaries.items[0..limit]) |summary| {
2002 const symbol = symbol: {
2003 const symbols = if (maybe_symbols) |*value|
2004 value
2005 else
2006 break :symbol null;
2007 const frames = symbols.find(summary.return_address);
2008 break :symbol if (frames.len == 0) null else frames[0];
2009 };
2010 var stream = pretty_json.Writer.init(writer, .minified);
2011 const object = try stream.object();
2012 try object.field("kind", "site");
2013 try object.field("sort", options.sort.name());
2014 try object.field("return_address", summary.return_address);
2015 if (symbol) |resolved| {
2016 try object.field("symbol", resolved.function);
2017 try object.field("location", resolved.location);
2018 }
2019 try writeCounterFields(
2020 object,
2021 summary.counters,
2022 options.layer == .logical,
2023 );
2024 try object.endLine();
2025 }
2026 }
2027
2028 fn writeSiteDetailSummaryJsonl(self: *Analyzer, writer: *std.Io.Writer, options: SummaryOptions, return_address: u64) !void {
2029 var detail_stream = pretty_json.Writer.init(writer, .minified);
2030 const detail = try detail_stream.object();
2031 try detail.field("kind", "site_detail");
2032 try detail.field("sort", options.sort.name());
2033 try detail.field("return_address", return_address);
2034 try detail.endLine();
2035
2036 var sizes = try self.collectSiteSizeSummaries(options.sort);
2037 defer sizes.deinit(self.allocator);
2038 const size_limit = @min(options.top, sizes.items.len);
2039 for (sizes.items[0..size_limit]) |summary| {
2040 var stream = pretty_json.Writer.init(writer, .minified);
2041 const object = try stream.object();
2042 try object.field("kind", "site_size");
2043 try object.field("sort", options.sort.name());
2044 try object.field("return_address", return_address);
2045 try object.field("len", summary.len);
2046 try writeCounterFields(
2047 object,
2048 summary.counters,
2049 options.layer == .logical,
2050 );
2051 try object.endLine();
2052 }
2053
2054 var scopes = try self.collectSiteScopeSummaries(options.sort);
2055 defer scopes.deinit(self.allocator);
2056 const scope_limit = @min(options.top, scopes.items.len);
2057 for (scopes.items[0..scope_limit]) |summary| {
2058 var stream = pretty_json.Writer.init(writer, .minified);
2059 const object = try stream.object();
2060 try object.field("kind", "site_scope");
2061 try object.field("sort", options.sort.name());
2062 try object.field("return_address", return_address);
2063 try object.field("scope", summary.scope);
2064 try writeCounterFields(
2065 object,
2066 summary.counters,
2067 options.layer == .logical,
2068 );
2069 try object.endLine();
2070 }
2071 }
2072
2073 fn collectSiteSummaries(self: *Analyzer, options: SummaryOptions) !std.ArrayListUnmanaged(SiteSummary) {
2074 var summaries = std.ArrayListUnmanaged(SiteSummary).empty;
2075 var iterator = self.sites.iterator();
2076 while (iterator.next()) |entry| {
2077 const counters = entry.value_ptr.*;
2078 if (!options.include_zero_live and counters.live_bytes == 0 and counters.high_water_live_bytes == 0 and counters.retained_bytes == 0) continue;
2079 if (counters.live_bytes < options.min_bytes and counters.high_water_live_bytes < options.min_bytes and counters.retained_bytes < options.min_bytes) continue;
2080 try summaries.append(self.allocator, .{
2081 .return_address = entry.key_ptr.*,
2082 .counters = counters,
2083 });
2084 }
2085 std.mem.sort(
2086 SiteSummary,
2087 summaries.items,
2088 options.sort,
2089 siteSummaryGreaterThan,
2090 );
2091 return summaries;
2092 }
2093
2094 fn collectSiteSizeSummaries(
2095 self: *Analyzer,
2096 sort: Sort,
2097 ) !std.ArrayListUnmanaged(SiteSizeSummary) {
2098 var summaries = std.ArrayListUnmanaged(SiteSizeSummary).empty;
2099 var iterator = self.site_detail_sizes.iterator();
2100 while (iterator.next()) |entry| {
2101 try summaries.append(self.allocator, .{
2102 .len = entry.key_ptr.*,
2103 .counters = entry.value_ptr.*,
2104 });
2105 }
2106 std.mem.sort(
2107 SiteSizeSummary,
2108 summaries.items,
2109 sort,
2110 siteSizeSummaryGreaterThan,
2111 );
2112 return summaries;
2113 }
2114
2115 fn collectSiteScopeSummaries(
2116 self: *Analyzer,
2117 sort: Sort,
2118 ) !std.ArrayListUnmanaged(SiteScopeSummary) {
2119 var summaries = std.ArrayListUnmanaged(SiteScopeSummary).empty;
2120 var iterator = self.site_detail_scopes.iterator();
2121 while (iterator.next()) |entry| {
2122 try summaries.append(self.allocator, .{
2123 .scope = entry.key_ptr.*,
2124 .counters = entry.value_ptr.*,
2125 });
2126 }
2127 std.mem.sort(
2128 SiteScopeSummary,
2129 summaries.items,
2130 sort,
2131 siteScopeSummaryGreaterThan,
2132 );
2133 return summaries;
2134 }
2135
2136 fn allocatorReleases(self: *Analyzer, allocator_id: u32) bool {
2137 const state = self.allocators.get(allocator_id) orelse return true;
2138 return state.retention == .releases_freed_memory;
2139 }
2140
2141 fn allocatorLayer(
2142 self: *Analyzer,
2143 allocator_id: u32,
2144 ) event_mod.Layer {
2145 const state = self.allocators.get(allocator_id) orelse
2146 return .backing_boundary;
2147 return state.layer;
2148 }
2149
2150 fn lifecycleCoverage(self: *Analyzer) LifecycleCoverage {
2151 var coverage: LifecycleCoverage = .{};
2152 var allocators = self.allocators.valueIterator();
2153 while (allocators.next()) |allocator| {
2154 if (allocator.layer != .logical_allocator) continue;
2155 if (allocator.retention == .releases_freed_memory) {
2156 coverage.not_required +|= 1;
2157 } else if (allocator.lifecycle_instrumented) {
2158 coverage.instrumented +|= 1;
2159 } else {
2160 coverage.uninstrumented +|= 1;
2161 }
2162 if (allocator.prefix_complete) {
2163 coverage.prefix_complete +|= 1;
2164 } else {
2165 coverage.prefix_partial +|= 1;
2166 }
2167 }
2168 return coverage;
2169 }
2170
2171 fn clearDrainedAllocations(self: *Analyzer) void {
2172 if (self.allocations.count() == 0) self.allocations.clearRetainingCapacity();
2173 }
2174
2175 fn internScope(self: *Analyzer, scope: []const u8) ![]const u8 {
2176 const entry = try self.scopes.getOrPut(self.allocator, scope);
2177 if (!entry.found_existing) {
2178 const owned = try self.allocator.dupe(u8, scope);
2179 entry.key_ptr.* = owned;
2180 entry.value_ptr.* = .{};
2181 }
2182 return entry.key_ptr.*;
2183 }
2184
2185 fn resolveScope(
2186 self: *const Analyzer,
2187 parsed: ParsedEvent,
2188 ) ![]const u8 {
2189 if (parsed.scope.len != 0) return parsed.scope;
2190 if (parsed.scope_id == 0) return "root";
2191 return self.scope_paths.get(parsed.scope_id) orelse
2192 error.MissingScopeDefinition;
2193 }
2194 };
2195
2196 fn summarySnapshot(analyzer: *const Analyzer) Summary {
2197 const counters = analyzer.counters;
2198 return .{
2199 .events = analyzer.events,
2200 .allocations = counters.allocations,
2201 .frees = counters.frees,
2202 .resizes = counters.resizes,
2203 .remaps = counters.remaps,
2204 .live_allocations = @intCast(counters.live_allocations),
2205 .allocated_bytes = @intCast(counters.allocated_bytes),
2206 .freed_bytes = @intCast(counters.freed_bytes),
2207 .live_bytes = @intCast(counters.live_bytes),
2208 .high_water_live_bytes = @intCast(counters.high_water_live_bytes),
2209 .retained_bytes = @intCast(counters.retained_bytes),
2210 .high_water_retained_bytes = @intCast(
2211 counters.high_water_retained_bytes,
2212 ),
2213 .completed_lifetimes = counters.completed_lifetimes,
2214 .lifetime_total_events = counters.lifetime_total_events,
2215 .lifetime_mean_events = meanLifetimeEvents(counters),
2216 .lifetime_max_events = counters.lifetime_max_events,
2217 .lifetime_total_byte_events = counters.lifetime_total_byte_events,
2218 .lifetime_mean_byte_events = meanLifetimeByteEvents(counters),
2219 .lifetime_max_byte_events = counters.lifetime_max_byte_events,
2220 .failed_allocations = counters.failed_allocations,
2221 .failed_resizes = counters.failed_resizes,
2222 .failed_remaps = counters.failed_remaps,
2223 .unmatched_frees = counters.unmatched_frees,
2224 .unmatched_resizes = counters.unmatched_resizes,
2225 };
2226 }
2227
2228 fn snapshotScopes(
2229 allocator: Allocator,
2230 summaries: []const ScopeSummary,
2231 ) ![]Scope {
2232 if (summaries.len == 0) return &.{};
2233 const result = try allocator.alloc(Scope, summaries.len);
2234 var initialized: usize = 0;
2235 errdefer {
2236 for (result[0..initialized]) |scope| allocator.free(scope.scope);
2237 allocator.free(result);
2238 }
2239 for (summaries, result) |summary, *scope| {
2240 scope.* = scopeSnapshot(
2241 try allocator.dupe(u8, summary.path),
2242 summary.counters,
2243 );
2244 initialized += 1;
2245 }
2246 return result;
2247 }
2248
2249 fn snapshotSources(
2250 allocator: Allocator,
2251 summaries: []const SiteSummary,
2252 maybe_symbols: ?*const stack_mod.symbolize.Symbols,
2253 ) ![]Source {
2254 if (summaries.len == 0) return &.{};
2255 const result = try allocator.alloc(Source, summaries.len);
2256 errdefer allocator.free(result);
2257 for (summaries, result) |summary, *source| {
2258 const symbol = symbol: {
2259 const symbols = maybe_symbols orelse break :symbol null;
2260 const frames = symbols.find(summary.return_address);
2261 break :symbol if (frames.len == 0) null else frames[0];
2262 };
2263 source.* = sourceSnapshot(
2264 summary.return_address,
2265 if (symbol) |resolved| resolved.function else null,
2266 if (symbol) |resolved| resolved.location else null,
2267 summary.counters,
2268 );
2269 }
2270 return result;
2271 }
2272
2273 fn scopeSnapshot(scope: []const u8, counters: anytype) Scope {
2274 return .{
2275 .scope = scope,
2276 .retained_bytes = @intCast(counters.retained_bytes),
2277 .high_water_retained_bytes = @intCast(
2278 counters.high_water_retained_bytes,
2279 ),
2280 .live_bytes = @intCast(counters.live_bytes),
2281 .high_water_live_bytes = @intCast(counters.high_water_live_bytes),
2282 .allocated_bytes = @intCast(counters.allocated_bytes),
2283 .freed_bytes = @intCast(counters.freed_bytes),
2284 .allocations = counters.allocations,
2285 .frees = counters.frees,
2286 .live_allocations = @intCast(counters.live_allocations),
2287 .completed_lifetimes = counters.completed_lifetimes,
2288 .lifetime_total_events = counters.lifetime_total_events,
2289 .lifetime_mean_events = meanLifetimeEvents(counters),
2290 .lifetime_max_events = counters.lifetime_max_events,
2291 .lifetime_total_byte_events = counters.lifetime_total_byte_events,
2292 .lifetime_mean_byte_events = meanLifetimeByteEvents(counters),
2293 .lifetime_max_byte_events = counters.lifetime_max_byte_events,
2294 };
2295 }
2296
2297 fn sourceSnapshot(
2298 return_address: u64,
2299 function: ?[]const u8,
2300 location: ?[]const u8,
2301 counters: anytype,
2302 ) Source {
2303 const scope = scopeSnapshot("", counters);
2304 return .{
2305 .return_address = return_address,
2306 .function = function,
2307 .location = location,
2308 .retained_bytes = scope.retained_bytes,
2309 .high_water_retained_bytes = scope.high_water_retained_bytes,
2310 .live_bytes = scope.live_bytes,
2311 .high_water_live_bytes = scope.high_water_live_bytes,
2312 .allocated_bytes = scope.allocated_bytes,
2313 .freed_bytes = scope.freed_bytes,
2314 .allocations = scope.allocations,
2315 .frees = scope.frees,
2316 .live_allocations = scope.live_allocations,
2317 .completed_lifetimes = scope.completed_lifetimes,
2318 .lifetime_total_events = scope.lifetime_total_events,
2319 .lifetime_mean_events = scope.lifetime_mean_events,
2320 .lifetime_max_events = scope.lifetime_max_events,
2321 .lifetime_total_byte_events = scope.lifetime_total_byte_events,
2322 .lifetime_mean_byte_events = scope.lifetime_mean_byte_events,
2323 .lifetime_max_byte_events = scope.lifetime_max_byte_events,
2324 };
2325 }
2326
2327 fn classifyCaptureIntegrity(result: *CaptureIntegrity) void {
2328 if (result.event_count == 0) return setIntegrity(
2329 result,
2330 "no_events",
2331 "capture_trace_events",
2332 "memtrace contains no events",
2333 );
2334 if (result.sequence_regression_count != 0) return setIntegrity(
2335 result,
2336 "non_monotonic_sequence",
2337 "inspect_trace_writer",
2338 "memtrace sequence is non-monotonic; treat the summary as corrupt evidence",
2339 );
2340 if (result.missing_sequence_event_count != 0) return setIntegrity(
2341 result,
2342 "sequence_gaps",
2343 "inspect_recorder_capacity_or_writer_failures",
2344 "memtrace sequence has gaps; treat the summary as partial evidence",
2345 );
2346 if (result.unsequenced_event_count != 0) return setIntegrity(
2347 result,
2348 "missing_sequence_metadata",
2349 "recapture_with_sequence_metadata",
2350 "one or more memtrace events lack sequence metadata; completeness is unknown",
2351 );
2352 classifyCaptureLifecycle(result);
2353 }
2354
2355 fn classifyCaptureLifecycle(result: *CaptureIntegrity) void {
2356 if (result.start_event_count > 1 or result.stop_event_count > 1) {
2357 return setIntegrity(
2358 result,
2359 "multiple_trace_sessions",
2360 "capture_one_trace_session",
2361 "memtrace mixes multiple trace lifecycles; completeness is ambiguous",
2362 );
2363 }
2364 if (result.start_event_count == 0) return setIntegrity(
2365 result,
2366 "missing_start_event",
2367 "capture_complete_trace_lifecycle",
2368 "memtrace does not contain its start event; treat it as partial evidence",
2369 );
2370 if (result.stop_event_count == 0) return setIntegrity(
2371 result,
2372 "missing_stop_event",
2373 "capture_complete_trace_lifecycle",
2374 "memtrace does not contain its stop event; terminal state is partial evidence",
2375 );
2376 if (result.start_sequence != result.first_sequence or
2377 result.stop_sequence != result.last_sequence)
2378 {
2379 return setIntegrity(
2380 result,
2381 "lifecycle_not_bounded",
2382 "capture_complete_trace_lifecycle",
2383 "memtrace start and stop events do not bound the event sequence",
2384 );
2385 }
2386 if (result.unbalanced_event_count != 0) setIntegrity(
2387 result,
2388 "unbalanced_events",
2389 "inspect_allocation_and_scope_lifecycles",
2390 "memtrace contains unmatched allocation or scope lifecycle events",
2391 );
2392 }
2393
2394 fn setIntegrity(
2395 result: *CaptureIntegrity,
2396 status: []const u8,
2397 action: []const u8,
2398 message: []const u8,
2399 ) void {
2400 result.status = status;
2401 result.action = action;
2402 result.message = message;
2403 }
2404
2405 fn writeCounterFields(
2406 object: pretty_json.Object,
2407 counters: anytype,
2408 logical: bool,
2409 ) !void {
2410 if (logical) {
2411 try object.field("open_request_bytes", counters.live_bytes);
2412 try object.field(
2413 "high_water_open_request_bytes",
2414 counters.high_water_live_bytes,
2415 );
2416 try object.field(
2417 "bulk_invalidated_requests",
2418 counters.bulk_invalidated_requests,
2419 );
2420 try object.field(
2421 "bulk_invalidated_bytes",
2422 counters.bulk_invalidated_bytes,
2423 );
2424 try object.field("untracked_requests", counters.untracked_requests);
2425 try object.field(
2426 "untracked_request_bytes",
2427 counters.untracked_request_bytes,
2428 );
2429 } else {
2430 try object.field("retained_bytes", counters.retained_bytes);
2431 try object.field(
2432 "high_water_retained_bytes",
2433 counters.high_water_retained_bytes,
2434 );
2435 try object.field("live_bytes", counters.live_bytes);
2436 try object.field(
2437 "high_water_live_bytes",
2438 counters.high_water_live_bytes,
2439 );
2440 }
2441 try object.field("allocated_bytes", counters.allocated_bytes);
2442 if (logical) {
2443 try object.field("requested_bytes", counters.allocated_bytes);
2444 try object.field(
2445 "explicitly_closed_bytes",
2446 counters.freed_bytes,
2447 );
2448 } else {
2449 try object.field("freed_bytes", counters.freed_bytes);
2450 }
2451 try object.field("allocations", counters.allocations);
2452 try object.field("frees", counters.frees);
2453 if (logical) {
2454 try object.field("open_requests", counters.live_allocations);
2455 } else {
2456 try object.field("live_allocations", counters.live_allocations);
2457 }
2458 try object.field("completed_lifetimes", counters.completed_lifetimes);
2459 try object.field("lifetime_total_events", counters.lifetime_total_events);
2460 try object.field("lifetime_mean_events", meanLifetimeEvents(counters));
2461 try object.field("lifetime_max_events", counters.lifetime_max_events);
2462 try object.field(
2463 "lifetime_total_byte_events",
2464 counters.lifetime_total_byte_events,
2465 );
2466 try object.field(
2467 "lifetime_mean_byte_events",
2468 meanLifetimeByteEvents(counters),
2469 );
2470 try object.field(
2471 "lifetime_max_byte_events",
2472 counters.lifetime_max_byte_events,
2473 );
2474 }
2475
2476 fn writeCaptureIntegrityText(
2477 writer: *std.Io.Writer,
2478 integrity: CaptureIntegrity,
2479 ) !void {
2480 try writer.print(
2481 "memtrace capture_integrity={s} events={d} sequenced_events={d} " ++
2482 "unsequenced_events={d} sequence_gaps={d} missing_sequence_events={d} " ++
2483 "sequence_regressions={d} start_events={d} stop_events={d} " ++
2484 "unbalanced_events={d}",
2485 .{
2486 integrity.status,
2487 integrity.event_count,
2488 integrity.sequenced_event_count,
2489 integrity.unsequenced_event_count,
2490 integrity.sequence_gap_count,
2491 integrity.missing_sequence_event_count,
2492 integrity.sequence_regression_count,
2493 integrity.start_event_count,
2494 integrity.stop_event_count,
2495 integrity.unbalanced_event_count,
2496 },
2497 );
2498 try writer.writeAll(" first_sequence=");
2499 try writeOptionalU64Text(writer, integrity.first_sequence);
2500 try writer.writeAll(" last_sequence=");
2501 try writeOptionalU64Text(writer, integrity.last_sequence);
2502 try writer.writeAll(" start_sequence=");
2503 try writeOptionalU64Text(writer, integrity.start_sequence);
2504 try writer.writeAll(" stop_sequence=");
2505 try writeOptionalU64Text(writer, integrity.stop_sequence);
2506 try writer.writeByte('\n');
2507 const message = integrity.message orelse return;
2508 try writer.print(
2509 "memtrace capture caveat={s} action={s} message=",
2510 .{ integrity.status, integrity.action },
2511 );
2512 var stream = pretty_json.Writer.init(writer, .minified);
2513 try stream.write(message);
2514 try writer.writeByte('\n');
2515 }
2516
2517 fn writeCaptureIntegrityJson(
2518 object: pretty_json.Object,
2519 integrity: CaptureIntegrity,
2520 ) !void {
2521 const capture = try object.object("capture_integrity");
2522 try capture.field("method", capture_integrity_method);
2523 try capture.field("status", integrity.status);
2524 try capture.field("action", integrity.action);
2525 try capture.field("message", integrity.message);
2526 try capture.field("event_count", integrity.event_count);
2527 try capture.field("sequenced_event_count", integrity.sequenced_event_count);
2528 try capture.field("unsequenced_event_count", integrity.unsequenced_event_count);
2529 try capture.field("first_sequence", integrity.first_sequence);
2530 try capture.field("last_sequence", integrity.last_sequence);
2531 try capture.field("sequence_gap_count", integrity.sequence_gap_count);
2532 try capture.field("missing_sequence_event_count", integrity.missing_sequence_event_count);
2533 try capture.field("sequence_regression_count", integrity.sequence_regression_count);
2534 try capture.field("start_event_count", integrity.start_event_count);
2535 try capture.field("stop_event_count", integrity.stop_event_count);
2536 try capture.field("unbalanced_event_count", integrity.unbalanced_event_count);
2537 try capture.field("start_sequence", integrity.start_sequence);
2538 try capture.field("stop_sequence", integrity.stop_sequence);
2539 const limits = try capture.array("limits");
2540 try limits.element(
2541 "sequence gaps are lower-bound missing-row evidence",
2542 );
2543 try limits.element(
2544 "complete bounds do not prove whole-process coverage, low perturbation, or " ++
2545 "representative workload coverage",
2546 );
2547 try limits.end();
2548 try capture.end();
2549 }
2550
2551 fn writeOptionalU64Text(writer: *std.Io.Writer, value: ?u64) !void {
2552 if (value) |actual| try writer.print("{d}", .{actual}) else try writer.writeAll("none");
2553 }
2554
2555 pub fn writeSummaryFromJsonlPath(allocator: Allocator, path: []const u8, writer: *std.Io.Writer, options: SummaryOptions) !void {
2556 var analyzer = Analyzer.initForLayer(allocator, options.layer);
2557 defer analyzer.deinit();
2558 analyzer.track_sites = options.include_sites;
2559 analyzer.site_detail_return_address = options.site_detail_return_address;
2560 try ingestPath(&analyzer, path);
2561 try analyzer.writeSummary(writer, options);
2562 }
2563
2564 pub fn writeSummaryJsonlFromJsonlPath(
2565 allocator: Allocator,
2566 path: []const u8,
2567 writer: *std.Io.Writer,
2568 options: SummaryOptions,
2569 ) !CaptureIntegrity {
2570 var analyzer = Analyzer.initForLayer(allocator, options.layer);
2571 defer analyzer.deinit();
2572 analyzer.track_sites = options.include_sites;
2573 analyzer.site_detail_return_address = options.site_detail_return_address;
2574 try ingestPath(&analyzer, path);
2575 try analyzer.writeSummaryJsonl(writer, options);
2576 return analyzer.captureIntegrity();
2577 }
2578
2579 fn ingestPath(analyzer: *Analyzer, path: []const u8) !void {
2580 var file = try sys.fs.cwd().openFile(sys.fs.debugIo(), path, .{});
2581 defer file.close(sys.fs.debugIo());
2582
2583 var buffer: [64 * 1024]u8 = undefined;
2584 var reader = file.reader(sys.fs.debugIo(), &buffer);
2585 while (true) {
2586 const line = reader.interface.takeDelimiter('\n') catch |err| switch (err) {
2587 error.ReadFailed => return reader.err.?,
2588 else => return err,
2589 };
2590 const actual = line orelse break;
2591 try analyzer.ingestJsonLine(actual);
2592 }
2593 }
2594
2595 fn allocationKey(allocation_id: u64, address: u64) u64 {
2596 if (allocation_id != 0) return allocation_id;
2597 return address;
2598 }
2599
2600 fn completedLifetimeEvents(allocation_event: u64, free_event: u64) u64 {
2601 if (free_event <= allocation_event) return 0;
2602 return free_event - allocation_event;
2603 }
2604
2605 fn completedLifetimeByteEvents(
2606 len: usize,
2607 allocation_event: u64,
2608 free_event: u64,
2609 ) u64 {
2610 const events = completedLifetimeEvents(allocation_event, free_event);
2611 return @as(u64, @intCast(len)) *| events;
2612 }
2613
2614 fn accrueLifetimeByteEvents(record: *AllocationRecord, event: u64) void {
2615 record.byte_events +|= completedLifetimeByteEvents(
2616 record.len,
2617 record.segment_event,
2618 event,
2619 );
2620 record.segment_event = event;
2621 }
2622
2623 fn applyCompletedLifetime(
2624 counters: anytype,
2625 lifetime_events: u64,
2626 lifetime_byte_events: u64,
2627 ) void {
2628 counters.completed_lifetimes +|= 1;
2629 counters.lifetime_total_events +|= lifetime_events;
2630 counters.lifetime_max_events = @max(counters.lifetime_max_events, lifetime_events);
2631 counters.lifetime_total_byte_events +|= lifetime_byte_events;
2632 counters.lifetime_max_byte_events = @max(
2633 counters.lifetime_max_byte_events,
2634 lifetime_byte_events,
2635 );
2636 }
2637
2638 fn applyCountersLostTracking(counters: anytype, len: usize) void {
2639 if (counters.live_allocations > 0) counters.live_allocations -= 1;
2640 if (counters.live_bytes >= len) {
2641 counters.live_bytes -= len;
2642 } else {
2643 counters.live_bytes = 0;
2644 }
2645 counters.untracked_requests +|= 1;
2646 counters.untracked_request_bytes +|= len;
2647 }
2648
2649 fn meanLifetimeEvents(counters: anytype) u64 {
2650 if (counters.completed_lifetimes == 0) return 0;
2651 return counters.lifetime_total_events / counters.completed_lifetimes;
2652 }
2653
2654 fn meanLifetimeByteEvents(counters: anytype) u64 {
2655 if (counters.completed_lifetimes == 0) return 0;
2656 return counters.lifetime_total_byte_events / counters.completed_lifetimes;
2657 }
2658
2659 test "escaped canonical scope uses generic replay" {
2660 var bytes = std.Io.Writer.Allocating.init(std.testing.allocator);
2661 defer bytes.deinit();
2662 try (event_mod.Event{
2663 .seq = 1,
2664 .kind = .alloc,
2665 .allocation_id = 1,
2666 .address = 4096,
2667 .len = 32,
2668 }).writeJsonLine(&bytes.writer, null, "root/quoted\"scope");
2669
2670 var analyzer = Analyzer.init(std.testing.allocator);
2671 defer analyzer.deinit();
2672 try analyzer.ingestJsonlBytes(bytes.written());
2673
2674 var summary = std.Io.Writer.Allocating.init(std.testing.allocator);
2675 defer summary.deinit();
2676 try analyzer.writeSummary(&summary.writer, .{});
2677 try std.testing.expect(std.mem.indexOf(u8, summary.written(), "root/quoted\"scope retained_bytes=32") != null);
2678 }
2679
2680 test "event analysis resolves scope ids from scope entry events" {
2681 const events =
2682 "{\"v\":3,\"seq\":1,\"kind\":\"trace.start\"}\n" ++
2683 "{\"v\":3,\"seq\":2,\"kind\":\"scope.enter\",\"scope_id\":1," ++
2684 "\"scope\":\"root/phase\"}\n" ++
2685 "{\"v\":3,\"seq\":3,\"kind\":\"alloc\",\"allocation_id\":1," ++
2686 "\"scope_id\":1,\"address\":4096,\"len\":32}\n" ++
2687 "{\"v\":3,\"seq\":4,\"kind\":\"free\",\"allocation_id\":1," ++
2688 "\"scope_id\":1,\"address\":4096,\"old_len\":32}\n" ++
2689 "{\"v\":3,\"seq\":5,\"kind\":\"scope.exit\",\"scope_id\":1}\n" ++
2690 "{\"v\":3,\"seq\":6,\"kind\":\"trace.stop\"}\n";
2691 var analyzer = Analyzer.init(std.testing.allocator);
2692 defer analyzer.deinit();
2693 try analyzer.ingestJsonlBytes(events);
2694
2695 var summary = std.Io.Writer.Allocating.init(std.testing.allocator);
2696 defer summary.deinit();
2697 try analyzer.writeSummary(
2698 &summary.writer,
2699 .{ .include_zero_live = true },
2700 );
2701 try std.testing.expect(
2702 std.mem.indexOf(
2703 u8,
2704 summary.written(),
2705 "root/phase retained_bytes=0 high_water_retained_bytes=32",
2706 ) != null,
2707 );
2708 }
2709
2710 fn scopeSummaryGreaterThan(
2711 sort: Sort,
2712 left: ScopeSummary,
2713 right: ScopeSummary,
2714 ) bool {
2715 if (counterOrder(sort, left.counters, right.counters)) |order| return order;
2716 return std.mem.lessThan(u8, left.path, right.path);
2717 }
2718
2719 fn siteSummaryGreaterThan(
2720 sort: Sort,
2721 left: SiteSummary,
2722 right: SiteSummary,
2723 ) bool {
2724 if (counterOrder(sort, left.counters, right.counters)) |order| return order;
2725 return left.return_address < right.return_address;
2726 }
2727
2728 fn siteSizeSummaryGreaterThan(
2729 sort: Sort,
2730 left: SiteSizeSummary,
2731 right: SiteSizeSummary,
2732 ) bool {
2733 if (counterOrder(sort, left.counters, right.counters)) |order| return order;
2734 return left.len < right.len;
2735 }
2736
2737 fn siteScopeSummaryGreaterThan(
2738 sort: Sort,
2739 left: SiteScopeSummary,
2740 right: SiteScopeSummary,
2741 ) bool {
2742 if (counterOrder(sort, left.counters, right.counters)) |order| return order;
2743 return std.mem.lessThan(u8, left.scope, right.scope);
2744 }
2745
2746 fn counterOrder(sort: Sort, left: anytype, right: @TypeOf(left)) ?bool {
2747 switch (sort) {
2748 .retained => {
2749 if (left.retained_bytes != right.retained_bytes) {
2750 return left.retained_bytes > right.retained_bytes;
2751 }
2752 if (left.high_water_retained_bytes !=
2753 right.high_water_retained_bytes)
2754 {
2755 return left.high_water_retained_bytes >
2756 right.high_water_retained_bytes;
2757 }
2758 },
2759 .traffic => {
2760 if (left.allocated_bytes != right.allocated_bytes) {
2761 return left.allocated_bytes > right.allocated_bytes;
2762 }
2763 if (left.allocations != right.allocations) {
2764 return left.allocations > right.allocations;
2765 }
2766 },
2767 .lifetime => {
2768 if (left.lifetime_total_byte_events !=
2769 right.lifetime_total_byte_events)
2770 {
2771 return left.lifetime_total_byte_events >
2772 right.lifetime_total_byte_events;
2773 }
2774 if (left.lifetime_total_events != right.lifetime_total_events) {
2775 return left.lifetime_total_events >
2776 right.lifetime_total_events;
2777 }
2778 if (left.lifetime_max_events != right.lifetime_max_events) {
2779 return left.lifetime_max_events > right.lifetime_max_events;
2780 }
2781 if (left.completed_lifetimes != right.completed_lifetimes) {
2782 return left.completed_lifetimes > right.completed_lifetimes;
2783 }
2784 },
2785 }
2786 if (left.retained_bytes != right.retained_bytes) {
2787 return left.retained_bytes > right.retained_bytes;
2788 }
2789 if (left.high_water_retained_bytes != right.high_water_retained_bytes) {
2790 return left.high_water_retained_bytes >
2791 right.high_water_retained_bytes;
2792 }
2793 if (left.allocated_bytes != right.allocated_bytes) {
2794 return left.allocated_bytes > right.allocated_bytes;
2795 }
2796 if (left.allocations != right.allocations) {
2797 return left.allocations > right.allocations;
2798 }
2799 if (left.lifetime_total_byte_events !=
2800 right.lifetime_total_byte_events)
2801 {
2802 return left.lifetime_total_byte_events >
2803 right.lifetime_total_byte_events;
2804 }
2805 if (left.lifetime_total_events != right.lifetime_total_events) {
2806 return left.lifetime_total_events > right.lifetime_total_events;
2807 }
2808 if (left.lifetime_max_events != right.lifetime_max_events) {
2809 return left.lifetime_max_events > right.lifetime_max_events;
2810 }
2811 if (left.completed_lifetimes != right.completed_lifetimes) {
2812 return left.completed_lifetimes > right.completed_lifetimes;
2813 }
2814 if (left.live_bytes != right.live_bytes) {
2815 return left.live_bytes > right.live_bytes;
2816 }
2817 if (left.high_water_live_bytes != right.high_water_live_bytes) {
2818 return left.high_water_live_bytes > right.high_water_live_bytes;
2819 }
2820 return null;
2821 }
2822
2823 test "allocation summary sort separates retention traffic and lifetime" {
2824 const retained = ScopeSummary{
2825 .path = "retained",
2826 .counters = .{
2827 .retained_bytes = 128,
2828 .allocated_bytes = 16,
2829 .lifetime_total_events = 8,
2830 },
2831 };
2832 const traffic = ScopeSummary{
2833 .path = "traffic",
2834 .counters = .{
2835 .retained_bytes = 32,
2836 .allocated_bytes = 256,
2837 .lifetime_total_events = 4,
2838 },
2839 };
2840 const lifetime = ScopeSummary{
2841 .path = "lifetime",
2842 .counters = .{
2843 .retained_bytes = 16,
2844 .allocated_bytes = 32,
2845 .lifetime_total_events = 512,
2846 },
2847 };
2848 try std.testing.expect(scopeSummaryGreaterThan(.retained, retained, traffic));
2849 try std.testing.expect(scopeSummaryGreaterThan(.traffic, traffic, retained));
2850 try std.testing.expect(scopeSummaryGreaterThan(.lifetime, lifetime, retained));
2851 inline for (std.meta.tags(Sort)) |sort| {
2852 try std.testing.expect(!scopeSummaryGreaterThan(sort, retained, retained));
2853 }
2854 try std.testing.expectEqual(Sort.retained, Sort.parse("retained").?);
2855 try std.testing.expectEqual(Sort.traffic, Sort.parse("traffic").?);
2856 try std.testing.expectEqual(Sort.lifetime, Sort.parse("lifetime").?);
2857 try std.testing.expect(Sort.parse("bytes") == null);
2858 }
2859
2860 fn resolveSiteSummarySymbolsAlloc(
2861 allocator: Allocator,
2862 binary_path: []const u8,
2863 summaries: []const SiteSummary,
2864 ) !?stack_mod.symbolize.Symbols {
2865 if (summaries.len == 0) return null;
2866 const addresses = try allocator.alloc(u64, summaries.len);
2867 defer allocator.free(addresses);
2868 for (summaries, 0..) |summary, index| {
2869 addresses[index] = summary.return_address;
2870 }
2871 return try stack_mod.symbolize.resolveAlloc(
2872 allocator,
2873 binary_path,
2874 addresses,
2875 );
2876 }
2877
2878 fn resolveSiteAddressesAlloc(
2879 allocator: Allocator,
2880 binary_path: []const u8,
2881 addresses: []const u64,
2882 ) !?stack_mod.symbolize.Symbols {
2883 if (addresses.len == 0) return null;
2884 return try stack_mod.symbolize.resolveAlloc(
2885 allocator,
2886 binary_path,
2887 addresses,
2888 );
2889 }
2890
2891 test "event analysis preserves retained allocator pressure" {
2892 var tracer = try tracer_mod.Tracer.init(std.testing.allocator, .{ .record_events = true });
2893 defer tracer.deinit();
2894 var traced = try tracer.tracedAllocatorWithOptions(std.testing.allocator, .{
2895 .name = "arena",
2896 .retention = .retains_freed_memory,
2897 });
2898 const allocator = traced.allocator();
2899
2900 var scope = try tracer.enter("phase");
2901 const bytes = try allocator.alloc(u8, 64);
2902 allocator.free(bytes);
2903 scope.exit();
2904
2905 var events = std.Io.Writer.Allocating.init(std.testing.allocator);
2906 defer events.deinit();
2907 try tracer.writeEventsJsonl(&events.writer);
2908
2909 var analyzer = Analyzer.init(std.testing.allocator);
2910 defer analyzer.deinit();
2911 try analyzer.ingestJsonlBytes(events.written());
2912 const integrity = analyzer.captureIntegrity();
2913 try std.testing.expectEqualStrings("complete", integrity.status);
2914 try std.testing.expectEqual(@as(?u64, 1), integrity.first_sequence);
2915 try std.testing.expectEqual(integrity.first_sequence, integrity.start_sequence);
2916 try std.testing.expectEqual(integrity.last_sequence, integrity.stop_sequence);
2917
2918 var out = std.Io.Writer.Allocating.init(std.testing.allocator);
2919 defer out.deinit();
2920 try analyzer.writeSummary(&out.writer, .{ .top = 8, .include_zero_live = true });
2921 const text = out.written();
2922 try std.testing.expect(std.mem.indexOf(u8, text, "retained_bytes=64") != null);
2923 try std.testing.expect(std.mem.indexOf(u8, text, "live_bytes=0") != null);
2924 try std.testing.expect(std.mem.indexOf(u8, text, "root/phase retained_bytes=64") != null);
2925 }
2926
2927 test "event analysis separates resize and remap event counts" {
2928 var analyzer = Analyzer.init(std.testing.allocator);
2929 defer analyzer.deinit();
2930 var events = std.Io.Writer.Allocating.init(std.testing.allocator);
2931 defer events.deinit();
2932 const rows = [_]event_mod.Event{
2933 .{ .seq = 1, .kind = .trace_start },
2934 .{ .seq = 2, .kind = .allocator, .allocator_id = 0 },
2935 .{
2936 .seq = 3,
2937 .kind = .alloc,
2938 .allocation_id = 1,
2939 .address = 100,
2940 .len = 16,
2941 },
2942 .{
2943 .seq = 4,
2944 .kind = .resize,
2945 .allocation_id = 1,
2946 .address = 100,
2947 .old_len = 16,
2948 .len = 24,
2949 },
2950 .{
2951 .seq = 5,
2952 .kind = .remap,
2953 .allocation_id = 1,
2954 .old_address = 100,
2955 .address = 200,
2956 .old_len = 24,
2957 .len = 32,
2958 },
2959 .{ .seq = 6, .kind = .trace_stop },
2960 };
2961 for (rows) |row| try row.writeJsonLine(&events.writer, null, null);
2962 try analyzer.ingestJsonlBytes(events.written());
2963 var output: [8 * 1024]u8 = undefined;
2964 var writer = std.Io.Writer.fixed(&output);
2965 try analyzer.writeSummaryJsonl(&writer, .{ .top = 0 });
2966 try std.testing.expect(
2967 std.mem.indexOf(u8, writer.buffered(), "\"resizes\":1,\"remaps\":1") != null,
2968 );
2969 }
2970
2971 test "event analysis classifies release-explicit lifecycle as not required" {
2972 var events = std.Io.Writer.Allocating.init(std.testing.allocator);
2973 defer events.deinit();
2974 const rows = [_]event_mod.Event{
2975 .{ .seq = 1, .kind = .trace_start },
2976 .{
2977 .seq = 2,
2978 .kind = .allocator,
2979 .allocator_id = 1,
2980 .retains_freed_memory = true,
2981 .layer = .logical_allocator,
2982 .lifecycle_instrumented = true,
2983 },
2984 .{
2985 .seq = 3,
2986 .kind = .allocator,
2987 .allocator_id = 2,
2988 .retains_freed_memory = false,
2989 .layer = .logical_allocator,
2990 },
2991 .{ .seq = 4, .kind = .trace_stop },
2992 };
2993 for (rows) |row| try row.writeJsonLine(&events.writer, null, null);
2994 var analyzer = Analyzer.initForLayer(std.testing.allocator, .logical);
2995 defer analyzer.deinit();
2996 try analyzer.ingestJsonlBytes(events.written());
2997
2998 var summary = std.Io.Writer.Allocating.init(std.testing.allocator);
2999 defer summary.deinit();
3000 try analyzer.writeSummaryJsonl(&summary.writer, .{
3001 .layer = .logical,
3002 });
3003 try std.testing.expect(std.mem.indexOf(
3004 u8,
3005 summary.written(),
3006 "\"lifecycle_coverage\":\"complete\"",
3007 ) != null);
3008 try std.testing.expect(std.mem.indexOf(
3009 u8,
3010 summary.written(),
3011 "\"lifecycle_instrumented_producers\":1",
3012 ) != null);
3013 try std.testing.expect(std.mem.indexOf(
3014 u8,
3015 summary.written(),
3016 "\"lifecycle_uninstrumented_producers\":0",
3017 ) != null);
3018 try std.testing.expect(std.mem.indexOf(
3019 u8,
3020 summary.written(),
3021 "\"lifecycle_not_required_producers\":1",
3022 ) != null);
3023 try std.testing.expect(std.mem.indexOf(
3024 u8,
3025 summary.written(),
3026 "\"prefix_complete_producers\":2",
3027 ) != null);
3028 }
3029
3030 test "event replay matches failed known and lost-tracking remaps" {
3031 if (!observe.enabled or !sys.memory.observe.enabled) {
3032 return error.SkipZigTest;
3033 }
3034 var tracer = try tracer_mod.Tracer.init(std.testing.allocator, .{
3035 .record_events = true,
3036 });
3037 defer tracer.deinit();
3038 var observation = try tracer.observeOwnedAllocators();
3039 defer observation.stop();
3040 const identity = observe.Identity{
3041 .producer_id = observe.producerId(),
3042 .producer = .arena,
3043 .generation = 0,
3044 .owner_cookie = 0xcafe,
3045 };
3046
3047 var first = observe.beginOwned(
3048 identity,
3049 .alloc,
3050 0,
3051 0,
3052 64,
3053 8,
3054 @returnAddress(),
3055 );
3056 first.finish(.{
3057 .address = 0x1000,
3058 .succeeded = true,
3059 });
3060 var second = observe.beginOwned(
3061 identity,
3062 .alloc,
3063 0,
3064 0,
3065 32,
3066 8,
3067 @returnAddress(),
3068 );
3069 second.finish(.{
3070 .address = 0x2000,
3071 .succeeded = true,
3072 });
3073 var failed_known = observe.beginOwned(
3074 identity,
3075 .remap,
3076 0x1000,
3077 64,
3078 80,
3079 8,
3080 @returnAddress(),
3081 );
3082 failed_known.finish(.{
3083 .address = 0,
3084 .succeeded = false,
3085 });
3086 var collision = observe.beginOwned(
3087 identity,
3088 .remap,
3089 0x1000,
3090 64,
3091 96,
3092 8,
3093 @returnAddress(),
3094 );
3095 collision.finish(.{
3096 .address = 0x2000,
3097 .succeeded = true,
3098 });
3099 observation.stop();
3100
3101 const live = tracer.snapshotLayer(.logical_allocator);
3102 var live_summary = std.Io.Writer.Allocating.init(
3103 std.testing.allocator,
3104 );
3105 defer live_summary.deinit();
3106 try tracer.writeSummaryJsonl(&live_summary.writer, .{
3107 .layer = .logical_allocator,
3108 .include_zero_live = true,
3109 });
3110 try std.testing.expect(std.mem.indexOf(
3111 u8,
3112 live_summary.written(),
3113 "\"resizes\":1,\"remaps\":0",
3114 ) != null);
3115 try std.testing.expect(std.mem.indexOf(
3116 u8,
3117 live_summary.written(),
3118 "\"untracked_requests\":1,\"untracked_request_bytes\":96",
3119 ) != null);
3120 try std.testing.expect(std.mem.indexOf(
3121 u8,
3122 live_summary.written(),
3123 "\"failed_remaps\":1,\"unmatched_frees\":0," ++
3124 "\"unmatched_resizes\":0,\"unmatched_remaps\":0",
3125 ) != null);
3126
3127 var emitted = std.Io.Writer.Allocating.init(std.testing.allocator);
3128 defer emitted.deinit();
3129 try tracer.writeEventsJsonl(&emitted.writer);
3130 var replay = Analyzer.initForLayer(std.testing.allocator, .logical);
3131 defer replay.deinit();
3132 try replay.ingestJsonlBytes(emitted.written());
3133
3134 try std.testing.expectEqual(live.allocations, replay.counters.allocations);
3135 try std.testing.expectEqual(live.frees, replay.counters.frees);
3136 try std.testing.expectEqual(
3137 live.live_allocations,
3138 replay.counters.live_allocations,
3139 );
3140 try std.testing.expectEqual(
3141 live.allocated_bytes,
3142 replay.counters.allocated_bytes,
3143 );
3144 try std.testing.expectEqual(
3145 live.freed_bytes,
3146 replay.counters.freed_bytes,
3147 );
3148 try std.testing.expectEqual(live.live_bytes, replay.counters.live_bytes);
3149 try std.testing.expectEqual(
3150 live.high_water_live_bytes,
3151 replay.counters.high_water_live_bytes,
3152 );
3153 try std.testing.expectEqual(@as(u64, 1), replay.counters.resizes);
3154 try std.testing.expectEqual(@as(u64, 0), replay.counters.remaps);
3155 try std.testing.expectEqual(@as(u64, 1), replay.counters.failed_remaps);
3156 try std.testing.expectEqual(@as(u64, 0), replay.counters.unmatched_remaps);
3157 try std.testing.expectEqual(
3158 @as(u64, 1),
3159 replay.counters.untracked_requests,
3160 );
3161 try std.testing.expectEqual(
3162 @as(usize, 96),
3163 replay.counters.untracked_request_bytes,
3164 );
3165 try std.testing.expectEqual(@as(usize, 1), replay.allocations.count());
3166 }
3167
3168 test "event analysis classifies sequence loss and tail truncation" {
3169 var gapped = Analyzer.init(std.testing.allocator);
3170 defer gapped.deinit();
3171 try gapped.ingestJsonlBytes(
3172 "{\"v\":3,\"seq\":1,\"kind\":\"trace.start\"}\n" ++
3173 "{\"v\":3,\"seq\":3,\"kind\":\"allocator\",\"allocator_id\":0}\n" ++
3174 "{\"v\":3,\"seq\":4,\"kind\":\"trace.stop\"}\n",
3175 );
3176 const gap_integrity = gapped.captureIntegrity();
3177 try std.testing.expectEqualStrings("sequence_gaps", gap_integrity.status);
3178 try std.testing.expectEqual(@as(u64, 1), gap_integrity.sequence_gap_count);
3179 try std.testing.expectEqual(@as(u64, 1), gap_integrity.missing_sequence_event_count);
3180
3181 var truncated = Analyzer.init(std.testing.allocator);
3182 defer truncated.deinit();
3183 try truncated.ingestJsonlBytes(
3184 "{\"v\":3,\"seq\":1,\"kind\":\"trace.start\"}\n" ++
3185 "{\"v\":3,\"seq\":2,\"kind\":\"allocator\",\"allocator_id\":0}\n",
3186 );
3187 const tail_integrity = truncated.captureIntegrity();
3188 try std.testing.expectEqualStrings("missing_stop_event", tail_integrity.status);
3189 try std.testing.expectEqual(@as(u64, 0), tail_integrity.sequence_gap_count);
3190 }
3191
3192 test "event analysis reports allocation sites" {
3193 var analyzer = Analyzer.init(std.testing.allocator);
3194 defer analyzer.deinit();
3195
3196 try analyzer.ingestJsonLine(
3197 "{\"v\":3,\"kind\":\"allocator\",\"allocator_id\":0,\"retains_freed_memory\":true}",
3198 );
3199 try analyzer.ingestJsonLine(
3200 "{\"v\":3,\"kind\":\"alloc\",\"allocator_id\":0,\"allocation_id\":1,\"address\":4096,\"len\":64,\"return_address\":2748,\"scope\":\"root/phase\"}",
3201 );
3202 try analyzer.ingestJsonLine(
3203 "{\"v\":3,\"kind\":\"free\",\"allocator_id\":0,\"allocation_id\":1,\"address\":4096,\"len\":64,\"return_address\":8192,\"scope\":\"root/phase\"}",
3204 );
3205
3206 var out = std.Io.Writer.Allocating.init(std.testing.allocator);
3207 defer out.deinit();
3208 try analyzer.writeSummary(&out.writer, .{ .top = 8, .include_sites = true });
3209 const text = out.written();
3210 try std.testing.expect(std.mem.indexOf(u8, text, "site return_address=0xabc retained_bytes=64") != null);
3211 try std.testing.expect(std.mem.indexOf(u8, text, "freed_bytes=64") != null);
3212 }
3213
3214 test "event analysis reports site detail by size and scope" {
3215 var analyzer = Analyzer.init(std.testing.allocator);
3216 defer analyzer.deinit();
3217 analyzer.site_detail_return_address = 0xabc;
3218
3219 try analyzer.ingestJsonLine(
3220 "{\"v\":3,\"kind\":\"allocator\",\"allocator_id\":0,\"retains_freed_memory\":true}",
3221 );
3222 try analyzer.ingestJsonLine(
3223 "{\"v\":3,\"kind\":\"alloc\",\"allocator_id\":0,\"allocation_id\":1,\"address\":4096,\"len\":64,\"return_address\":2748,\"scope\":\"root/left\"}",
3224 );
3225 try analyzer.ingestJsonLine(
3226 "{\"v\":3,\"kind\":\"alloc\",\"allocator_id\":0,\"allocation_id\":2,\"address\":8192,\"len\":128,\"return_address\":2748,\"scope\":\"root/right\"}",
3227 );
3228 try analyzer.ingestJsonLine(
3229 "{\"v\":3,\"kind\":\"free\",\"allocator_id\":0,\"allocation_id\":1,\"address\":4096,\"len\":64,\"return_address\":8192,\"scope\":\"root/left\"}",
3230 );
3231
3232 var out = std.Io.Writer.Allocating.init(std.testing.allocator);
3233 defer out.deinit();
3234 try analyzer.writeSummary(&out.writer, .{ .top = 8, .site_detail_return_address = 0xabc });
3235 const text = out.written();
3236 try std.testing.expect(std.mem.indexOf(u8, text, "site_detail return_address=0xabc") != null);
3237 try std.testing.expect(std.mem.indexOf(u8, text, "site_size len=128 retained_bytes=128") != null);
3238 try std.testing.expect(std.mem.indexOf(u8, text, "site_size len=64 retained_bytes=64") != null);
3239 try std.testing.expect(std.mem.indexOf(u8, text, "site_scope scope=\"root/right\" retained_bytes=128") != null);
3240 try std.testing.expect(std.mem.indexOf(u8, text, "site_scope scope=\"root/left\" retained_bytes=64") != null);
3241 }
3242
3243 test "event analysis reports completed allocation lifetimes" {
3244 var analyzer = Analyzer.init(std.testing.allocator);
3245 defer analyzer.deinit();
3246
3247 try analyzer.ingestJsonLine(
3248 "{\"v\":3,\"kind\":\"allocator\",\"allocator_id\":0,\"retains_freed_memory\":false}",
3249 );
3250 try analyzer.ingestJsonLine(
3251 "{\"v\":3,\"kind\":\"alloc\",\"allocator_id\":0,\"allocation_id\":1,\"address\":4096,\"len\":64,\"return_address\":2748,\"scope\":\"root/phase\"}",
3252 );
3253 try analyzer.ingestJsonLine(
3254 "{\"v\":3,\"kind\":\"resize\",\"allocator_id\":0,\"allocation_id\":1,\"address\":4096,\"old_len\":64,\"len\":80,\"return_address\":2748,\"scope\":\"root/phase\"}",
3255 );
3256 try analyzer.ingestJsonLine(
3257 "{\"v\":3,\"kind\":\"alloc\",\"allocator_id\":0,\"allocation_id\":2,\"address\":8192,\"len\":32,\"return_address\":3567,\"scope\":\"root/temp\"}",
3258 );
3259 try analyzer.ingestJsonLine(
3260 "{\"v\":3,\"kind\":\"free\",\"allocator_id\":0,\"allocation_id\":2,\"address\":8192,\"len\":32,\"return_address\":3567,\"scope\":\"root/temp\"}",
3261 );
3262 try analyzer.ingestJsonLine(
3263 "{\"v\":3,\"kind\":\"free\",\"allocator_id\":0,\"allocation_id\":1,\"address\":4096,\"len\":80,\"return_address\":2748,\"scope\":\"root/phase\"}",
3264 );
3265
3266 var out = std.Io.Writer.Allocating.init(std.testing.allocator);
3267 defer out.deinit();
3268 try analyzer.writeSummary(&out.writer, .{ .top = 8, .include_sites = true, .include_zero_live = true });
3269 const text = out.written();
3270 try std.testing.expect(std.mem.indexOf(u8, text, "memtrace lifetimes completed=2 total_events=5 mean_events=2 max_events=4") != null);
3271 try std.testing.expect(std.mem.indexOf(u8, text, "completed_lifetimes=1 lifetime_total_events=4 lifetime_mean_events=4 lifetime_max_events=4") != null);
3272 try std.testing.expect(std.mem.indexOf(u8, text, "completed_lifetimes=1 lifetime_total_events=1 lifetime_mean_events=1 lifetime_max_events=1") != null);
3273 try std.testing.expect(std.mem.indexOf(
3274 u8,
3275 text,
3276 "total_byte_events=336 mean_byte_events=168 max_byte_events=304",
3277 ) != null);
3278
3279 var jsonl = std.Io.Writer.Allocating.init(std.testing.allocator);
3280 defer jsonl.deinit();
3281 try analyzer.writeSummaryJsonl(&jsonl.writer, .{ .top = 8, .include_sites = true, .include_zero_live = true });
3282 const jsonl_text = jsonl.written();
3283 try std.testing.expect(std.mem.indexOf(u8, jsonl_text, "\"completed_lifetimes\":2") != null);
3284 try std.testing.expect(std.mem.indexOf(u8, jsonl_text, "\"lifetime_total_events\":5") != null);
3285 try std.testing.expect(std.mem.indexOf(u8, jsonl_text, "\"lifetime_max_events\":4") != null);
3286 try std.testing.expect(std.mem.indexOf(
3287 u8,
3288 jsonl_text,
3289 "\"lifetime_total_byte_events\":336",
3290 ) != null);
3291 try std.testing.expect(std.mem.indexOf(
3292 u8,
3293 jsonl_text,
3294 "\"kind\":\"scope\",\"sort\":\"retained\"",
3295 ) != null);
3296 try std.testing.expect(std.mem.indexOf(
3297 u8,
3298 jsonl_text,
3299 "\"kind\":\"site\",\"sort\":\"retained\"",
3300 ) != null);
3301
3302 var snapshot_value = try analyzer.snapshot(std.testing.allocator, .{
3303 .top = 8,
3304 .include_zero_live = true,
3305 });
3306 defer snapshot_value.deinit();
3307 try std.testing.expectEqual(
3308 @as(u64, 336),
3309 snapshot_value.summary.lifetime_total_byte_events,
3310 );
3311 try std.testing.expectEqual(
3312 @as(u64, 304),
3313 snapshot_value.summary.lifetime_max_byte_events,
3314 );
3315 inline for (std.meta.tags(Sort)) |sort| {
3316 const ranking = snapshot_value.rankings.get(sort);
3317 try std.testing.expectEqual(@as(usize, 2), ranking.scopes.len);
3318 try std.testing.expectEqual(@as(usize, 2), ranking.sources.len);
3319 }
3320 try std.testing.expectEqualStrings(
3321 "root/phase",
3322 snapshot_value.rankings.lifetime.scopes[0].scope,
3323 );
3324 try std.testing.expectEqual(
3325 @as(u64, 304),
3326 snapshot_value.rankings.lifetime.scopes[0]
3327 .lifetime_total_byte_events,
3328 );
3329 }
3330
3331 test "event analysis separates backing logical and failed allocations" {
3332 const events =
3333 "{\"v\":3,\"kind\":\"allocator\",\"allocator_id\":0," ++
3334 "\"layer\":\"backing_boundary\"}\n" ++
3335 "{\"v\":3,\"kind\":\"allocator\",\"allocator_id\":1," ++
3336 "\"layer\":\"logical_allocator\",\"producer\":\"arena\"}\n" ++
3337 "{\"v\":3,\"kind\":\"alloc\",\"allocator_id\":0," ++
3338 "\"allocation_id\":1,\"address\":4096,\"len\":128," ++
3339 "\"scope\":\"root\",\"layer\":\"backing_boundary\"}\n" ++
3340 "{\"v\":3,\"kind\":\"alloc\",\"allocator_id\":1," ++
3341 "\"allocation_id\":2,\"address\":8192,\"len\":16," ++
3342 "\"scope\":\"root\",\"layer\":\"logical_allocator\"," ++
3343 "\"producer\":\"arena\"}\n" ++
3344 "{\"v\":3,\"kind\":\"alloc\",\"allocator_id\":1,\"len\":64," ++
3345 "\"succeeded\":false,\"scope\":\"root\"," ++
3346 "\"layer\":\"logical_allocator\",\"producer\":\"arena\"}\n";
3347
3348 var backing = Analyzer.initForLayer(std.testing.allocator, .backing);
3349 defer backing.deinit();
3350 try backing.ingestJsonlBytes(events);
3351 var backing_summary = std.Io.Writer.Allocating.init(
3352 std.testing.allocator,
3353 );
3354 defer backing_summary.deinit();
3355 try backing.writeSummary(&backing_summary.writer, .{});
3356 try std.testing.expect(std.mem.indexOf(
3357 u8,
3358 backing_summary.written(),
3359 "layer=backing sort=retained events=5 allocations=1",
3360 ) != null);
3361 try std.testing.expect(std.mem.indexOf(
3362 u8,
3363 backing_summary.written(),
3364 "allocated_bytes=128",
3365 ) != null);
3366
3367 var logical = Analyzer.initForLayer(std.testing.allocator, .logical);
3368 defer logical.deinit();
3369 try logical.ingestJsonlBytes(events);
3370 var logical_summary = std.Io.Writer.Allocating.init(
3371 std.testing.allocator,
3372 );
3373 defer logical_summary.deinit();
3374 try logical.writeSummary(
3375 &logical_summary.writer,
3376 .{ .layer = .logical },
3377 );
3378 try std.testing.expect(std.mem.indexOf(
3379 u8,
3380 logical_summary.written(),
3381 "layer=logical sort=retained events=5 allocations=1",
3382 ) != null);
3383 try std.testing.expect(std.mem.indexOf(
3384 u8,
3385 logical_summary.written(),
3386 "requested_bytes=16",
3387 ) != null);
3388 try std.testing.expect(std.mem.indexOf(
3389 u8,
3390 logical_summary.written(),
3391 "failed_allocations=1",
3392 ) != null);
3393 }
3394
3395 test "physical analysis preserves partial mapping lifecycles" {
3396 const events =
3397 "{\"v\":3,\"kind\":\"map\",\"allocator_id\":2," ++
3398 "\"address\":4096,\"len\":16384,\"scope\":\"root\"," ++
3399 "\"layer\":\"physical_page\",\"producer\":\"sys_memory\"}\n" ++
3400 "{\"v\":3,\"kind\":\"unmap\",\"allocator_id\":2," ++
3401 "\"address\":4096,\"len\":4096,\"scope\":\"root\"," ++
3402 "\"layer\":\"physical_page\",\"producer\":\"sys_memory\"}\n" ++
3403 "{\"v\":3,\"kind\":\"unmap\",\"allocator_id\":2," ++
3404 "\"address\":16384,\"len\":4096,\"scope\":\"root\"," ++
3405 "\"layer\":\"physical_page\",\"producer\":\"sys_memory\"}\n" ++
3406 "{\"v\":3,\"kind\":\"discard\",\"allocator_id\":2," ++
3407 "\"address\":8192,\"len\":4096,\"scope\":\"root\"," ++
3408 "\"layer\":\"physical_page\",\"producer\":\"sys_memory\"}\n";
3409
3410 var analyzer = Analyzer.initForLayer(std.testing.allocator, .physical);
3411 defer analyzer.deinit();
3412 try analyzer.ingestJsonlBytes(events);
3413 try std.testing.expectEqual(@as(u64, 1), analyzer.physical.maps);
3414 try std.testing.expectEqual(@as(u64, 2), analyzer.physical.unmaps);
3415 try std.testing.expectEqual(@as(u64, 1), analyzer.physical.discards);
3416 try std.testing.expectEqual(
3417 @as(usize, 8192),
3418 analyzer.physical.live_mapped_bytes,
3419 );
3420 try std.testing.expectEqual(@as(usize, 1), analyzer.mappings.count());
3421 try std.testing.expectEqual(
3422 @as(usize, 0),
3423 analyzer.physical.untracked_unmap_bytes,
3424 );
3425
3426 var output = std.Io.Writer.Allocating.init(std.testing.allocator);
3427 defer output.deinit();
3428 try analyzer.writeSummaryJsonl(
3429 &output.writer,
3430 .{ .layer = .physical },
3431 );
3432 try std.testing.expect(std.mem.indexOf(
3433 u8,
3434 output.written(),
3435 "\"live_mapped_bytes\":8192",
3436 ) != null);
3437 }
3438
3439 test "physical advice records success and failure without changing mapped bytes" {
3440 const events =
3441 "{\"v\":3,\"kind\":\"map\",\"allocator_id\":2," ++
3442 "\"address\":4096,\"len\":4096,\"layer\":\"physical_page\"}\n" ++
3443 "{\"v\":3,\"kind\":\"advise\",\"allocator_id\":2," ++
3444 "\"address\":4096,\"len\":4096,\"layer\":\"physical_page\"}\n" ++
3445 "{\"v\":3,\"kind\":\"advise\",\"allocator_id\":2," ++
3446 "\"address\":4096,\"len\":4096,\"layer\":\"physical_page\",\"succeeded\":false}\n";
3447 var analyzer = Analyzer.initForLayer(std.testing.allocator, .physical);
3448 defer analyzer.deinit();
3449 try analyzer.ingestJsonlBytes(events);
3450 try std.testing.expectEqual(@as(u64, 1), analyzer.physical.advises);
3451 try std.testing.expectEqual(@as(u64, 1), analyzer.physical.failed_advises);
3452 try std.testing.expectEqual(@as(u64, 1), analyzer.physical.maps);
3453 try std.testing.expectEqual(@as(usize, 4096), analyzer.physical.live_mapped_bytes);
3454 try std.testing.expectEqual(@as(usize, 4096), analyzer.physical.high_water_mapped_bytes);
3455 try std.testing.expectEqual(@as(usize, 1), analyzer.mappings.count());
3456 }