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 }