lib/memtrace/src/stack/roots.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const sys = @import("sys");
3 const observe = @import("alloc_observe");
4 const pretty_json = @import("pretty").json;
5 const memtrace = @import("../root.zig");
6 const analyze_mod = @import("analyze.zig");
7 const capture_mod = @import("capture.zig");
8 const identity_mod = @import("identity.zig");
9 const symbolize_mod = @import("symbolize.zig");
10
11 const coverage_mod = memtrace.coverage;
12 const event_mod = memtrace.event;
13
14 const Allocator = std.mem.Allocator;
15
16 pub const Format = enum {
17 text,
18 jsonl,
19 };
20
21 pub const Sort = enum {
22 roots,
23 requested_bytes,
24 backing_bytes,
25 high_water,
26 };
27
28 pub const Detail = enum {
29 summary,
30 sources,
31 full,
32 };
33
34 pub const Options = struct {
35 top: usize = std.math.maxInt(usize),
36 frame_limit: usize = capture_mod.max_frames_limit,
37 format: Format = .text,
38 binary_path: ?[]const u8 = null,
39 selection: analyze_mod.Selection = .allocations,
40 sort: Sort = .high_water,
41 detail: Detail = .full,
42 window: analyze_mod.Window = .{},
43 };
44
45 const Metrics = struct {
46 operations: u64 = 0,
47 logical_operations: u64 = 0,
48 backing_operations: u64 = 0,
49 physical_operations: u64 = 0,
50 backing_requested_bytes: u128 = 0,
51 physical_requested_bytes: u128 = 0,
52 max_depth: u32 = 0,
53
54 fn record(self: *Metrics, event: event_mod.ReplayEvent) !void {
55 self.operations = try std.math.add(u64, self.operations, 1);
56 const bytes = requestBytes(event);
57 switch (event.layer) {
58 .logical_allocator => {
59 self.logical_operations = try std.math.add(
60 u64,
61 self.logical_operations,
62 1,
63 );
64 },
65 .backing_boundary => {
66 self.backing_operations = try std.math.add(
67 u64,
68 self.backing_operations,
69 1,
70 );
71 self.backing_requested_bytes = try std.math.add(
72 u128,
73 self.backing_requested_bytes,
74 bytes,
75 );
76 },
77 .physical_page => {
78 self.physical_operations = try std.math.add(
79 u64,
80 self.physical_operations,
81 1,
82 );
83 self.physical_requested_bytes = try std.math.add(
84 u128,
85 self.physical_requested_bytes,
86 bytes,
87 );
88 },
89 }
90 }
91
92 fn merge(self: *Metrics, other: Metrics) !void {
93 self.operations = try std.math.add(
94 u64,
95 self.operations,
96 other.operations,
97 );
98 self.logical_operations = try std.math.add(
99 u64,
100 self.logical_operations,
101 other.logical_operations,
102 );
103 self.backing_operations = try std.math.add(
104 u64,
105 self.backing_operations,
106 other.backing_operations,
107 );
108 self.physical_operations = try std.math.add(
109 u64,
110 self.physical_operations,
111 other.physical_operations,
112 );
113 self.backing_requested_bytes = try std.math.add(
114 u128,
115 self.backing_requested_bytes,
116 other.backing_requested_bytes,
117 );
118 self.physical_requested_bytes = try std.math.add(
119 u128,
120 self.physical_requested_bytes,
121 other.physical_requested_bytes,
122 );
123 self.max_depth = @max(self.max_depth, other.max_depth);
124 }
125
126 fn mergeChild(self: *Metrics, child: Metrics) !void {
127 try self.merge(child);
128 self.max_depth = @max(
129 self.max_depth,
130 try std.math.add(u32, child.max_depth, 1),
131 );
132 }
133 };
134
135 const RootKey = struct {
136 stack_id: u32,
137 kind: event_mod.Kind,
138 succeeded: bool,
139 layer: event_mod.Layer,
140 producer: observe.Producer,
141 };
142
143 const Lifetime = struct {
144 live_allocations: u64 = 0,
145 live_bytes: u128 = 0,
146 high_water_live_bytes: u128 = 0,
147
148 fn allocate(self: *Lifetime, len: usize) !void {
149 self.live_allocations = try std.math.add(
150 u64,
151 self.live_allocations,
152 1,
153 );
154 self.live_bytes = try std.math.add(u128, self.live_bytes, len);
155 self.high_water_live_bytes = @max(
156 self.high_water_live_bytes,
157 self.live_bytes,
158 );
159 }
160
161 fn free(self: *Lifetime, len: usize) !void {
162 if (self.live_allocations == 0 or self.live_bytes < len) {
163 return error.InvalidCausalRootLifetime;
164 }
165 self.live_allocations -= 1;
166 self.live_bytes -= len;
167 }
168
169 fn resize(self: *Lifetime, old_len: usize, new_len: usize) !void {
170 if (self.live_bytes < old_len) {
171 return error.InvalidCausalRootLifetime;
172 }
173 self.live_bytes -= old_len;
174 self.live_bytes = try std.math.add(u128, self.live_bytes, new_len);
175 self.high_water_live_bytes = @max(
176 self.high_water_live_bytes,
177 self.live_bytes,
178 );
179 }
180 };
181
182 const Counters = struct {
183 roots: u64 = 0,
184 root_requested_bytes: u128 = 0,
185 metrics: Metrics = .{},
186 lifetime: Lifetime = .{},
187
188 fn record(
189 self: *Counters,
190 event: event_mod.ReplayEvent,
191 metrics: Metrics,
192 ) !void {
193 self.roots = try std.math.add(u64, self.roots, 1);
194 self.root_requested_bytes = try std.math.add(
195 u128,
196 self.root_requested_bytes,
197 requestBytes(event),
198 );
199 try self.metrics.merge(metrics);
200 }
201
202 fn merge(self: *Counters, other: Counters) !void {
203 self.roots = try std.math.add(u64, self.roots, other.roots);
204 self.root_requested_bytes = try std.math.add(
205 u128,
206 self.root_requested_bytes,
207 other.root_requested_bytes,
208 );
209 try self.metrics.merge(other.metrics);
210 }
211 };
212
213 const Summary = struct {
214 key: RootKey,
215 counters: Counters,
216 definition: analyze_mod.Definition,
217 };
218
219 const Totals = struct {
220 roots: u64 = 0,
221 successful: u64 = 0,
222 failed: u64 = 0,
223 root_requested_bytes: u128 = 0,
224 metrics: Metrics = .{},
225 lifetime: Lifetime = .{},
226 };
227
228 const SourceKey = struct {
229 kind: event_mod.Kind,
230 succeeded: bool,
231 layer: event_mod.Layer,
232 producer: observe.Producer,
233 site: u64,
234 caller: u64,
235 };
236
237 const Source = struct {
238 key: SourceKey,
239 counters: Counters,
240 unique_stacks: u32,
241 };
242
243 const LiveAllocation = struct {
244 root: RootKey,
245 source: SourceKey,
246 len: usize,
247 };
248
249 const Display = struct {
250 source_groups: usize,
251 displayed_sources: usize,
252 root_stacks: usize,
253 displayed_stacks: usize,
254 };
255
256 const ChildEvidence = struct {
257 operation_id: u64,
258 sequence: u64,
259 stack_id: u32,
260 scope_id: u32,
261 kind: event_mod.Kind,
262 layer: event_mod.Layer,
263 producer: observe.Producer,
264 succeeded: bool,
265 requested_bytes: usize,
266 return_address: u64,
267
268 fn fromEvent(event: event_mod.ReplayEvent) ChildEvidence {
269 return .{
270 .operation_id = event.operation_id,
271 .sequence = event.seq orelse 0,
272 .stack_id = event.stack_id,
273 .scope_id = event.scope_id,
274 .kind = event.kind,
275 .layer = event.layer,
276 .producer = event.producer,
277 .succeeded = event.succeeded,
278 .requested_bytes = if (event.kind.isMemoryOperation())
279 requestBytes(event)
280 else
281 0,
282 .return_address = event.return_address,
283 };
284 }
285 };
286
287 const Pending = struct {
288 metrics: Metrics = .{},
289 child: ChildEvidence,
290 };
291
292 const DanglingParent = struct {
293 operation_id: u64,
294 child: ChildEvidence,
295 site_address: u64,
296 };
297
298 const OperationLedger = struct {
299 seen: std.DynamicBitSetUnmanaged = .{},
300 unique: u64 = 0,
301 minimum: u64 = std.math.maxInt(u64),
302 maximum: u64 = 0,
303 last_id: u64 = 0,
304 last_parent: u64 = 0,
305
306 fn deinit(self: *OperationLedger, allocator: Allocator) void {
307 self.seen.deinit(allocator);
308 self.* = undefined;
309 }
310
311 fn record(
312 self: *OperationLedger,
313 allocator: Allocator,
314 operation_id: u64,
315 parent_operation_id: u64,
316 ) !void {
317 const index = std.math.cast(usize, operation_id) orelse
318 return error.CausalOperationIdOverflow;
319 try self.ensureCapacity(allocator, index);
320 if (self.seen.isSet(index)) {
321 if (operation_id != self.last_id) {
322 return error.NonContiguousCausalOperationGroup;
323 }
324 if (parent_operation_id != self.last_parent) {
325 return error.CausalOperationParentConflict;
326 }
327 return;
328 }
329 self.seen.set(index);
330 self.unique = try std.math.add(u64, self.unique, 1);
331 self.minimum = @min(self.minimum, operation_id);
332 self.maximum = @max(self.maximum, operation_id);
333 self.last_id = operation_id;
334 self.last_parent = parent_operation_id;
335 }
336
337 fn ensureCapacity(
338 self: *OperationLedger,
339 allocator: Allocator,
340 index: usize,
341 ) !void {
342 if (index < self.seen.bit_length) return;
343 const required = try std.math.add(usize, index, 1);
344 const doubled = std.math.mul(
345 usize,
346 @max(self.seen.bit_length, 1024),
347 2,
348 ) catch std.math.maxInt(usize);
349 try self.seen.resize(allocator, @max(required, doubled), false);
350 }
351
352 fn validate(self: OperationLedger) !void {
353 if (self.unique == 0) return;
354 const span = try std.math.add(
355 u64,
356 self.maximum - self.minimum,
357 1,
358 );
359 if (span != self.unique) return error.NonContiguousCausalOperations;
360 }
361 };
362
363 const Analyzer = struct {
364 allocator: Allocator,
365 stack: analyze_mod.Analyzer,
366 pending: std.AutoHashMapUnmanaged(u64, Pending) = .{},
367 groups: std.AutoHashMapUnmanaged(RootKey, Counters) = .{},
368 source_lifetimes: std.AutoHashMapUnmanaged(SourceKey, Lifetime) = .{},
369 live_allocations: std.AutoHashMapUnmanaged(u64, LiveAllocation) = .{},
370 operation_ledger: OperationLedger = .{},
371 lifetime: Lifetime = .{},
372 finished: bool = false,
373
374 fn init(allocator: Allocator, window: analyze_mod.Window) Analyzer {
375 return .{
376 .allocator = allocator,
377 .stack = analyze_mod.Analyzer.init(allocator, window),
378 };
379 }
380
381 fn deinit(self: *Analyzer) void {
382 self.stack.deinit();
383 self.pending.deinit(self.allocator);
384 self.groups.deinit(self.allocator);
385 self.source_lifetimes.deinit(self.allocator);
386 self.live_allocations.deinit(self.allocator);
387 self.operation_ledger.deinit(self.allocator);
388 self.* = undefined;
389 }
390
391 fn ingestJsonLine(self: *Analyzer, line: []const u8) !void {
392 if (self.finished) return error.CausalRootAnalyzerFinished;
393 const text = std.mem.trim(u8, line, " \t\r\n");
394 if (text.len == 0) return;
395 try self.stack.ingestJsonLine(text);
396 if (identity_mod.isMetadataLine(text) or
397 coverage_mod.isMetadataLine(text) or
398 capture_mod.isMetadataLine(text))
399 {
400 return;
401 }
402 const event = try event_mod.parseReplayFast(text);
403 const memory_operation = event.kind.isMemoryOperation();
404 if (!memory_operation and event.kind != .lifecycle) return;
405 if (event.operation_id == 0) return error.MissingCausalOperation;
406 if (event.parent_operation_id >= event.operation_id and
407 event.parent_operation_id != 0)
408 {
409 return error.CausalOperationCycle;
410 }
411 if (memory_operation) try self.recordExistingLifetime(event);
412 try self.operation_ledger.record(
413 self.allocator,
414 event.operation_id,
415 event.parent_operation_id,
416 );
417
418 var metrics = if (self.pending.fetchRemove(event.operation_id)) |entry|
419 entry.value.metrics
420 else
421 Metrics{};
422 if (memory_operation) try metrics.record(event);
423 if (event.parent_operation_id == 0) {
424 if (!memory_operation) return;
425 if (!try self.stack.includes(event)) return;
426 return self.recordRoot(event, metrics);
427 }
428 const parent = try self.pending.getOrPut(
429 self.allocator,
430 event.parent_operation_id,
431 );
432 if (!parent.found_existing) {
433 parent.value_ptr.* = .{
434 .child = ChildEvidence.fromEvent(event),
435 };
436 }
437 if (memory_operation) {
438 try parent.value_ptr.metrics.mergeChild(metrics);
439 } else {
440 try parent.value_ptr.metrics.merge(metrics);
441 }
442 }
443
444 fn finish(self: *Analyzer) !void {
445 if (self.finished) return error.CausalRootAnalyzerFinished;
446 try self.stack.validate();
447 try self.finishCausal();
448 }
449
450 fn finishCausal(self: *Analyzer) !void {
451 if (self.finished) return error.CausalRootAnalyzerFinished;
452 if (self.pending.count() != 0) return error.DanglingParentOperation;
453 try self.operation_ledger.validate();
454 try self.validateLifetimes();
455 self.finished = true;
456 }
457
458 fn validateLifetimes(self: *const Analyzer) !void {
459 var group_allocations: u64 = 0;
460 var group_bytes: u128 = 0;
461 var groups = self.groups.valueIterator();
462 while (groups.next()) |counters| {
463 group_allocations = try std.math.add(
464 u64,
465 group_allocations,
466 counters.lifetime.live_allocations,
467 );
468 group_bytes = try std.math.add(
469 u128,
470 group_bytes,
471 counters.lifetime.live_bytes,
472 );
473 }
474 var source_allocations: u64 = 0;
475 var source_bytes: u128 = 0;
476 var sources = self.source_lifetimes.valueIterator();
477 while (sources.next()) |lifetime| {
478 source_allocations = try std.math.add(
479 u64,
480 source_allocations,
481 lifetime.live_allocations,
482 );
483 source_bytes = try std.math.add(
484 u128,
485 source_bytes,
486 lifetime.live_bytes,
487 );
488 }
489 if (group_allocations != self.lifetime.live_allocations or
490 source_allocations != self.lifetime.live_allocations or
491 self.live_allocations.count() != self.lifetime.live_allocations or
492 group_bytes != self.lifetime.live_bytes or
493 source_bytes != self.lifetime.live_bytes)
494 {
495 return error.InvalidCausalRootLifetime;
496 }
497 }
498
499 fn collectDangling(
500 self: *const Analyzer,
501 ) !std.ArrayListUnmanaged(DanglingParent) {
502 var result = std.ArrayListUnmanaged(DanglingParent).empty;
503 errdefer result.deinit(self.allocator);
504 try result.ensureTotalCapacity(self.allocator, self.pending.count());
505 var pending = self.pending.iterator();
506 while (pending.next()) |entry| {
507 const child = entry.value_ptr.child;
508 const site_address = if (child.stack_id != 0)
509 (self.stack.stackDefinition(child.stack_id) orelse
510 return error.MissingStackDefinition).call_addresses[0]
511 else source: {
512 const return_address = std.math.cast(
513 usize,
514 child.return_address,
515 ) orelse return error.InvalidDanglingParentSite;
516 if (return_address == 0) return error.MissingDanglingParentSite;
517 break :source capture_mod.callAddress(return_address);
518 };
519 result.appendAssumeCapacity(.{
520 .operation_id = entry.key_ptr.*,
521 .child = child,
522 .site_address = site_address,
523 });
524 }
525 std.mem.sort(
526 DanglingParent,
527 result.items,
528 {},
529 danglingParentLessThan,
530 );
531 return result;
532 }
533
534 fn recordRoot(
535 self: *Analyzer,
536 event: event_mod.ReplayEvent,
537 metrics: Metrics,
538 ) !void {
539 const key = RootKey{
540 .stack_id = event.stack_id,
541 .kind = event.kind,
542 .succeeded = event.succeeded,
543 .layer = event.layer,
544 .producer = event.producer,
545 };
546 const entry = try self.groups.getOrPut(self.allocator, key);
547 if (!entry.found_existing) entry.value_ptr.* = .{};
548 try entry.value_ptr.record(event, metrics);
549 try self.recordLifetime(event, key);
550 }
551
552 fn recordLifetime(
553 self: *Analyzer,
554 event: event_mod.ReplayEvent,
555 root: RootKey,
556 ) !void {
557 switch (event.kind) {
558 .alloc => if (event.succeeded and event.tracked and event.len != 0) {
559 try self.recordAllocation(event, root);
560 },
561 else => {},
562 }
563 }
564
565 fn recordExistingLifetime(
566 self: *Analyzer,
567 event: event_mod.ReplayEvent,
568 ) !void {
569 switch (event.kind) {
570 .free, .release => if (event.tracked and event.len != 0) {
571 try self.recordFree(event);
572 },
573 .resize => if (event.succeeded and event.tracked) {
574 try self.recordResize(event);
575 },
576 .remap => if (event.succeeded and event.tracked) {
577 try self.recordRemap(event);
578 },
579 else => {},
580 }
581 }
582
583 fn recordAllocation(
584 self: *Analyzer,
585 event: event_mod.ReplayEvent,
586 root: RootKey,
587 ) !void {
588 const source = self.sourceKey(root);
589 const allocation_key = allocationKey(
590 event.allocation_id,
591 event.address,
592 );
593 if (allocation_key == 0) return error.MissingCausalAllocationIdentity;
594 const live = try self.live_allocations.getOrPut(
595 self.allocator,
596 allocation_key,
597 );
598 if (live.found_existing) return error.DuplicateCausalAllocation;
599 live.value_ptr.* = .{
600 .root = root,
601 .source = source,
602 .len = event.len,
603 };
604 try self.lifetime.allocate(event.len);
605 try self.groups.getPtr(root).?.lifetime.allocate(event.len);
606 const source_entry = try self.source_lifetimes.getOrPut(
607 self.allocator,
608 source,
609 );
610 if (!source_entry.found_existing) source_entry.value_ptr.* = .{};
611 try source_entry.value_ptr.allocate(event.len);
612 }
613
614 fn recordFree(
615 self: *Analyzer,
616 event: event_mod.ReplayEvent,
617 ) !void {
618 const allocation_key = allocationKey(
619 event.allocation_id,
620 event.address,
621 );
622 const removed = self.live_allocations.fetchRemove(allocation_key) orelse {
623 return;
624 };
625 try self.release(removed.value, removed.value.len);
626 }
627
628 fn recordResize(
629 self: *Analyzer,
630 event: event_mod.ReplayEvent,
631 ) !void {
632 const allocation_key = allocationKey(
633 event.allocation_id,
634 event.address,
635 );
636 const live = self.live_allocations.getPtr(allocation_key) orelse {
637 return;
638 };
639 try self.resize(live.*, event.len);
640 live.len = event.len;
641 }
642
643 fn recordRemap(
644 self: *Analyzer,
645 event: event_mod.ReplayEvent,
646 ) !void {
647 const old_key = allocationKey(
648 event.allocation_id,
649 event.old_address,
650 );
651 const new_key = allocationKey(
652 event.allocation_id,
653 event.address,
654 );
655 if (old_key == new_key) return self.recordResize(event);
656 const removed = self.live_allocations.fetchRemove(old_key) orelse {
657 return;
658 };
659 const entry = try self.live_allocations.getOrPut(
660 self.allocator,
661 new_key,
662 );
663 if (entry.found_existing) return error.DuplicateCausalAllocation;
664 entry.value_ptr.* = removed.value;
665 try self.resize(entry.value_ptr.*, event.len);
666 entry.value_ptr.len = event.len;
667 }
668
669 fn release(
670 self: *Analyzer,
671 live: LiveAllocation,
672 len: usize,
673 ) !void {
674 try self.lifetime.free(len);
675 try self.groups.getPtr(live.root).?.lifetime.free(len);
676 try self.source_lifetimes.getPtr(live.source).?.free(len);
677 }
678
679 fn resize(
680 self: *Analyzer,
681 live: LiveAllocation,
682 new_len: usize,
683 ) !void {
684 try self.lifetime.resize(live.len, new_len);
685 try self.groups.getPtr(live.root).?.lifetime.resize(
686 live.len,
687 new_len,
688 );
689 try self.source_lifetimes.getPtr(live.source).?.resize(
690 live.len,
691 new_len,
692 );
693 }
694
695 fn sourceKey(self: *const Analyzer, root: RootKey) SourceKey {
696 const definition = self.stack.stackDefinition(root.stack_id).?;
697 return .{
698 .kind = root.kind,
699 .succeeded = root.succeeded,
700 .layer = root.layer,
701 .producer = root.producer,
702 .site = definition.call_addresses[0],
703 .caller = callerAddress(definition),
704 };
705 }
706
707 fn collect(
708 self: *const Analyzer,
709 selection: analyze_mod.Selection,
710 ) !std.ArrayListUnmanaged(Summary) {
711 if (!self.finished) return error.CausalRootAnalyzerNotFinished;
712 var result = std.ArrayListUnmanaged(Summary).empty;
713 errdefer result.deinit(self.allocator);
714 try result.ensureTotalCapacity(self.allocator, self.groups.count());
715 var groups = self.groups.iterator();
716 while (groups.next()) |entry| {
717 if (!selection.includes(entry.key_ptr.kind)) continue;
718 result.appendAssumeCapacity(.{
719 .key = entry.key_ptr.*,
720 .counters = entry.value_ptr.*,
721 .definition = self.stack.stackDefinition(
722 entry.key_ptr.stack_id,
723 ).?,
724 });
725 }
726 return result;
727 }
728
729 fn totals(
730 self: *const Analyzer,
731 selection: analyze_mod.Selection,
732 ) !Totals {
733 if (!self.finished) return error.CausalRootAnalyzerNotFinished;
734 var result: Totals = .{};
735 var groups = self.groups.iterator();
736 while (groups.next()) |entry| {
737 if (!selection.includes(entry.key_ptr.kind)) continue;
738 result.roots = try std.math.add(
739 u64,
740 result.roots,
741 entry.value_ptr.roots,
742 );
743 result.root_requested_bytes = try std.math.add(
744 u128,
745 result.root_requested_bytes,
746 entry.value_ptr.root_requested_bytes,
747 );
748 if (entry.key_ptr.succeeded) {
749 result.successful = try std.math.add(
750 u64,
751 result.successful,
752 entry.value_ptr.roots,
753 );
754 } else {
755 result.failed = try std.math.add(
756 u64,
757 result.failed,
758 entry.value_ptr.roots,
759 );
760 }
761 try result.metrics.merge(entry.value_ptr.metrics);
762 }
763 result.lifetime = self.lifetime;
764 return result;
765 }
766 };
767
768 pub fn writeFromPath(
769 allocator: Allocator,
770 events_path: []const u8,
771 writer: *std.Io.Writer,
772 options: Options,
773 ) !void {
774 if (options.top == 0 or
775 options.frame_limit == 0 or
776 options.frame_limit > capture_mod.max_frames_limit)
777 {
778 return error.InvalidCausalRootReportLimit;
779 }
780 try options.window.validate();
781 var analyzer = Analyzer.init(allocator, options.window);
782 defer analyzer.deinit();
783 try ingestPath(&analyzer, events_path);
784 try analyzer.stack.validate();
785
786 var inferred_binary: ?[]u8 = null;
787 defer if (inferred_binary) |path| allocator.free(path);
788 const binary_path = options.binary_path orelse inferred: {
789 inferred_binary = try identity_mod.artifactPathAlloc(
790 allocator,
791 events_path,
792 );
793 break :inferred inferred_binary.?;
794 };
795 const actual_digest = identity_mod.fileDigest(
796 allocator,
797 binary_path,
798 ) catch |err| switch (err) {
799 error.FileNotFound => return error.MissingExecutableArtifact,
800 else => return err,
801 };
802 const expected_digest = analyzer.stack.executable_digest.?;
803 if (!std.mem.eql(u8, &actual_digest, &expected_digest)) {
804 return error.ExecutableIdentityMismatch;
805 }
806 analyzer.finishCausal() catch |err| switch (err) {
807 error.DanglingParentOperation => {
808 try writeDanglingReport(
809 allocator,
810 writer,
811 &analyzer,
812 options,
813 binary_path,
814 expected_digest,
815 );
816 return err;
817 },
818 else => return err,
819 };
820
821 var summaries = try analyzer.collect(options.selection);
822 defer summaries.deinit(allocator);
823 const totals = try analyzer.totals(options.selection);
824 std.mem.sort(Summary, summaries.items, options.sort, summaryGreaterThan);
825 const stack_limit = if (options.detail == .full)
826 @min(options.top, summaries.items.len)
827 else
828 0;
829 var sources = try collectSources(allocator, &analyzer, summaries.items);
830 defer sources.deinit(allocator);
831 std.mem.sort(Source, sources.items, options.sort, sourceGreaterThan);
832 const source_limit = if (options.detail == .summary)
833 0
834 else
835 @min(options.top, sources.items.len);
836 const display = Display{
837 .source_groups = sources.items.len,
838 .displayed_sources = source_limit,
839 .root_stacks = summaries.items.len,
840 .displayed_stacks = stack_limit,
841 };
842 var addresses = try collectAddresses(
843 allocator,
844 summaries.items[0..stack_limit],
845 sources.items[0..source_limit],
846 options.frame_limit,
847 );
848 defer addresses.deinit(allocator);
849 var symbols = if (addresses.items.len == 0)
850 null
851 else
852 try symbolize_mod.resolveAlloc(
853 allocator,
854 binary_path,
855 addresses.items,
856 );
857 defer if (symbols) |*resolved| resolved.deinit(allocator);
858 switch (options.format) {
859 .text => try writeText(
860 writer,
861 &analyzer,
862 totals,
863 options.selection,
864 options.sort,
865 options.window,
866 display,
867 sources.items[0..source_limit],
868 summaries.items[0..stack_limit],
869 symbols,
870 options.frame_limit,
871 expected_digest,
872 ),
873 .jsonl => try writeJsonl(
874 writer,
875 &analyzer,
876 totals,
877 options.selection,
878 options.sort,
879 options.window,
880 display,
881 sources.items[0..source_limit],
882 summaries.items[0..stack_limit],
883 symbols,
884 options.frame_limit,
885 expected_digest,
886 ),
887 }
888 }
889
890 fn ingestPath(analyzer: *Analyzer, path: []const u8) !void {
891 var file = try sys.fs.cwd().openFile(sys.fs.debugIo(), path, .{});
892 defer file.close(sys.fs.debugIo());
893 var buffer: [64 * 1024]u8 = undefined;
894 var reader = file.reader(sys.fs.debugIo(), &buffer);
895 while (true) {
896 const line = reader.interface.takeDelimiter('\n') catch |err| switch (err) {
897 error.ReadFailed => return reader.err.?,
898 else => return err,
899 };
900 const actual = line orelse break;
901 try analyzer.ingestJsonLine(actual);
902 }
903 }
904
905 fn writeDanglingReport(
906 allocator: Allocator,
907 writer: *std.Io.Writer,
908 analyzer: *const Analyzer,
909 options: Options,
910 binary_path: []const u8,
911 digest: identity_mod.Digest,
912 ) !void {
913 var dangling = try analyzer.collectDangling();
914 defer dangling.deinit(allocator);
915 const limit = @min(options.top, dangling.items.len);
916 var addresses = try collectDanglingAddresses(
917 allocator,
918 dangling.items[0..limit],
919 );
920 defer addresses.deinit(allocator);
921 var symbols = if (addresses.items.len == 0)
922 null
923 else
924 try symbolize_mod.resolveAlloc(
925 allocator,
926 binary_path,
927 addresses.items,
928 );
929 defer if (symbols) |*resolved| resolved.deinit(allocator);
930 switch (options.format) {
931 .text => try writeDanglingText(
932 writer,
933 analyzer,
934 dangling.items[0..limit],
935 dangling.items.len,
936 symbols,
937 digest,
938 ),
939 .jsonl => try writeDanglingJsonl(
940 writer,
941 analyzer,
942 dangling.items[0..limit],
943 dangling.items.len,
944 symbols,
945 digest,
946 ),
947 }
948 }
949
950 fn collectDanglingAddresses(
951 allocator: Allocator,
952 dangling: []const DanglingParent,
953 ) !std.ArrayListUnmanaged(u64) {
954 var seen = std.AutoHashMapUnmanaged(u64, void){};
955 defer seen.deinit(allocator);
956 var addresses = std.ArrayListUnmanaged(u64).empty;
957 errdefer addresses.deinit(allocator);
958 try addresses.ensureTotalCapacity(allocator, dangling.len);
959 for (dangling) |entry| {
960 const address = entry.site_address;
961 const result = try seen.getOrPut(allocator, address);
962 if (result.found_existing) continue;
963 addresses.appendAssumeCapacity(address);
964 }
965 std.mem.sort(u64, addresses.items, {}, lessThan);
966 return addresses;
967 }
968
969 fn writeDanglingText(
970 writer: *std.Io.Writer,
971 analyzer: *const Analyzer,
972 dangling: []const DanglingParent,
973 total: usize,
974 symbols: ?symbolize_mod.Symbols,
975 digest: identity_mod.Digest,
976 ) !void {
977 const digest_hex = std.fmt.bytesToHex(digest, .lower);
978 try writer.print(
979 "causal_roots_error code=dangling_parent_operation " ++
980 "diagnostic_universe=all_memory_operations missing_parents={d} " ++
981 "displayed_missing_parents={d} binary_sha256={s}\n",
982 .{ total, dangling.len, digest_hex },
983 );
984 for (dangling) |entry| {
985 const child = entry.child;
986 try writer.print(
987 "dangling_parent missing_parent_operation_id={d} " ++
988 "observed_child_operation_id={d} observed_child_sequence={d} " ++
989 "observed_child_scope_id={d} observed_child_operation={s} " ++
990 "observed_child_layer={s} observed_child_producer={s} " ++
991 "observed_child_outcome={s} observed_child_requested_bytes={d} " ++
992 "observed_child_stack_id={d} observed_child_scope=",
993 .{
994 entry.operation_id,
995 child.operation_id,
996 child.sequence,
997 child.scope_id,
998 child.kind.tag(),
999 child.layer.tag(),
1000 @tagName(child.producer),
1001 outcomeTag(child.succeeded),
1002 child.requested_bytes,
1003 child.stack_id,
1004 },
1005 );
1006 const scope = analyzer.stack.scopePath(child.scope_id) orelse "unknown";
1007 try pretty_json.writeString(writer, scope);
1008 try writeTextAddress(
1009 writer,
1010 " observed_child_site",
1011 entry.site_address,
1012 symbols,
1013 );
1014 try writer.writeByte('\n');
1015 }
1016 }
1017
1018 fn writeDanglingJsonl(
1019 writer: *std.Io.Writer,
1020 analyzer: *const Analyzer,
1021 dangling: []const DanglingParent,
1022 total: usize,
1023 symbols: ?symbolize_mod.Symbols,
1024 digest: identity_mod.Digest,
1025 ) !void {
1026 var header_stream = pretty_json.Writer.init(writer, .minified);
1027 const header = try header_stream.object();
1028 try header.field("kind", "causal_root_error");
1029 try header.field("code", "dangling_parent_operation");
1030 try header.field("diagnostic_universe", "all_memory_operations");
1031 try header.field("missing_parents", total);
1032 try header.field("displayed_missing_parents", dangling.len);
1033 try header.hexString("binary_sha256", &digest);
1034 try header.endLine();
1035 for (dangling) |entry| {
1036 try writeDanglingJsonEntry(writer, analyzer, entry, symbols);
1037 }
1038 }
1039
1040 fn writeDanglingJsonEntry(
1041 writer: *std.Io.Writer,
1042 analyzer: *const Analyzer,
1043 entry: DanglingParent,
1044 symbols: ?symbolize_mod.Symbols,
1045 ) !void {
1046 const child = entry.child;
1047 var stream = pretty_json.Writer.init(writer, .minified);
1048 const object = try stream.object();
1049 try object.field("kind", "causal_root_dangling_parent");
1050 try object.field("missing_parent_operation_id", entry.operation_id);
1051 try object.field("observed_child_operation_id", child.operation_id);
1052 try object.field("observed_child_sequence", child.sequence);
1053 try object.field("observed_child_scope_id", child.scope_id);
1054 try object.field("observed_child_scope", analyzer.stack.scopePath(
1055 child.scope_id,
1056 ));
1057 try object.field("observed_child_operation", child.kind.tag());
1058 try object.field("observed_child_layer", child.layer.tag());
1059 try object.field("observed_child_producer", @tagName(child.producer));
1060 try object.field("observed_child_succeeded", child.succeeded);
1061 try object.field("observed_child_requested_bytes", child.requested_bytes);
1062 try object.field("observed_child_stack_id", child.stack_id);
1063 const address = entry.site_address;
1064 try object.field("observed_child_site_address", address);
1065 try writeJsonSymbol(object, "observed_child_site", address, symbols);
1066 try object.endLine();
1067 }
1068
1069 fn collectSources(
1070 allocator: Allocator,
1071 analyzer: *const Analyzer,
1072 summaries: []const Summary,
1073 ) !std.ArrayListUnmanaged(Source) {
1074 var counts = std.AutoHashMapUnmanaged(SourceKey, Source){};
1075 defer counts.deinit(allocator);
1076 for (summaries) |summary| {
1077 const key = SourceKey{
1078 .kind = summary.key.kind,
1079 .succeeded = summary.key.succeeded,
1080 .layer = summary.key.layer,
1081 .producer = summary.key.producer,
1082 .site = summary.definition.call_addresses[0],
1083 .caller = callerAddress(summary.definition),
1084 };
1085 const entry = try counts.getOrPut(allocator, key);
1086 if (!entry.found_existing) {
1087 entry.value_ptr.* = .{
1088 .key = key,
1089 .counters = .{},
1090 .unique_stacks = 0,
1091 };
1092 }
1093 try entry.value_ptr.counters.merge(summary.counters);
1094 entry.value_ptr.unique_stacks = try std.math.add(
1095 u32,
1096 entry.value_ptr.unique_stacks,
1097 1,
1098 );
1099 }
1100 var sources = std.ArrayListUnmanaged(Source).empty;
1101 errdefer sources.deinit(allocator);
1102 try sources.ensureTotalCapacity(allocator, counts.count());
1103 var values = counts.valueIterator();
1104 while (values.next()) |source| sources.appendAssumeCapacity(source.*);
1105 for (sources.items) |*source| {
1106 source.counters.lifetime = analyzer.source_lifetimes.get(
1107 source.key,
1108 ) orelse .{};
1109 }
1110 return sources;
1111 }
1112
1113 fn collectAddresses(
1114 allocator: Allocator,
1115 summaries: []const Summary,
1116 sources: []const Source,
1117 frame_limit: usize,
1118 ) !std.ArrayListUnmanaged(u64) {
1119 var seen = std.AutoHashMapUnmanaged(u64, void){};
1120 defer seen.deinit(allocator);
1121 var addresses = std.ArrayListUnmanaged(u64).empty;
1122 errdefer addresses.deinit(allocator);
1123 for (sources) |source| {
1124 try appendAddress(allocator, &seen, &addresses, source.key.site);
1125 if (source.key.caller != 0) {
1126 try appendAddress(
1127 allocator,
1128 &seen,
1129 &addresses,
1130 source.key.caller,
1131 );
1132 }
1133 }
1134 for (summaries) |summary| {
1135 const limit = @min(
1136 frame_limit,
1137 summary.definition.call_addresses.len,
1138 );
1139 for (summary.definition.call_addresses[0..limit]) |address| {
1140 try appendAddress(allocator, &seen, &addresses, address);
1141 }
1142 }
1143 std.mem.sort(u64, addresses.items, {}, lessThan);
1144 return addresses;
1145 }
1146
1147 fn appendAddress(
1148 allocator: Allocator,
1149 seen: *std.AutoHashMapUnmanaged(u64, void),
1150 addresses: *std.ArrayListUnmanaged(u64),
1151 address: u64,
1152 ) !void {
1153 const entry = try seen.getOrPut(allocator, address);
1154 if (entry.found_existing) return;
1155 try addresses.append(allocator, address);
1156 }
1157
1158 fn callerAddress(definition: analyze_mod.Definition) u64 {
1159 const site = definition.call_addresses[0];
1160 for (definition.call_addresses[1..], 1..) |address, index| {
1161 if (address != site) continue;
1162 const caller_index = index + 1;
1163 if (caller_index < definition.call_addresses.len) {
1164 return definition.call_addresses[caller_index];
1165 }
1166 return 0;
1167 }
1168 return 0;
1169 }
1170
1171 fn summaryGreaterThan(sort: Sort, left: Summary, right: Summary) bool {
1172 if (counterOrder(sort, left.counters, right.counters)) |order| {
1173 return order;
1174 }
1175 if (left.counters.roots != right.counters.roots) {
1176 return left.counters.roots > right.counters.roots;
1177 }
1178 if (left.counters.root_requested_bytes !=
1179 right.counters.root_requested_bytes)
1180 {
1181 return left.counters.root_requested_bytes >
1182 right.counters.root_requested_bytes;
1183 }
1184 if (left.counters.metrics.operations != right.counters.metrics.operations) {
1185 return left.counters.metrics.operations >
1186 right.counters.metrics.operations;
1187 }
1188 return keyLessThan(left.key, right.key);
1189 }
1190
1191 fn sourceGreaterThan(sort: Sort, left: Source, right: Source) bool {
1192 if (counterOrder(sort, left.counters, right.counters)) |order| {
1193 return order;
1194 }
1195 if (left.counters.roots != right.counters.roots) {
1196 return left.counters.roots > right.counters.roots;
1197 }
1198 if (left.counters.root_requested_bytes !=
1199 right.counters.root_requested_bytes)
1200 {
1201 return left.counters.root_requested_bytes >
1202 right.counters.root_requested_bytes;
1203 }
1204 if (left.key.site != right.key.site) return left.key.site < right.key.site;
1205 if (left.key.caller != right.key.caller) {
1206 return left.key.caller < right.key.caller;
1207 }
1208 return sourceKeyLessThan(left.key, right.key);
1209 }
1210
1211 fn counterOrder(
1212 sort: Sort,
1213 left: Counters,
1214 right: Counters,
1215 ) ?bool {
1216 const left_value: u128 = switch (sort) {
1217 .roots => left.roots,
1218 .requested_bytes => left.root_requested_bytes,
1219 .backing_bytes => left.metrics.backing_requested_bytes,
1220 .high_water => left.lifetime.high_water_live_bytes,
1221 };
1222 const right_value: u128 = switch (sort) {
1223 .roots => right.roots,
1224 .requested_bytes => right.root_requested_bytes,
1225 .backing_bytes => right.metrics.backing_requested_bytes,
1226 .high_water => right.lifetime.high_water_live_bytes,
1227 };
1228 if (left_value == right_value) return null;
1229 return left_value > right_value;
1230 }
1231
1232 fn keyLessThan(left: RootKey, right: RootKey) bool {
1233 if (left.kind != right.kind) {
1234 return @backingInt(left.kind) < @backingInt(right.kind);
1235 }
1236 if (left.layer != right.layer) {
1237 return @backingInt(left.layer) < @backingInt(right.layer);
1238 }
1239 if (left.producer != right.producer) {
1240 return @backingInt(left.producer) < @backingInt(right.producer);
1241 }
1242 if (left.succeeded != right.succeeded) return left.succeeded;
1243 return left.stack_id < right.stack_id;
1244 }
1245
1246 fn sourceKeyLessThan(left: SourceKey, right: SourceKey) bool {
1247 if (left.kind != right.kind) {
1248 return @backingInt(left.kind) < @backingInt(right.kind);
1249 }
1250 if (left.layer != right.layer) {
1251 return @backingInt(left.layer) < @backingInt(right.layer);
1252 }
1253 if (left.producer != right.producer) {
1254 return @backingInt(left.producer) < @backingInt(right.producer);
1255 }
1256 return left.succeeded and !right.succeeded;
1257 }
1258
1259 fn selectionTag(selection: analyze_mod.Selection) []const u8 {
1260 return switch (selection) {
1261 .allocations => "allocations",
1262 .all => "all",
1263 };
1264 }
1265
1266 fn sortTag(sort: Sort) []const u8 {
1267 return switch (sort) {
1268 .roots => "roots",
1269 .requested_bytes => "requested_bytes",
1270 .backing_bytes => "backing_bytes",
1271 .high_water => "high_water",
1272 };
1273 }
1274
1275 fn allocationKey(allocation_id: u64, address: u64) u64 {
1276 return if (allocation_id != 0) allocation_id else address;
1277 }
1278
1279 fn outcomeTag(succeeded: bool) []const u8 {
1280 return if (succeeded) "success" else "failure";
1281 }
1282
1283 fn requestBytes(event: event_mod.ReplayEvent) usize {
1284 return switch (event.kind) {
1285 .free, .release, .unmap => event.old_len,
1286 .alloc, .resize, .remap, .map, .protect, .discard, .decommit, .advise => event.len,
1287 else => unreachable,
1288 };
1289 }
1290
1291 fn lessThan(_: void, left: u64, right: u64) bool {
1292 return left < right;
1293 }
1294
1295 fn danglingParentLessThan(
1296 _: void,
1297 left: DanglingParent,
1298 right: DanglingParent,
1299 ) bool {
1300 if (left.operation_id != right.operation_id) {
1301 return left.operation_id < right.operation_id;
1302 }
1303 if (left.child.sequence != right.child.sequence) {
1304 return left.child.sequence < right.child.sequence;
1305 }
1306 return left.child.operation_id < right.child.operation_id;
1307 }
1308
1309 fn writeText(
1310 writer: *std.Io.Writer,
1311 analyzer: *const Analyzer,
1312 totals: Totals,
1313 selection: analyze_mod.Selection,
1314 sort: Sort,
1315 window: analyze_mod.Window,
1316 display: Display,
1317 sources: []const Source,
1318 summaries: []const Summary,
1319 maybe_symbols: ?symbolize_mod.Symbols,
1320 frame_limit: usize,
1321 digest: identity_mod.Digest,
1322 ) !void {
1323 const digest_hex = std.fmt.bytesToHex(digest, .lower);
1324 const coverage = analyzer.stack.coverage.?;
1325 try writer.print(
1326 "causal_roots status={s} universe={s} selection={s} sort={s} " ++
1327 "roots={d} " ++
1328 "successful={d} failed={d} root_requested_bytes={d} " ++
1329 "live_allocations={d} live_bytes={d} " ++
1330 "high_water_live_bytes={d} " ++
1331 "causal_operations={d} logical_operations={d} " ++
1332 "backing_operations={d} physical_operations={d} " ++
1333 "backing_requested_bytes={d} physical_requested_bytes={d} " ++
1334 "max_depth={d} source_groups={d} displayed_sources={d} " ++
1335 "root_stacks={d} displayed_stacks={d} binary_sha256={s}",
1336 .{
1337 coverage.statusTag(),
1338 coverage.universe.tag(),
1339 selectionTag(selection),
1340 sortTag(sort),
1341 totals.roots,
1342 totals.successful,
1343 totals.failed,
1344 totals.root_requested_bytes,
1345 totals.lifetime.live_allocations,
1346 totals.lifetime.live_bytes,
1347 totals.lifetime.high_water_live_bytes,
1348 totals.metrics.operations,
1349 totals.metrics.logical_operations,
1350 totals.metrics.backing_operations,
1351 totals.metrics.physical_operations,
1352 totals.metrics.backing_requested_bytes,
1353 totals.metrics.physical_requested_bytes,
1354 totals.metrics.max_depth,
1355 display.source_groups,
1356 display.displayed_sources,
1357 display.root_stacks,
1358 display.displayed_stacks,
1359 digest_hex,
1360 },
1361 );
1362 try writeTextWindow(writer, window, "root_operation");
1363 try writer.writeByte('\n');
1364 try writeCoverage(writer, coverage);
1365 for (sources) |source| {
1366 try writer.print(
1367 "root_source layer={s} producer={s} operation={s} outcome={s} " ++
1368 "roots={d} root_requested_bytes={d} live_allocations={d} " ++
1369 "live_bytes={d} high_water_live_bytes={d} " ++
1370 "causal_operations={d} " ++
1371 "logical_operations={d} backing_operations={d} " ++
1372 "physical_operations={d} backing_requested_bytes={d} " ++
1373 "physical_requested_bytes={d} max_depth={d} unique_stacks={d}",
1374 .{
1375 source.key.layer.tag(),
1376 @tagName(source.key.producer),
1377 source.key.kind.tag(),
1378 outcomeTag(source.key.succeeded),
1379 source.counters.roots,
1380 source.counters.root_requested_bytes,
1381 source.counters.lifetime.live_allocations,
1382 source.counters.lifetime.live_bytes,
1383 source.counters.lifetime.high_water_live_bytes,
1384 source.counters.metrics.operations,
1385 source.counters.metrics.logical_operations,
1386 source.counters.metrics.backing_operations,
1387 source.counters.metrics.physical_operations,
1388 source.counters.metrics.backing_requested_bytes,
1389 source.counters.metrics.physical_requested_bytes,
1390 source.counters.metrics.max_depth,
1391 source.unique_stacks,
1392 },
1393 );
1394 try writeTextAddress(
1395 writer,
1396 " site",
1397 source.key.site,
1398 maybe_symbols,
1399 );
1400 if (source.key.caller != 0) {
1401 try writeTextAddress(
1402 writer,
1403 " caller",
1404 source.key.caller,
1405 maybe_symbols,
1406 );
1407 } else {
1408 try writer.writeAll(" caller=unavailable");
1409 }
1410 try writer.writeByte('\n');
1411 }
1412 for (summaries) |summary| {
1413 const displayed = @min(
1414 frame_limit,
1415 summary.definition.call_addresses.len,
1416 );
1417 try writer.print(
1418 "root_stack id={d} layer={s} producer={s} operation={s} " ++
1419 "outcome={s} roots={d} root_requested_bytes={d} " ++
1420 "live_allocations={d} live_bytes={d} " ++
1421 "high_water_live_bytes={d} " ++
1422 "causal_operations={d} logical_operations={d} " ++
1423 "backing_operations={d} physical_operations={d} " ++
1424 "backing_requested_bytes={d} physical_requested_bytes={d} " ++
1425 "max_depth={d} captured_frames={d} displayed_frames={d}\n",
1426 .{
1427 summary.key.stack_id,
1428 summary.key.layer.tag(),
1429 @tagName(summary.key.producer),
1430 summary.key.kind.tag(),
1431 outcomeTag(summary.key.succeeded),
1432 summary.counters.roots,
1433 summary.counters.root_requested_bytes,
1434 summary.counters.lifetime.live_allocations,
1435 summary.counters.lifetime.live_bytes,
1436 summary.counters.lifetime.high_water_live_bytes,
1437 summary.counters.metrics.operations,
1438 summary.counters.metrics.logical_operations,
1439 summary.counters.metrics.backing_operations,
1440 summary.counters.metrics.physical_operations,
1441 summary.counters.metrics.backing_requested_bytes,
1442 summary.counters.metrics.physical_requested_bytes,
1443 summary.counters.metrics.max_depth,
1444 summary.definition.call_addresses.len,
1445 displayed,
1446 },
1447 );
1448 try writeTextFrames(
1449 writer,
1450 summary.definition,
1451 displayed,
1452 maybe_symbols,
1453 );
1454 }
1455 }
1456
1457 fn writeCoverage(
1458 writer: *std.Io.Writer,
1459 coverage: coverage_mod.Manifest,
1460 ) !void {
1461 try writer.print(
1462 "coverage child_allocator_fast_paths={s} sys_memory_operations={s} " ++
1463 "direct_os_memory_operations={s} unowned_allocator_producers={s} " ++
1464 "foreign_allocations={s} observer_control={s} " ++
1465 "observer_control_operations={d} zero_length_operations={s} " ++
1466 "predispatch_failures={s}\n",
1467 .{
1468 coverage.child_allocator_fast_paths.tag(),
1469 coverage.sys_memory_operations.tag(),
1470 coverage.direct_os_memory_operations.tag(),
1471 coverage.unowned_allocator_producers.tag(),
1472 coverage.foreign_allocations.tag(),
1473 coverage.observer_control.tag(),
1474 coverage.observer_control_operations,
1475 coverage.zero_length_operations.tag(),
1476 coverage.predispatch_failures.tag(),
1477 },
1478 );
1479 }
1480
1481 fn writeTextFrames(
1482 writer: *std.Io.Writer,
1483 definition: analyze_mod.Definition,
1484 displayed: usize,
1485 maybe_symbols: ?symbolize_mod.Symbols,
1486 ) !void {
1487 for (definition.call_addresses[0..displayed], 0..) |address, frame_index| {
1488 const resolved = if (maybe_symbols) |symbols|
1489 symbols.find(address)
1490 else
1491 &.{};
1492 try writer.print(
1493 " frame={d} kind={s} call_address=0x{x}",
1494 .{
1495 frame_index,
1496 if (frame_index == 0) "root_site" else "physical",
1497 address,
1498 },
1499 );
1500 if (resolved.len != 0) {
1501 try writer.writeAll(" function=");
1502 try pretty_json.writeString(writer, resolved[0].function);
1503 try writer.writeAll(" location=");
1504 try pretty_json.writeString(writer, resolved[0].location);
1505 }
1506 try writer.writeByte('\n');
1507 for (resolved[1..], 1..) |inline_frame, inline_index| {
1508 try writer.print(" inline={d} function=", .{inline_index});
1509 try pretty_json.writeString(writer, inline_frame.function);
1510 try writer.writeAll(" location=");
1511 try pretty_json.writeString(writer, inline_frame.location);
1512 try writer.writeByte('\n');
1513 }
1514 }
1515 }
1516
1517 fn writeTextAddress(
1518 writer: *std.Io.Writer,
1519 prefix: []const u8,
1520 address: u64,
1521 maybe_symbols: ?symbolize_mod.Symbols,
1522 ) !void {
1523 try writer.print("{s}_address=0x{x}", .{ prefix, address });
1524 const resolved = if (maybe_symbols) |symbols|
1525 symbols.find(address)
1526 else
1527 &.{};
1528 if (resolved.len == 0) return;
1529 try writer.print("{s}_function=", .{prefix});
1530 try pretty_json.writeString(writer, resolved[0].function);
1531 try writer.print("{s}_location=", .{prefix});
1532 try pretty_json.writeString(writer, resolved[0].location);
1533 if (resolved.len == 1) return;
1534 const owner = resolved[resolved.len - 1];
1535 try writer.print("{s}_owner_function=", .{prefix});
1536 try pretty_json.writeString(writer, owner.function);
1537 try writer.print("{s}_owner_location=", .{prefix});
1538 try pretty_json.writeString(writer, owner.location);
1539 }
1540
1541 fn writeTextWindow(
1542 writer: *std.Io.Writer,
1543 window: analyze_mod.Window,
1544 anchor: []const u8,
1545 ) !void {
1546 try writer.writeAll(" window_scope=");
1547 if (window.scope) |scope| {
1548 try pretty_json.writeString(writer, scope);
1549 } else {
1550 try writer.writeAll("all");
1551 }
1552 try writer.writeAll(" window_scope_match=subtree window_first_sequence=");
1553 try writeOptionalSequence(writer, window.first_sequence);
1554 try writer.writeAll(" window_last_sequence=");
1555 try writeOptionalSequence(writer, window.last_sequence);
1556 try writer.print(" window_sequence_bounds=inclusive window_anchor={s}", .{
1557 anchor,
1558 });
1559 }
1560
1561 fn writeOptionalSequence(writer: *std.Io.Writer, sequence: ?u64) !void {
1562 if (sequence) |value| {
1563 try writer.print("{d}", .{value});
1564 } else {
1565 try writer.writeAll("all");
1566 }
1567 }
1568
1569 fn writeJsonWindow(
1570 object: pretty_json.Object,
1571 window: analyze_mod.Window,
1572 anchor: []const u8,
1573 ) !void {
1574 try object.field("window_scope", window.scope);
1575 try object.field("window_scope_match", "subtree");
1576 try object.field("window_first_sequence", window.first_sequence);
1577 try object.field("window_last_sequence", window.last_sequence);
1578 try object.field("window_sequence_bounds", "inclusive");
1579 try object.field("window_anchor", anchor);
1580 }
1581
1582 fn writeCounterFields(object: pretty_json.Object, counters: anytype) !void {
1583 try object.field("roots", counters.roots);
1584 try object.field("root_requested_bytes", counters.root_requested_bytes);
1585 try object.field("live_allocations", counters.lifetime.live_allocations);
1586 try object.field("live_bytes", counters.lifetime.live_bytes);
1587 try object.field("high_water_live_bytes", counters.lifetime.high_water_live_bytes);
1588 try object.field("causal_operations", counters.metrics.operations);
1589 try object.field("logical_operations", counters.metrics.logical_operations);
1590 try object.field("backing_operations", counters.metrics.backing_operations);
1591 try object.field("physical_operations", counters.metrics.physical_operations);
1592 try object.field("backing_requested_bytes", counters.metrics.backing_requested_bytes);
1593 try object.field("physical_requested_bytes", counters.metrics.physical_requested_bytes);
1594 try object.field("max_depth", counters.metrics.max_depth);
1595 }
1596
1597 fn writeJsonl(
1598 writer: *std.Io.Writer,
1599 analyzer: *const Analyzer,
1600 totals: Totals,
1601 selection: analyze_mod.Selection,
1602 sort: Sort,
1603 window: analyze_mod.Window,
1604 display: Display,
1605 sources: []const Source,
1606 summaries: []const Summary,
1607 maybe_symbols: ?symbolize_mod.Symbols,
1608 frame_limit: usize,
1609 digest: identity_mod.Digest,
1610 ) !void {
1611 const coverage = analyzer.stack.coverage.?;
1612 var summary_stream = pretty_json.Writer.init(writer, .minified);
1613 const header = try summary_stream.object();
1614 try header.field("kind", "causal_root_summary");
1615 try header.field("status", coverage.statusTag());
1616 try header.field("universe", coverage.universe.tag());
1617 try header.field("selection", selectionTag(selection));
1618 try header.field("sort", sortTag(sort));
1619 try header.field("roots", totals.roots);
1620 try header.field("successful", totals.successful);
1621 try header.field("failed", totals.failed);
1622 try header.field("root_requested_bytes", totals.root_requested_bytes);
1623 try header.field("live_allocations", totals.lifetime.live_allocations);
1624 try header.field("live_bytes", totals.lifetime.live_bytes);
1625 try header.field("high_water_live_bytes", totals.lifetime.high_water_live_bytes);
1626 try header.field("causal_operations", totals.metrics.operations);
1627 try header.field("logical_operations", totals.metrics.logical_operations);
1628 try header.field("backing_operations", totals.metrics.backing_operations);
1629 try header.field("physical_operations", totals.metrics.physical_operations);
1630 try header.field("backing_requested_bytes", totals.metrics.backing_requested_bytes);
1631 try header.field("physical_requested_bytes", totals.metrics.physical_requested_bytes);
1632 try header.field("max_depth", totals.metrics.max_depth);
1633 try header.field("source_groups", display.source_groups);
1634 try header.field("displayed_sources", display.displayed_sources);
1635 try header.field("root_stacks", display.root_stacks);
1636 try header.field("displayed_stacks", display.displayed_stacks);
1637 try header.hexString("binary_sha256", &digest);
1638 try writeJsonWindow(header, window, "root_operation");
1639 try header.field("child_allocator_fast_paths", coverage.child_allocator_fast_paths.tag());
1640 try header.field("sys_memory_operations", coverage.sys_memory_operations.tag());
1641 try header.field("direct_os_memory_operations", coverage.direct_os_memory_operations.tag());
1642 try header.field("unowned_allocator_producers", coverage.unowned_allocator_producers.tag());
1643 try header.field("foreign_allocations", coverage.foreign_allocations.tag());
1644 try header.field("observer_control", coverage.observer_control.tag());
1645 try header.field("observer_control_operations", coverage.observer_control_operations);
1646 try header.field("zero_length_operations", coverage.zero_length_operations.tag());
1647 try header.field("predispatch_failures", coverage.predispatch_failures.tag());
1648 try header.endLine();
1649 for (sources) |source| {
1650 var stream = pretty_json.Writer.init(writer, .minified);
1651 const object = try stream.object();
1652 try object.field("kind", "causal_root_source");
1653 try object.field("layer", source.key.layer.tag());
1654 try object.field("producer", @tagName(source.key.producer));
1655 try object.field("operation", source.key.kind.tag());
1656 try object.field("succeeded", source.key.succeeded);
1657 try writeCounterFields(object, source.counters);
1658 try object.field("unique_stacks", source.unique_stacks);
1659 try object.field("site_address", source.key.site);
1660 const caller_address: ?u64 = if (source.key.caller == 0) null else source.key.caller;
1661 try object.field("caller_address", caller_address);
1662 try writeJsonSymbol(
1663 object,
1664 "site",
1665 source.key.site,
1666 maybe_symbols,
1667 );
1668 if (source.key.caller != 0) {
1669 try writeJsonSymbol(
1670 object,
1671 "caller",
1672 source.key.caller,
1673 maybe_symbols,
1674 );
1675 }
1676 try object.endLine();
1677 }
1678 for (summaries) |summary| {
1679 const displayed = @min(
1680 frame_limit,
1681 summary.definition.call_addresses.len,
1682 );
1683 var stream = pretty_json.Writer.init(writer, .minified);
1684 const object = try stream.object();
1685 try object.field("kind", "causal_root_stack");
1686 try object.field("stack_id", summary.key.stack_id);
1687 try object.field("layer", summary.key.layer.tag());
1688 try object.field("producer", @tagName(summary.key.producer));
1689 try object.field("operation", summary.key.kind.tag());
1690 try object.field("succeeded", summary.key.succeeded);
1691 try writeCounterFields(object, summary.counters);
1692 try object.field("captured_frames", summary.definition.call_addresses.len);
1693 try object.field("displayed_frames", displayed);
1694 try object.endLine();
1695 for (
1696 summary.definition.call_addresses[0..displayed],
1697 0..,
1698 ) |address, frame_index| {
1699 const resolved = if (maybe_symbols) |symbols|
1700 symbols.find(address)
1701 else
1702 &.{};
1703 if (resolved.len == 0) {
1704 try writeJsonFrame(
1705 writer,
1706 summary.key.stack_id,
1707 frame_index,
1708 address,
1709 0,
1710 "",
1711 "",
1712 );
1713 continue;
1714 }
1715 for (resolved, 0..) |inline_frame, inline_index| {
1716 try writeJsonFrame(
1717 writer,
1718 summary.key.stack_id,
1719 frame_index,
1720 address,
1721 inline_index,
1722 inline_frame.function,
1723 inline_frame.location,
1724 );
1725 }
1726 }
1727 }
1728 }
1729
1730 fn writeJsonSymbol(
1731 object: pretty_json.Object,
1732 prefix: []const u8,
1733 address: u64,
1734 maybe_symbols: ?symbolize_mod.Symbols,
1735 ) !void {
1736 const resolved = if (maybe_symbols) |symbols|
1737 symbols.find(address)
1738 else
1739 &.{};
1740 if (resolved.len == 0) return;
1741 try object.fieldParts(&.{ prefix, "_function" }, resolved[0].function);
1742 try object.fieldParts(&.{ prefix, "_location" }, resolved[0].location);
1743 if (resolved.len == 1) return;
1744 const owner = resolved[resolved.len - 1];
1745 try object.fieldParts(&.{ prefix, "_owner_function" }, owner.function);
1746 try object.fieldParts(&.{ prefix, "_owner_location" }, owner.location);
1747 }
1748
1749 fn writeJsonFrame(
1750 writer: *std.Io.Writer,
1751 stack_id: u32,
1752 frame_index: usize,
1753 address: u64,
1754 inline_index: usize,
1755 function: []const u8,
1756 location: []const u8,
1757 ) !void {
1758 var stream = pretty_json.Writer.init(writer, .minified);
1759 const object = try stream.object();
1760 try object.field("kind", "causal_root_stack_frame");
1761 try object.field("stack_id", stack_id);
1762 try object.field("frame", frame_index);
1763 try object.field("frame_kind", if (frame_index == 0) "root_site" else "physical");
1764 try object.field("call_address", address);
1765 try object.field("inline", inline_index);
1766 try object.field("function", function);
1767 try object.field("location", location);
1768 try object.endLine();
1769 }
1770
1771 fn fixture(analyzer: *Analyzer, dangling: bool) !void {
1772 const digest: identity_mod.Digest = @splat(0xaa);
1773 var input = std.Io.Writer.Allocating.init(std.testing.allocator);
1774 defer input.deinit();
1775 try identity_mod.writeMetadata(&input.writer, digest);
1776 try coverage_mod.writeMetadata(
1777 &input.writer,
1778 coverage_mod.ownedProducerManifest(),
1779 );
1780 var addresses = [_]usize{ 1, 2 };
1781 try capture_mod.writeDefinition(&input.writer, 1, .{
1782 .addresses = &addresses,
1783 .truncated = false,
1784 .unwind_failed = false,
1785 .missing_return_address = false,
1786 .collision_next = 0,
1787 });
1788 try (event_mod.Event{
1789 .seq = 1,
1790 .kind = .trace_start,
1791 }).writeJsonLine(&input.writer, null, null);
1792 try (event_mod.Event{
1793 .seq = 2,
1794 .kind = .map,
1795 .len = 128,
1796 .stack_id = 1,
1797 .layer = .physical_page,
1798 .operation_id = 3,
1799 .parent_operation_id = 2,
1800 .producer = .sys_memory,
1801 }).writeJsonLine(&input.writer, null, null);
1802 try (event_mod.Event{
1803 .seq = 3,
1804 .kind = .alloc,
1805 .len = 64,
1806 .stack_id = 1,
1807 .operation_id = 2,
1808 .parent_operation_id = 1,
1809 }).writeJsonLine(&input.writer, null, null);
1810 if (!dangling) {
1811 try (event_mod.Event{
1812 .seq = 4,
1813 .kind = .alloc,
1814 .allocation_id = 1,
1815 .address = 4096,
1816 .len = 16,
1817 .stack_id = 1,
1818 .layer = .logical_allocator,
1819 .operation_id = 1,
1820 .producer = .arena,
1821 }).writeJsonLine(&input.writer, null, null);
1822 try (event_mod.Event{
1823 .seq = 5,
1824 .kind = .alloc,
1825 .allocation_id = 2,
1826 .address = 8192,
1827 .len = 32,
1828 .stack_id = 1,
1829 .layer = .logical_allocator,
1830 .operation_id = 4,
1831 .producer = .arena,
1832 }).writeJsonLine(&input.writer, null, null);
1833 }
1834 try (event_mod.Event{
1835 .seq = if (dangling) 4 else 6,
1836 .kind = .trace_stop,
1837 }).writeJsonLine(&input.writer, null, null);
1838 var lines = std.mem.splitScalar(u8, input.written(), '\n');
1839 while (lines.next()) |line| try analyzer.ingestJsonLine(line);
1840 }
1841
1842 test "causal roots collapse nested allocator layers" {
1843 var analyzer = Analyzer.init(std.testing.allocator, .{});
1844 defer analyzer.deinit();
1845 try fixture(&analyzer, false);
1846 try analyzer.finish();
1847 const totals = try analyzer.totals(.allocations);
1848 try std.testing.expectEqual(@as(u64, 2), totals.roots);
1849 try std.testing.expectEqual(@as(u128, 48), totals.root_requested_bytes);
1850 try std.testing.expectEqual(@as(u64, 4), totals.metrics.operations);
1851 try std.testing.expectEqual(@as(u64, 2), totals.metrics.logical_operations);
1852 try std.testing.expectEqual(@as(u64, 1), totals.metrics.backing_operations);
1853 try std.testing.expectEqual(@as(u64, 1), totals.metrics.physical_operations);
1854 try std.testing.expectEqual(@as(u32, 2), totals.metrics.max_depth);
1855 try std.testing.expectEqual(@as(u64, 2), totals.lifetime.live_allocations);
1856 try std.testing.expectEqual(@as(u128, 48), totals.lifetime.live_bytes);
1857 try std.testing.expectEqual(
1858 @as(u128, 48),
1859 totals.lifetime.high_water_live_bytes,
1860 );
1861 }
1862
1863 test "causal roots reject a missing parent operation" {
1864 var analyzer = Analyzer.init(std.testing.allocator, .{});
1865 defer analyzer.deinit();
1866 try fixture(&analyzer, true);
1867 try std.testing.expectError(
1868 error.DanglingParentOperation,
1869 analyzer.finish(),
1870 );
1871 }
1872
1873 test "causal roots identify each missing parent through its observed child site" {
1874 var analyzer = Analyzer.init(std.testing.allocator, .{});
1875 defer analyzer.deinit();
1876 try fixture(&analyzer, true);
1877 try analyzer.stack.validate();
1878 var dangling = try analyzer.collectDangling();
1879 defer dangling.deinit(std.testing.allocator);
1880
1881 try std.testing.expectEqual(@as(usize, 1), dangling.items.len);
1882 try std.testing.expectEqual(@as(u64, 1), dangling.items[0].operation_id);
1883 try std.testing.expectEqual(@as(u64, 2), dangling.items[0].child.operation_id);
1884 try std.testing.expectEqual(@as(u64, 1), dangling.items[0].site_address);
1885
1886 var output = std.Io.Writer.Allocating.init(std.testing.allocator);
1887 defer output.deinit();
1888 const digest: identity_mod.Digest = @splat(0xaa);
1889 try writeDanglingText(
1890 &output.writer,
1891 &analyzer,
1892 dangling.items,
1893 dangling.items.len,
1894 null,
1895 digest,
1896 );
1897 try std.testing.expect(std.mem.indexOf(
1898 u8,
1899 output.written(),
1900 "missing_parent_operation_id=1 observed_child_operation_id=2",
1901 ) != null);
1902 try std.testing.expect(std.mem.indexOf(
1903 u8,
1904 output.written(),
1905 "observed_child_site_address=0x1",
1906 ) != null);
1907 }
1908
1909 test "causal root window anchors a complete tree at its root operation" {
1910 var analyzer = Analyzer.init(std.testing.allocator, .{
1911 .scope = "root/phase",
1912 .first_sequence = 4,
1913 .last_sequence = 4,
1914 });
1915 defer analyzer.deinit();
1916 try filteredRootFixture(&analyzer);
1917 try analyzer.finish();
1918
1919 const totals = try analyzer.totals(.allocations);
1920 try std.testing.expectEqual(@as(u64, 1), totals.roots);
1921 try std.testing.expectEqual(@as(u128, 16), totals.root_requested_bytes);
1922 try std.testing.expectEqual(@as(u64, 2), totals.metrics.operations);
1923 try std.testing.expectEqual(@as(u64, 1), totals.metrics.logical_operations);
1924 try std.testing.expectEqual(@as(u64, 1), totals.metrics.backing_operations);
1925 try std.testing.expectEqual(@as(u128, 64), totals.metrics.backing_requested_bytes);
1926 }
1927
1928 fn filteredRootFixture(analyzer: *Analyzer) !void {
1929 const digest: identity_mod.Digest = @splat(0xaa);
1930 var input = std.Io.Writer.Allocating.init(std.testing.allocator);
1931 defer input.deinit();
1932 try identity_mod.writeMetadata(&input.writer, digest);
1933 try coverage_mod.writeMetadata(
1934 &input.writer,
1935 coverage_mod.ownedProducerManifest(),
1936 );
1937 var addresses = [_]usize{ 1, 2 };
1938 try capture_mod.writeDefinition(&input.writer, 1, .{
1939 .addresses = &addresses,
1940 .truncated = false,
1941 .unwind_failed = false,
1942 .missing_return_address = false,
1943 .collision_next = 0,
1944 });
1945 const lines = [_][]const u8{
1946 "{\"v\":3,\"seq\":1,\"kind\":\"trace.start\"}",
1947 "{\"v\":3,\"seq\":2,\"kind\":\"scope.enter\",\"scope_id\":1," ++
1948 "\"scope\":\"root/phase\"}",
1949 "{\"v\":3,\"seq\":3,\"kind\":\"alloc\",\"scope_id\":1,\"len\":64," ++
1950 "\"stack_id\":1,\"operation_id\":2,\"parent_operation_id\":1}",
1951 "{\"v\":3,\"seq\":4,\"kind\":\"alloc\",\"scope_id\":1," ++
1952 "\"allocation_id\":1,\"address\":4096,\"len\":16,\"stack_id\":1," ++
1953 "\"layer\":\"logical_allocator\",\"operation_id\":1," ++
1954 "\"producer\":\"arena\"}",
1955 "{\"v\":3,\"seq\":5,\"kind\":\"scope.exit\",\"scope_id\":1}",
1956 "{\"v\":3,\"seq\":6,\"kind\":\"scope.enter\",\"scope_id\":2," ++
1957 "\"scope\":\"root/other\"}",
1958 "{\"v\":3,\"seq\":7,\"kind\":\"alloc\",\"scope_id\":2,\"len\":128," ++
1959 "\"stack_id\":1,\"operation_id\":4,\"parent_operation_id\":3}",
1960 "{\"v\":3,\"seq\":8,\"kind\":\"alloc\",\"scope_id\":2," ++
1961 "\"allocation_id\":2,\"address\":8192,\"len\":32,\"stack_id\":1," ++
1962 "\"layer\":\"logical_allocator\",\"operation_id\":3," ++
1963 "\"producer\":\"arena\"}",
1964 "{\"v\":3,\"seq\":9,\"kind\":\"free\",\"scope_id\":2,\"address\":99," ++
1965 "\"len\":8,\"old_len\":8,\"stack_id\":1,\"operation_id\":6," ++
1966 "\"parent_operation_id\":5}",
1967 "{\"v\":3,\"seq\":10,\"kind\":\"release\",\"scope_id\":2," ++
1968 "\"address\":99,\"len\":8,\"old_len\":8,\"stack_id\":1," ++
1969 "\"operation_id\":5}",
1970 "{\"v\":3,\"seq\":11,\"kind\":\"lifecycle\",\"scope_id\":2," ++
1971 "\"return_address\":2,\"operation_id\":5}",
1972 "{\"v\":3,\"seq\":12,\"kind\":\"scope.exit\",\"scope_id\":2}",
1973 "{\"v\":3,\"seq\":13,\"kind\":\"trace.stop\"}",
1974 };
1975 var metadata = std.mem.splitScalar(u8, input.written(), '\n');
1976 while (metadata.next()) |line| try analyzer.ingestJsonLine(line);
1977 for (lines) |line| try analyzer.ingestJsonLine(line);
1978 }
1979
1980 test "causal root source ordering is irreflexive" {
1981 const source = Source{
1982 .key = .{
1983 .kind = .alloc,
1984 .succeeded = true,
1985 .layer = .logical_allocator,
1986 .producer = .arena,
1987 .site = 1,
1988 .caller = 2,
1989 },
1990 .counters = .{},
1991 .unique_stacks = 1,
1992 };
1993 try std.testing.expect(!sourceGreaterThan(.high_water, source, source));
1994 }
1995
1996 test "causal root lifetime tracks resize and release" {
1997 var lifetime = Lifetime{};
1998 try lifetime.allocate(16);
1999 try lifetime.resize(16, 40);
2000 try lifetime.resize(40, 24);
2001 try lifetime.free(24);
2002 try std.testing.expectEqual(@as(u64, 0), lifetime.live_allocations);
2003 try std.testing.expectEqual(@as(u128, 0), lifetime.live_bytes);
2004 try std.testing.expectEqual(
2005 @as(u128, 40),
2006 lifetime.high_water_live_bytes,
2007 );
2008 }
2009
2010 test "causal operation ledger validates contiguous multi-event groups" {
2011 var ledger = OperationLedger{};
2012 defer ledger.deinit(std.testing.allocator);
2013 try ledger.record(std.testing.allocator, 2, 1);
2014 try ledger.record(std.testing.allocator, 2, 1);
2015 try ledger.record(std.testing.allocator, 1, 0);
2016 try ledger.validate();
2017 try std.testing.expectEqual(@as(u64, 2), ledger.unique);
2018 try std.testing.expectError(
2019 error.NonContiguousCausalOperationGroup,
2020 ledger.record(std.testing.allocator, 2, 1),
2021 );
2022 }
2023
2024 test "causal operation ledger rejects a parent conflict within one group" {
2025 var ledger = OperationLedger{};
2026 defer ledger.deinit(std.testing.allocator);
2027 try ledger.record(std.testing.allocator, 2, 1);
2028 try std.testing.expectError(
2029 error.CausalOperationParentConflict,
2030 ledger.record(std.testing.allocator, 2, 0),
2031 );
2032 }