lib/memtrace/src/stack/report.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const pretty_json = @import("pretty").json;
3 const sys = @import("sys");
4 const memtrace = @import("../root.zig");
5 const analyze_mod = @import("analyze.zig");
6 const capture_mod = @import("capture.zig");
7 const identity_mod = @import("identity.zig");
8 const symbolize_mod = @import("symbolize.zig");
9
10 const event_mod = memtrace.event;
11
12 pub const Format = enum {
13 text,
14 jsonl,
15 };
16
17 pub const Selection = analyze_mod.Selection;
18
19 pub const Options = struct {
20 top: usize = std.math.maxInt(usize),
21 frame_limit: usize = capture_mod.max_frames_limit,
22 format: Format = .text,
23 binary_path: ?[]const u8 = null,
24 selection: analyze_mod.Selection = .allocations,
25 layer: event_mod.LayerFilter = .backing,
26 window: analyze_mod.Window = .{},
27 };
28
29 const SourceKey = struct {
30 kind: event_mod.Kind,
31 succeeded: bool,
32 layer: event_mod.Layer,
33 producer: @import("alloc_observe").Producer,
34 site: u64,
35 caller: u64,
36 };
37
38 const Source = struct {
39 key: SourceKey,
40 counters: analyze_mod.Counters,
41 unique_stacks: u32,
42 };
43
44 const Display = struct {
45 source_groups: usize,
46 displayed_sources: usize,
47 operation_stacks: usize,
48 displayed_stacks: usize,
49 };
50
51 pub fn writeFromPath(
52 allocator: std.mem.Allocator,
53 events_path: []const u8,
54 writer: *std.Io.Writer,
55 options: Options,
56 ) !void {
57 if (options.top == 0 or
58 options.frame_limit == 0 or
59 options.frame_limit > capture_mod.max_frames_limit)
60 {
61 return error.InvalidStackReportLimit;
62 }
63 try options.window.validate();
64 var analyzer = analyze_mod.Analyzer.init(allocator, options.window);
65 defer analyzer.deinit();
66 try ingestPath(&analyzer, events_path);
67 try analyzer.validate();
68
69 var inferred_binary: ?[]u8 = null;
70 defer if (inferred_binary) |path| allocator.free(path);
71 const binary_path = options.binary_path orelse inferred: {
72 inferred_binary = try identity_mod.artifactPathAlloc(
73 allocator,
74 events_path,
75 );
76 break :inferred inferred_binary.?;
77 };
78 const actual_digest = identity_mod.fileDigest(
79 allocator,
80 binary_path,
81 ) catch |err| switch (err) {
82 error.FileNotFound => return error.MissingExecutableArtifact,
83 else => return err,
84 };
85 const expected_digest = analyzer.executable_digest.?;
86 if (!std.mem.eql(u8, &actual_digest, &expected_digest)) {
87 return error.ExecutableIdentityMismatch;
88 }
89
90 var summaries = try analyzer.collect(options.selection, options.layer);
91 defer summaries.deinit(allocator);
92 const totals = analyzer.totals(options.selection, options.layer);
93 const stack_limit = @min(options.top, summaries.items.len);
94 var sources = try collectSources(allocator, summaries.items);
95 defer sources.deinit(allocator);
96 const source_limit = @min(options.top, sources.items.len);
97 const display = Display{
98 .source_groups = sources.items.len,
99 .displayed_sources = source_limit,
100 .operation_stacks = summaries.items.len,
101 .displayed_stacks = stack_limit,
102 };
103 var addresses = try collectAddresses(
104 allocator,
105 summaries.items[0..stack_limit],
106 sources.items[0..source_limit],
107 options.frame_limit,
108 );
109 defer addresses.deinit(allocator);
110 var symbols = if (addresses.items.len == 0)
111 null
112 else
113 try symbolize_mod.resolveAlloc(
114 allocator,
115 binary_path,
116 addresses.items,
117 );
118 defer if (symbols) |*resolved| resolved.deinit(allocator);
119 switch (options.format) {
120 .text => try writeText(
121 writer,
122 &analyzer,
123 totals,
124 options.selection,
125 options.layer,
126 options.window,
127 display,
128 sources.items[0..source_limit],
129 summaries.items[0..stack_limit],
130 symbols,
131 options.frame_limit,
132 expected_digest,
133 ),
134 .jsonl => try writeJsonl(
135 writer,
136 &analyzer,
137 totals,
138 options.selection,
139 options.layer,
140 options.window,
141 display,
142 sources.items[0..source_limit],
143 summaries.items[0..stack_limit],
144 symbols,
145 options.frame_limit,
146 expected_digest,
147 ),
148 }
149 }
150
151 fn ingestPath(analyzer: *analyze_mod.Analyzer, path: []const u8) !void {
152 var file = try sys.fs.cwd().openFile(sys.fs.debugIo(), path, .{});
153 defer file.close(sys.fs.debugIo());
154 var buffer: [64 * 1024]u8 = undefined;
155 var reader = file.reader(sys.fs.debugIo(), &buffer);
156 while (true) {
157 const line = reader.interface.takeDelimiter('\n') catch |err| switch (err) {
158 error.ReadFailed => return reader.err.?,
159 else => return err,
160 };
161 const actual = line orelse break;
162 try analyzer.ingestJsonLine(actual);
163 }
164 }
165
166 fn collectAddresses(
167 allocator: std.mem.Allocator,
168 summaries: []const analyze_mod.Summary,
169 sources: []const Source,
170 frame_limit: usize,
171 ) !std.ArrayListUnmanaged(u64) {
172 var seen = std.AutoHashMapUnmanaged(u64, void){};
173 defer seen.deinit(allocator);
174 var addresses = std.ArrayListUnmanaged(u64).empty;
175 errdefer addresses.deinit(allocator);
176 for (sources) |source| {
177 try appendAddress(allocator, &seen, &addresses, source.key.site);
178 if (source.key.caller != 0) {
179 try appendAddress(
180 allocator,
181 &seen,
182 &addresses,
183 source.key.caller,
184 );
185 }
186 }
187 for (summaries) |summary| {
188 const limit = @min(
189 frame_limit,
190 summary.definition.call_addresses.len,
191 );
192 for (summary.definition.call_addresses[0..limit]) |address| {
193 try appendAddress(allocator, &seen, &addresses, address);
194 }
195 }
196 std.mem.sort(u64, addresses.items, {}, lessThan);
197 return addresses;
198 }
199
200 fn appendAddress(
201 allocator: std.mem.Allocator,
202 seen: *std.AutoHashMapUnmanaged(u64, void),
203 addresses: *std.ArrayListUnmanaged(u64),
204 address: u64,
205 ) !void {
206 const entry = try seen.getOrPut(allocator, address);
207 if (entry.found_existing) return;
208 try addresses.append(allocator, address);
209 }
210
211 fn collectSources(
212 allocator: std.mem.Allocator,
213 summaries: []const analyze_mod.Summary,
214 ) !std.ArrayListUnmanaged(Source) {
215 var counts = std.AutoHashMapUnmanaged(SourceKey, Source){};
216 defer counts.deinit(allocator);
217 for (summaries) |summary| {
218 const key = SourceKey{
219 .kind = summary.key.kind,
220 .succeeded = summary.key.succeeded,
221 .layer = summary.key.layer,
222 .producer = summary.key.producer,
223 .site = summary.definition.call_addresses[0],
224 .caller = callerAddress(summary.definition),
225 };
226 const entry = try counts.getOrPut(allocator, key);
227 if (!entry.found_existing) {
228 entry.value_ptr.* = .{
229 .key = key,
230 .counters = .{},
231 .unique_stacks = 0,
232 };
233 }
234 entry.value_ptr.counters.calls = try std.math.add(
235 u64,
236 entry.value_ptr.counters.calls,
237 summary.counters.calls,
238 );
239 entry.value_ptr.counters.requested_bytes = try std.math.add(
240 u128,
241 entry.value_ptr.counters.requested_bytes,
242 summary.counters.requested_bytes,
243 );
244 entry.value_ptr.unique_stacks = try std.math.add(
245 u32,
246 entry.value_ptr.unique_stacks,
247 1,
248 );
249 }
250 var sources = std.ArrayListUnmanaged(Source).empty;
251 errdefer sources.deinit(allocator);
252 try sources.ensureTotalCapacity(allocator, counts.count());
253 var values = counts.valueIterator();
254 while (values.next()) |source| sources.appendAssumeCapacity(source.*);
255 std.mem.sort(Source, sources.items, {}, sourceGreaterThan);
256 return sources;
257 }
258
259 fn callerAddress(definition: analyze_mod.Definition) u64 {
260 const site = definition.call_addresses[0];
261 for (definition.call_addresses[1..], 1..) |address, index| {
262 if (address != site) continue;
263 const caller_index = index + 1;
264 if (caller_index < definition.call_addresses.len) {
265 return definition.call_addresses[caller_index];
266 }
267 return 0;
268 }
269 return 0;
270 }
271
272 fn sourceGreaterThan(_: void, left: Source, right: Source) bool {
273 if (left.counters.calls != right.counters.calls) {
274 return left.counters.calls > right.counters.calls;
275 }
276 if (left.counters.requested_bytes != right.counters.requested_bytes) {
277 return left.counters.requested_bytes > right.counters.requested_bytes;
278 }
279 if (left.key.kind != right.key.kind) {
280 return @backingInt(left.key.kind) < @backingInt(right.key.kind);
281 }
282 if (left.key.layer != right.key.layer) {
283 return @backingInt(left.key.layer) < @backingInt(right.key.layer);
284 }
285 if (left.key.producer != right.key.producer) {
286 return @backingInt(left.key.producer) < @backingInt(right.key.producer);
287 }
288 if (left.key.succeeded != right.key.succeeded) return left.key.succeeded;
289 if (left.key.site != right.key.site) return left.key.site < right.key.site;
290 return left.key.caller < right.key.caller;
291 }
292
293 fn selectionTag(selection: analyze_mod.Selection) []const u8 {
294 return switch (selection) {
295 .allocations => "allocations",
296 .all => "all",
297 };
298 }
299
300 fn outcomeTag(succeeded: bool) []const u8 {
301 return if (succeeded) "success" else "failure";
302 }
303
304 fn lessThan(_: void, left: u64, right: u64) bool {
305 return left < right;
306 }
307
308 fn writeText(
309 writer: *std.Io.Writer,
310 analyzer: *const analyze_mod.Analyzer,
311 totals: analyze_mod.Totals,
312 selection: analyze_mod.Selection,
313 layer: event_mod.LayerFilter,
314 window: analyze_mod.Window,
315 display: Display,
316 sources: []const Source,
317 summaries: []const analyze_mod.Summary,
318 maybe_symbols: ?symbolize_mod.Symbols,
319 frame_limit: usize,
320 digest: identity_mod.Digest,
321 ) !void {
322 const digest_hex = std.fmt.bytesToHex(digest, .lower);
323 const coverage = analyzer.coverage.?;
324 try writer.print(
325 "memory_operations status={s} universe={s} selection={s} layer={s} " ++
326 "calls={d} successful={d} failed={d} requested_bytes={d} " ++
327 "source_groups={d} displayed_sources={d} operation_stacks={d} " ++
328 "displayed_stacks={d} binary_sha256={s}",
329 .{
330 coverage.statusTag(),
331 coverage.universe.tag(),
332 selectionTag(selection),
333 layer.tag(),
334 totals.calls,
335 totals.successful,
336 totals.failed,
337 totals.requested_bytes,
338 display.source_groups,
339 display.displayed_sources,
340 display.operation_stacks,
341 display.displayed_stacks,
342 digest_hex,
343 },
344 );
345 try writeTextWindow(writer, window, "memory_operation");
346 try writer.writeByte('\n');
347 try writer.print(
348 "coverage child_allocator_fast_paths={s} sys_memory_operations={s} " ++
349 "direct_os_memory_operations={s} unowned_allocator_producers={s} " ++
350 "foreign_allocations={s} " ++
351 "observer_control={s} observer_control_operations={d} " ++
352 "zero_length_operations={s} predispatch_failures={s}\n",
353 .{
354 coverage.child_allocator_fast_paths.tag(),
355 coverage.sys_memory_operations.tag(),
356 coverage.direct_os_memory_operations.tag(),
357 coverage.unowned_allocator_producers.tag(),
358 coverage.foreign_allocations.tag(),
359 coverage.observer_control.tag(),
360 coverage.observer_control_operations,
361 coverage.zero_length_operations.tag(),
362 coverage.predispatch_failures.tag(),
363 },
364 );
365 for (sources) |source| {
366 try writer.print(
367 "source layer={s} producer={s} operation={s} outcome={s} calls={d} " ++
368 "requested_bytes={d} unique_stacks={d}",
369 .{
370 source.key.layer.tag(),
371 @tagName(source.key.producer),
372 source.key.kind.tag(),
373 outcomeTag(source.key.succeeded),
374 source.counters.calls,
375 source.counters.requested_bytes,
376 source.unique_stacks,
377 },
378 );
379 try writeTextAddress(
380 writer,
381 " site",
382 source.key.site,
383 maybe_symbols,
384 );
385 if (source.key.caller != 0) {
386 try writeTextAddress(
387 writer,
388 " caller",
389 source.key.caller,
390 maybe_symbols,
391 );
392 } else {
393 try writer.writeAll(" caller=unavailable");
394 }
395 try writer.writeByte('\n');
396 }
397 for (summaries) |summary| {
398 const displayed = @min(
399 frame_limit,
400 summary.definition.call_addresses.len,
401 );
402 try writer.print(
403 "stack id={d} layer={s} producer={s} operation={s} outcome={s} calls={d} " ++
404 "requested_bytes={d} captured_frames={d} displayed_frames={d}\n",
405 .{
406 summary.key.stack_id,
407 summary.key.layer.tag(),
408 @tagName(summary.key.producer),
409 summary.key.kind.tag(),
410 outcomeTag(summary.key.succeeded),
411 summary.counters.calls,
412 summary.counters.requested_bytes,
413 summary.definition.call_addresses.len,
414 displayed,
415 },
416 );
417 for (
418 summary.definition.call_addresses[0..displayed],
419 0..,
420 ) |address, frame_index| {
421 const resolved = if (maybe_symbols) |symbols|
422 symbols.find(address)
423 else
424 &.{};
425 try writer.print(
426 " frame={d} kind={s} call_address=0x{x}",
427 .{
428 frame_index,
429 if (frame_index == 0) "operation_site" else "physical",
430 address,
431 },
432 );
433 if (resolved.len != 0) {
434 try writer.writeAll(" function=");
435 try pretty_json.writeString(writer, resolved[0].function);
436 try writer.writeAll(" location=");
437 try pretty_json.writeString(writer, resolved[0].location);
438 }
439 try writer.writeByte('\n');
440 for (resolved[1..], 1..) |inline_frame, inline_index| {
441 try writer.print(" inline={d} function=", .{inline_index});
442 try pretty_json.writeString(writer, inline_frame.function);
443 try writer.writeAll(" location=");
444 try pretty_json.writeString(writer, inline_frame.location);
445 try writer.writeByte('\n');
446 }
447 }
448 }
449 }
450
451 fn writeTextAddress(
452 writer: *std.Io.Writer,
453 prefix: []const u8,
454 address: u64,
455 maybe_symbols: ?symbolize_mod.Symbols,
456 ) !void {
457 try writer.print("{s}_address=0x{x}", .{ prefix, address });
458 const resolved = if (maybe_symbols) |symbols|
459 symbols.find(address)
460 else
461 &.{};
462 if (resolved.len == 0) return;
463 try writer.print("{s}_function=", .{prefix});
464 try pretty_json.writeString(writer, resolved[0].function);
465 try writer.print("{s}_location=", .{prefix});
466 try pretty_json.writeString(writer, resolved[0].location);
467 if (resolved.len == 1) return;
468 const owner = resolved[resolved.len - 1];
469 try writer.print("{s}_owner_function=", .{prefix});
470 try pretty_json.writeString(writer, owner.function);
471 try writer.print("{s}_owner_location=", .{prefix});
472 try pretty_json.writeString(writer, owner.location);
473 }
474
475 fn writeJsonl(
476 writer: *std.Io.Writer,
477 analyzer: *const analyze_mod.Analyzer,
478 totals: analyze_mod.Totals,
479 selection: analyze_mod.Selection,
480 layer: event_mod.LayerFilter,
481 window: analyze_mod.Window,
482 display: Display,
483 sources: []const Source,
484 summaries: []const analyze_mod.Summary,
485 maybe_symbols: ?symbolize_mod.Symbols,
486 frame_limit: usize,
487 digest: identity_mod.Digest,
488 ) !void {
489 const coverage = analyzer.coverage.?;
490 var summary_stream = pretty_json.Writer.init(writer, .minified);
491 const header = try summary_stream.object();
492 try header.field("kind", "memory_operation_summary");
493 try header.field("status", coverage.statusTag());
494 try header.field("universe", coverage.universe.tag());
495 try header.field("selection", selectionTag(selection));
496 try header.field("layer", layer.tag());
497 try header.field("calls", totals.calls);
498 try header.field("successful", totals.successful);
499 try header.field("failed", totals.failed);
500 try header.field("requested_bytes", totals.requested_bytes);
501 try header.field("source_groups", display.source_groups);
502 try header.field("displayed_sources", display.displayed_sources);
503 try header.field("operation_stacks", display.operation_stacks);
504 try header.field("displayed_stacks", display.displayed_stacks);
505 try header.hexString("binary_sha256", &digest);
506 try writeJsonWindow(header, window, "memory_operation");
507 try header.field("child_allocator_fast_paths", coverage.child_allocator_fast_paths.tag());
508 try header.field("sys_memory_operations", coverage.sys_memory_operations.tag());
509 try header.field("direct_os_memory_operations", coverage.direct_os_memory_operations.tag());
510 try header.field("unowned_allocator_producers", coverage.unowned_allocator_producers.tag());
511 try header.field("foreign_allocations", coverage.foreign_allocations.tag());
512 try header.field("observer_control", coverage.observer_control.tag());
513 try header.field("observer_control_operations", coverage.observer_control_operations);
514 try header.field("zero_length_operations", coverage.zero_length_operations.tag());
515 try header.field("predispatch_failures", coverage.predispatch_failures.tag());
516 try header.endLine();
517 for (sources) |source| {
518 var stream = pretty_json.Writer.init(writer, .minified);
519 const object = try stream.object();
520 try object.field("kind", "memory_operation_source");
521 try object.field("layer", source.key.layer.tag());
522 try object.field("producer", @tagName(source.key.producer));
523 try object.field("operation", source.key.kind.tag());
524 try object.field("succeeded", source.key.succeeded);
525 try object.field("calls", source.counters.calls);
526 try object.field("requested_bytes", source.counters.requested_bytes);
527 try object.field("unique_stacks", source.unique_stacks);
528 try object.field("site_address", source.key.site);
529 const caller_address: ?u64 = if (source.key.caller == 0) null else source.key.caller;
530 try object.field("caller_address", caller_address);
531 try writeJsonSymbol(
532 object,
533 "site",
534 source.key.site,
535 maybe_symbols,
536 );
537 if (source.key.caller != 0) {
538 try writeJsonSymbol(
539 object,
540 "caller",
541 source.key.caller,
542 maybe_symbols,
543 );
544 }
545 try object.endLine();
546 }
547 for (summaries) |summary| {
548 const displayed = @min(
549 frame_limit,
550 summary.definition.call_addresses.len,
551 );
552 var stream = pretty_json.Writer.init(writer, .minified);
553 const object = try stream.object();
554 try object.field("kind", "memory_operation_stack");
555 try object.field("stack_id", summary.key.stack_id);
556 try object.field("layer", summary.key.layer.tag());
557 try object.field("producer", @tagName(summary.key.producer));
558 try object.field("operation", summary.key.kind.tag());
559 try object.field("succeeded", summary.key.succeeded);
560 try object.field("calls", summary.counters.calls);
561 try object.field("requested_bytes", summary.counters.requested_bytes);
562 try object.field("captured_frames", summary.definition.call_addresses.len);
563 try object.field("displayed_frames", displayed);
564 try object.endLine();
565 for (
566 summary.definition.call_addresses[0..displayed],
567 0..,
568 ) |address, frame_index| {
569 const resolved = if (maybe_symbols) |symbols|
570 symbols.find(address)
571 else
572 &.{};
573 if (resolved.len == 0) {
574 try writeFrameJsonl(
575 writer,
576 summary.key.stack_id,
577 frame_index,
578 address,
579 0,
580 "",
581 "",
582 );
583 continue;
584 }
585 for (resolved, 0..) |inline_frame, inline_index| {
586 try writeFrameJsonl(
587 writer,
588 summary.key.stack_id,
589 frame_index,
590 address,
591 inline_index,
592 inline_frame.function,
593 inline_frame.location,
594 );
595 }
596 }
597 }
598 }
599
600 fn writeTextWindow(
601 writer: *std.Io.Writer,
602 window: analyze_mod.Window,
603 anchor: []const u8,
604 ) !void {
605 try writer.writeAll(" window_scope=");
606 if (window.scope) |scope| {
607 try pretty_json.writeString(writer, scope);
608 } else {
609 try writer.writeAll("all");
610 }
611 try writer.writeAll(" window_scope_match=subtree window_first_sequence=");
612 try writeOptionalSequence(writer, window.first_sequence);
613 try writer.writeAll(" window_last_sequence=");
614 try writeOptionalSequence(writer, window.last_sequence);
615 try writer.print(" window_sequence_bounds=inclusive window_anchor={s}", .{
616 anchor,
617 });
618 }
619
620 fn writeOptionalSequence(writer: *std.Io.Writer, sequence: ?u64) !void {
621 if (sequence) |value| {
622 try writer.print("{d}", .{value});
623 } else {
624 try writer.writeAll("all");
625 }
626 }
627
628 fn writeJsonWindow(
629 object: pretty_json.Object,
630 window: analyze_mod.Window,
631 anchor: []const u8,
632 ) !void {
633 try object.field("window_scope", window.scope);
634 try object.field("window_scope_match", "subtree");
635 try object.field("window_first_sequence", window.first_sequence);
636 try object.field("window_last_sequence", window.last_sequence);
637 try object.field("window_sequence_bounds", "inclusive");
638 try object.field("window_anchor", anchor);
639 }
640
641 fn writeJsonSymbol(
642 object: pretty_json.Object,
643 prefix: []const u8,
644 address: u64,
645 maybe_symbols: ?symbolize_mod.Symbols,
646 ) !void {
647 const resolved = if (maybe_symbols) |symbols|
648 symbols.find(address)
649 else
650 &.{};
651 if (resolved.len == 0) return;
652 try object.fieldParts(&.{ prefix, "_function" }, resolved[0].function);
653 try object.fieldParts(&.{ prefix, "_location" }, resolved[0].location);
654 if (resolved.len == 1) return;
655 const owner = resolved[resolved.len - 1];
656 try object.fieldParts(&.{ prefix, "_owner_function" }, owner.function);
657 try object.fieldParts(&.{ prefix, "_owner_location" }, owner.location);
658 }
659
660 fn writeFrameJsonl(
661 writer: *std.Io.Writer,
662 stack_id: u32,
663 frame_index: usize,
664 address: u64,
665 inline_index: usize,
666 function: []const u8,
667 location: []const u8,
668 ) !void {
669 var stream = pretty_json.Writer.init(writer, .minified);
670 const object = try stream.object();
671 try object.field("kind", "memory_operation_stack_frame");
672 try object.field("stack_id", stack_id);
673 try object.field("frame", frame_index);
674 try object.field("frame_kind", if (frame_index == 0) "operation_site" else "physical");
675 try object.field("call_address", address);
676 try object.field("inline", inline_index);
677 try object.field("function", function);
678 try object.field("location", location);
679 try object.endLine();
680 }