tiny.profiling.report.memory
Defined in report.
API (17)
Actions
Public operations.
Types and contracts
Public types and contracts.
AccountingOptionsReportScopeSourceTotalsTraceTraceFaultTraceRankingTraceRankingsTraceSummaryWorkload
Source
Source: src/profiling/report/memory.zig
zig
const std = @import("std");const memtrace = @import("memtrace");const pretty = @import("pretty");const pretty_usage = @import("pretty_usage");const sys = @import("sys");const analyze = @import("../root.zig").analyze;const memory = @import("../root.zig").memory;const record = @import("../root.zig").record;const reducer = @import("../root.zig").reduction;const pretty_json = pretty.json;const fixture_trace_version = std.fmt.comptimePrint( "{d}", .{memtrace.event.format_version},);pub const Options = struct { input: []const u8, json: bool = false, top: usize = 10, min_bytes: usize = 0,};pub const Report = struct { run: analyze.Run, workloads: []const Workload, totals: Totals,};pub const Workload = struct { name: []const u8, package: []const u8, step: []const u8, status: []const u8, max_rss_kib: ?i64, max_rss_bytes: ?u64, trace_path: ?[]const u8, trace: ?Trace, trace_fault: ?TraceFault, memory_metrics: []const memory.Metric, accounting: Accounting,};pub const TraceFault = struct { path: []const u8, line: u64, reason: []const u8,};pub const Totals = struct { workloads: usize = 0, rss_workloads: usize = 0, traced_workloads: usize = 0, complete_traces: usize = 0, partial_traces: usize = 0, invalid_traces: usize = 0, rss_bytes_sum: u64 = 0, attributed_retained_bytes_sum: u64 = 0, unclassified_bytes_sum: u64 = 0,};pub const Accounting = struct { state: []const u8, rss_bytes: ?u64, attributed_retained_bytes: ?u64, unclassified_bytes: ?u64, coverage_percent: ?f64,};pub const Trace = struct { path: []const u8, integrity: memtrace.CaptureIntegrity, summary: TraceSummary, scopes: []const Scope, rankings: TraceRankings, snapshot: ?memtrace.analysis.Snapshot = null,};pub const TraceRankings = struct { retained: TraceRanking, traffic: TraceRanking, lifetime: TraceRanking, pub fn get( self: TraceRankings, sort: memtrace.analysis.Sort, ) TraceRanking { return switch (sort) { .retained => self.retained, .traffic => self.traffic, .lifetime => self.lifetime, }; }};pub const TraceRanking = struct { owners: []const Scope, sources: []const Source,};pub const TraceSummary = memtrace.analysis.Summary;pub const Scope = memtrace.analysis.Scope;pub const Source = memtrace.analysis.Source;pub fn run(allocator: std.mem.Allocator, options: Options) !u8 { const report = try load(allocator, options); if (options.json) { var buffer: [8192]u8 = undefined; var file_writer = sys.stdio.stdout().writer(std.Options.debug_io, &buffer); defer file_writer.interface.flush() catch {}; try writeJson(allocator, &file_writer.interface, report, options); } else { try writeText(allocator, pretty_usage.Terminal.stdout(allocator, .{}), report, options); } return 0;}pub fn load(allocator: std.mem.Allocator, options: Options) !Report { const run_value = try analyze.loadRun(allocator, options.input); var rows: std.ArrayList(Workload) = .empty; var totals = Totals{ .workloads = run_value.workloads.len }; for (run_value.workloads) |workload| { const artifacts = workload.analysisArtifacts(); const allocations_path = if (artifacts) |actual| actual.allocations else null; const loaded = try loadTrace(allocator, allocations_path, options); const trace: ?Trace = switch (loaded) { .trace => |actual| actual, .none, .fault => null, }; const fault: ?TraceFault = switch (loaded) { .fault => |actual| actual, .none, .trace => null, }; const accounting = if (fault == null) account(workload, trace) else Accounting{ .state = "trace_invalid", .rss_bytes = rssBytes(workload.max_rss_kib), .attributed_retained_bytes = null, .unclassified_bytes = null, .coverage_percent = null, }; if (accounting.rss_bytes) |rss| { totals.rss_workloads += 1; totals.rss_bytes_sum +|= rss; } if (trace) |actual| { totals.traced_workloads += 1; if (std.mem.eql(u8, actual.integrity.status, "complete")) { totals.complete_traces += 1; totals.attributed_retained_bytes_sum +|= actual.summary.retained_bytes; } else { totals.partial_traces += 1; } } if (fault != null) totals.invalid_traces += 1; if (accounting.unclassified_bytes) |bytes| totals.unclassified_bytes_sum +|= bytes; try rows.append(allocator, .{ .name = workload.name, .package = workload.package, .step = workload.step, .status = workload.status, .max_rss_kib = workload.max_rss_kib, .max_rss_bytes = rssBytes(workload.max_rss_kib), .trace_path = allocations_path, .trace = trace, .trace_fault = fault, .memory_metrics = workload.memory_metrics, .accounting = accounting, }); } return .{ .run = run_value, .workloads = try rows.toOwnedSlice(allocator), .totals = totals, };}const LoadedTrace = union(enum) { none, trace: Trace, fault: TraceFault,};fn availableSymbolBinary(path: []const u8) !?[]const u8 { const file = (if (std.fs.path.isAbsolute(path)) sys.fs.openAbsoluteFile(path, .{}) else sys.fs.cwd().openFile(sys.fs.debugIo(), path, .{})) catch |err| switch (err) { error.FileNotFound => return null, else => |actual| return actual, }; defer sys.fs.closeHandle(file); return path;}fn loadTrace(allocator: std.mem.Allocator, path: ?[]const u8, options: Options) !LoadedTrace { const actual_path = path orelse return .none; var file = sys.fs.cwd().openFile(sys.fs.debugIo(), actual_path, .{}) catch |err| switch (err) { error.FileNotFound => return .none, else => |actual| return actual, }; defer file.close(sys.fs.debugIo()); var analyzer = memtrace.Analyzer.init(allocator); defer analyzer.deinit(); if (try ingestTrace(&analyzer, file, actual_path)) |fault| return .{ .fault = fault }; const executable_path = try memtrace.stack.identity.artifactPathAlloc( allocator, actual_path, ); defer allocator.free(executable_path); const symbol_binary = try availableSymbolBinary(executable_path); const path_copy = try allocator.dupe(u8, actual_path); errdefer allocator.free(path_copy); var snapshot = try analyzer.snapshot(allocator, .{ .top = options.top, .min_bytes = options.min_bytes, .site_symbol_binary = symbol_binary, }); errdefer snapshot.deinit(); const retained = snapshot.rankings.retained; const traffic = snapshot.rankings.traffic; const lifetime = snapshot.rankings.lifetime; return .{ .trace = .{ .path = path_copy, .integrity = snapshot.integrity, .summary = snapshot.summary, .scopes = retained.scopes, .rankings = .{ .retained = .{ .owners = retained.scopes, .sources = retained.sources, }, .traffic = .{ .owners = traffic.scopes, .sources = traffic.sources, }, .lifetime = .{ .owners = lifetime.scopes, .sources = lifetime.sources, }, }, .snapshot = snapshot, }, };}fn ingestTrace(analyzer: *memtrace.Analyzer, file: std.Io.File, path: []const u8) !?TraceFault { var buffer: [64 * 1024]u8 = undefined; var reader = file.reader(sys.fs.debugIo(), &buffer); var line_number: u64 = 0; while (true) { const line = reader.interface.takeDelimiter('\n') catch |err| switch (err) { error.ReadFailed => return reader.err.?, else => return err, }; const actual = line orelse break; line_number += 1; analyzer.ingestJsonLine(actual) catch |err| switch (err) { error.OutOfMemory => return error.OutOfMemory, else => return .{ .path = path, .line = line_number, .reason = @errorName(err), }, }; } return null;}fn account(workload: analyze.Workload, trace: ?Trace) Accounting { const rss = rssBytes(workload.max_rss_kib); const actual_trace = trace orelse return .{ .state = "missing_trace", .rss_bytes = rss, .attributed_retained_bytes = null, .unclassified_bytes = null, .coverage_percent = null, }; const attributed = actual_trace.summary.retained_bytes; if (!std.mem.eql(u8, actual_trace.integrity.status, "complete")) return .{ .state = "trace_partial", .rss_bytes = rss, .attributed_retained_bytes = attributed, .unclassified_bytes = null, .coverage_percent = null, }; const rss_value = rss orelse return .{ .state = "missing_rss", .rss_bytes = null, .attributed_retained_bytes = attributed, .unclassified_bytes = null, .coverage_percent = null, }; if (rss_value == 0) return .{ .state = "missing_rss", .rss_bytes = rss_value, .attributed_retained_bytes = attributed, .unclassified_bytes = null, .coverage_percent = null, }; if (attributed > rss_value) return .{ .state = "over_attributed", .rss_bytes = rss_value, .attributed_retained_bytes = attributed, .unclassified_bytes = 0, .coverage_percent = (@as(f64, @floatFromInt(attributed)) / @as(f64, @floatFromInt(rss_value))) * 100, }; return .{ .state = "measured", .rss_bytes = rss_value, .attributed_retained_bytes = attributed, .unclassified_bytes = rss_value - attributed, .coverage_percent = (@as(f64, @floatFromInt(attributed)) / @as(f64, @floatFromInt(rss_value))) * 100, };}fn rssBytes(max_rss_kib: ?i64) ?u64 { const kib = max_rss_kib orelse return null; if (kib < 0) return null; return @as(u64, @intCast(kib)) * 1024;}pub fn writeText(allocator: std.mem.Allocator, terminal: pretty_usage.Terminal, report: Report, options: Options) !void { try terminal.writeTextFmt("memory report: {s}\n", .{report.run.ref.run_id}); try terminal.writeTextFmt("artifacts: {s}\n", .{report.run.ref.root}); try terminal.writeTextFmt( "workloads: {d}, rss {d}, traced {d} (complete {d}, partial {d}), " ++ "invalid traces {d}, memory metrics {d}\n", .{ report.totals.workloads, report.totals.rss_workloads, report.totals.traced_workloads, report.totals.complete_traces, report.totals.partial_traces, report.totals.invalid_traces, report.run.memoryMetricCount(), }, ); try terminal.writeTextFmt( "accountability: rss_bytes_sum {d}, attributed_retained_bytes_sum {d}, unclassified_bytes_sum {d}\n", .{ report.totals.rss_bytes_sum, report.totals.attributed_retained_bytes_sum, report.totals.unclassified_bytes_sum }, ); try writeAllocationRankingsText( allocator, terminal, report.workloads, options.top, ); const ranked = try rankedWorkloads(allocator, report.workloads); defer allocator.free(ranked); const count = @min(options.top, ranked.len); for (ranked[0..count], 0..) |workload_index, rank| { const workload = report.workloads[workload_index]; try terminal.writeTextFmt(" {d}. {s}: {s}", .{ rank + 1, workload.name, workload.accounting.state }); if (workload.max_rss_bytes) |rss| try terminal.writeTextFmt(", rss {d} bytes", .{rss}); if (workload.trace) |trace| { try terminal.writeTextFmt( ", integrity {s}, retained {d} bytes, live {d} bytes, allocated {d} bytes", .{ trace.integrity.status, trace.summary.retained_bytes, trace.summary.live_bytes, trace.summary.allocated_bytes, }, ); } if (workload.accounting.unclassified_bytes) |bytes| try terminal.writeTextFmt(", unclassified {d} bytes", .{bytes}); if (workload.accounting.coverage_percent) |coverage| try terminal.writeTextFmt(", coverage {d:.2}%", .{coverage}); if (workload.trace_fault) |fault| { try terminal.writeTextFmt(", trace invalid: {s} at {s}:{d}", .{ fault.reason, fault.path, fault.line }); } try terminal.writeText("\n"); try writeWorkloadDetailsText(allocator, terminal, workload, options.top); }}fn writeAllocationRankingsText( allocator: std.mem.Allocator, terminal: pretty_usage.Terminal, workloads: []const Workload, top: usize,) !void { inline for (std.meta.tags(memtrace.analysis.Sort)) |sort| { const ranked = try rankedTraceWorkloads(allocator, workloads, sort); defer allocator.free(ranked); if (ranked.len != 0) { try terminal.writeTextFmt( "allocation {s} ranking:\n", .{sort.name()}, ); const count = @min(top, ranked.len); for (ranked[0..count], 0..) |workload_index, rank| { const workload = workloads[workload_index]; const summary = workload.trace.?.summary; try terminal.writeTextFmt( " {d}. {s}: allocated {d} bytes in {d} allocations, retained {d} bytes, lifetime byte-events total {d}, mean {d}, max {d}; duration events total {d}, mean {d}, max {d}\n", .{ rank + 1, workload.name, summary.allocated_bytes, summary.allocations, summary.retained_bytes, summary.lifetime_total_byte_events, summary.lifetime_mean_byte_events, summary.lifetime_max_byte_events, summary.lifetime_total_events, summary.lifetime_mean_events, summary.lifetime_max_events, }, ); } } }}fn writeWorkloadDetailsText(allocator: std.mem.Allocator, terminal: pretty_usage.Terminal, workload: Workload, top: usize) !void { if (workload.trace) |trace| { if (trace.integrity.message) |message| { try terminal.writeTextFmt( " trace caveat {s}: {s}; action {s}\n", .{ trace.integrity.status, message, trace.integrity.action }, ); } inline for (std.meta.tags(memtrace.analysis.Sort)) |sort| { const ranking = trace.rankings.get(sort); const owner_count = @min(top, ranking.owners.len); if (owner_count != 0) { try terminal.writeTextFmt( " {s} owners:\n", .{sort.name()}, ); } for (ranking.owners[0..owner_count]) |owner| { try terminal.writeTextFmt( " {s}: allocated {d} bytes in {d} allocations, retained {d} bytes, lifetime byte-events total {d}, mean {d}, max {d}; duration events total {d}, mean {d}, max {d}\n", .{ owner.scope, owner.allocated_bytes, owner.allocations, owner.retained_bytes, owner.lifetime_total_byte_events, owner.lifetime_mean_byte_events, owner.lifetime_max_byte_events, owner.lifetime_total_events, owner.lifetime_mean_events, owner.lifetime_max_events, }, ); } const source_count = @min(top, ranking.sources.len); if (source_count != 0) { try terminal.writeTextFmt( " {s} sources:\n", .{sort.name()}, ); } for (ranking.sources[0..source_count]) |source| { try terminal.writeText(" "); if (source.function) |function| { try terminal.writeText(function); } else { try terminal.writeTextFmt( "return_address=0x{x}", .{source.return_address}, ); } if (source.location) |location| { if (location.len != 0) { try terminal.writeTextFmt(" at {s}", .{location}); } } try terminal.writeTextFmt( ": allocated {d} bytes in {d} allocations, retained {d} bytes, lifetime byte-events total {d}, mean {d}, max {d}; duration events total {d}, mean {d}, max {d}\n", .{ source.allocated_bytes, source.allocations, source.retained_bytes, source.lifetime_total_byte_events, source.lifetime_mean_byte_events, source.lifetime_max_byte_events, source.lifetime_total_events, source.lifetime_mean_events, source.lifetime_max_events, }, ); } } } const metric_count = @min(top, workload.memory_metrics.len); if (metric_count == 0) return; try terminal.writeText(" metrics:\n"); const ranked = try rankedMetrics(allocator, workload.memory_metrics); defer allocator.free(ranked); for (ranked[0..metric_count]) |metric_index| { const row = workload.memory_metrics[metric_index]; try terminal.writeTextFmt( " {s}: {d:.2} {s} ({s}, {d} sample(s))\n", .{ row.label, row.value, row.unit.name(), row.distribution.name(), row.sample_count, }, ); }}pub fn writeJson( allocator: std.mem.Allocator, writer: *std.Io.Writer, report: Report, options: Options,) !void { var out = pretty_json.Writer.init(writer, .minified); try out.beginObject(); try out.objectField("schema"); try out.write("tiny.profiling.memory-report/v1"); try out.objectField("run"); try out.beginObject(); try out.objectField("run_id"); try out.write(report.run.ref.run_id); try out.objectField("root"); try out.write(report.run.ref.root); try out.objectField("results"); try out.write(report.run.ref.results_path); try out.endObject(); try out.objectField("totals"); try writeTotalsJson(&out, report.totals); try out.objectField("allocation_rankings"); try writeAllocationRankingsJson( allocator, &out, report.workloads, options.top, ); try out.objectField("workloads"); try out.beginArray(); for (report.workloads) |workload| try writeWorkloadJson(&out, workload, options); try out.endArray(); try out.endObject(); try writer.writeByte('\n');}fn writeTotalsJson(out: *pretty_json.Writer, totals: Totals) !void { try out.beginObject(); try out.objectField("workloads"); try out.write(totals.workloads); try out.objectField("rss_workloads"); try out.write(totals.rss_workloads); try out.objectField("traced_workloads"); try out.write(totals.traced_workloads); try out.objectField("complete_traces"); try out.write(totals.complete_traces); try out.objectField("partial_traces"); try out.write(totals.partial_traces); try out.objectField("rss_bytes_sum"); try out.write(totals.rss_bytes_sum); try out.objectField("attributed_retained_bytes_sum"); try out.write(totals.attributed_retained_bytes_sum); try out.objectField("unclassified_bytes_sum"); try out.write(totals.unclassified_bytes_sum); try out.objectField("invalid_traces"); try out.write(totals.invalid_traces); try out.endObject();}fn writeAllocationRankingsJson( allocator: std.mem.Allocator, out: *pretty_json.Writer, workloads: []const Workload, top: usize,) !void { try out.beginObject(); inline for (std.meta.tags(memtrace.analysis.Sort)) |sort| { try out.objectField(sort.name()); try out.beginArray(); const ranked = try rankedTraceWorkloads(allocator, workloads, sort); defer allocator.free(ranked); const count = @min(top, ranked.len); for (ranked[0..count], 0..) |workload_index, rank| { const workload = workloads[workload_index]; const summary = workload.trace.?.summary; try out.beginObject(); try out.objectField("rank"); try out.write(rank + 1); try out.objectField("workload"); try out.write(workload.name); try out.objectField("package"); try out.write(workload.package); try out.objectField("allocated_bytes"); try out.write(summary.allocated_bytes); try out.objectField("allocations"); try out.write(summary.allocations); try out.objectField("retained_bytes"); try out.write(summary.retained_bytes); try out.objectField("high_water_retained_bytes"); try out.write(summary.high_water_retained_bytes); try out.objectField("lifetime_total_events"); try out.write(summary.lifetime_total_events); try out.objectField("lifetime_mean_events"); try out.write(summary.lifetime_mean_events); try out.objectField("lifetime_max_events"); try out.write(summary.lifetime_max_events); try out.objectField("lifetime_total_byte_events"); try out.write(summary.lifetime_total_byte_events); try out.objectField("lifetime_mean_byte_events"); try out.write(summary.lifetime_mean_byte_events); try out.objectField("lifetime_max_byte_events"); try out.write(summary.lifetime_max_byte_events); try out.endObject(); } try out.endArray(); } try out.endObject();}fn writeWorkloadJson(out: *pretty_json.Writer, workload: Workload, options: Options) !void { try out.beginObject(); try out.objectField("name"); try out.write(workload.name); try out.objectField("package"); try out.write(workload.package); try out.objectField("step"); try out.write(workload.step); try out.objectField("status"); try out.write(workload.status); try out.objectField("max_rss_kib"); try out.write(workload.max_rss_kib); try out.objectField("max_rss_bytes"); try out.write(workload.max_rss_bytes); try out.objectField("trace_path"); try out.write(workload.trace_path); try out.objectField("accounting"); try writeAccountingJson(out, workload.accounting); try out.objectField("trace"); if (workload.trace) |trace| { try writeTraceJson(out, trace, options.top); } else { try out.write(null); } try out.objectField("trace_fault"); if (workload.trace_fault) |fault| { try out.beginObject(); try out.objectField("path"); try out.write(fault.path); try out.objectField("line"); try out.write(fault.line); try out.objectField("reason"); try out.write(fault.reason); try out.endObject(); } else { try out.write(null); } try out.objectField("memory_metrics"); try writeMetricsJson(out, workload.memory_metrics, options.top); try out.endObject();}fn writeAccountingJson(out: *pretty_json.Writer, accounting: Accounting) !void { try out.beginObject(); try out.objectField("state"); try out.write(accounting.state); try out.objectField("rss_bytes"); try out.write(accounting.rss_bytes); try out.objectField("attributed_retained_bytes"); try out.write(accounting.attributed_retained_bytes); try out.objectField("unclassified_bytes"); try out.write(accounting.unclassified_bytes); try out.objectField("coverage_percent"); try out.write(accounting.coverage_percent); try out.endObject();}fn writeTraceJson(out: *pretty_json.Writer, trace: Trace, top: usize) !void { try out.beginObject(); try out.objectField("path"); try out.write(trace.path); try out.objectField("capture_integrity"); try writeTraceIntegrityJson(out, trace.integrity); try out.objectField("summary"); try writeTraceSummaryJson(out, trace.summary); try out.objectField("scopes"); try out.beginArray(); const count = @min(top, trace.scopes.len); for (trace.scopes[0..count]) |scope| try writeScopeJson(out, scope); try out.endArray(); try out.objectField("allocation_rankings"); try writeTraceRankingsJson(out, trace.rankings, top); try out.endObject();}fn writeTraceRankingsJson( out: *pretty_json.Writer, rankings: TraceRankings, top: usize,) !void { try out.beginObject(); inline for (std.meta.tags(memtrace.analysis.Sort)) |sort| { const ranking = rankings.get(sort); try out.objectField(sort.name()); try out.beginObject(); try out.objectField("owners"); try out.beginArray(); const owner_count = @min(top, ranking.owners.len); for (ranking.owners[0..owner_count]) |owner| { try writeScopeJson(out, owner); } try out.endArray(); try out.objectField("sources"); try out.beginArray(); const source_count = @min(top, ranking.sources.len); for (ranking.sources[0..source_count]) |source| { try writeSourceJson(out, source); } try out.endArray(); try out.endObject(); } try out.endObject();}fn writeTraceIntegrityJson( out: *pretty_json.Writer, integrity: memtrace.CaptureIntegrity,) !void { try out.beginObject(); try out.objectField("method"); try out.write(memtrace.analysis.capture_integrity_method); try out.objectField("status"); try out.write(integrity.status); try out.objectField("action"); try out.write(integrity.action); try out.objectField("message"); try out.write(integrity.message); try out.objectField("event_count"); try out.write(integrity.event_count); try out.objectField("sequenced_event_count"); try out.write(integrity.sequenced_event_count); try out.objectField("unsequenced_event_count"); try out.write(integrity.unsequenced_event_count); try out.objectField("first_sequence"); try out.write(integrity.first_sequence); try out.objectField("last_sequence"); try out.write(integrity.last_sequence); try out.objectField("sequence_gap_count"); try out.write(integrity.sequence_gap_count); try out.objectField("missing_sequence_event_count"); try out.write(integrity.missing_sequence_event_count); try out.objectField("sequence_regression_count"); try out.write(integrity.sequence_regression_count); try out.objectField("start_event_count"); try out.write(integrity.start_event_count); try out.objectField("stop_event_count"); try out.write(integrity.stop_event_count); try out.objectField("start_sequence"); try out.write(integrity.start_sequence); try out.objectField("stop_sequence"); try out.write(integrity.stop_sequence); try out.objectField("unbalanced_event_count"); try out.write(integrity.unbalanced_event_count); try out.endObject();}fn writeTraceSummaryJson(out: *pretty_json.Writer, summary: TraceSummary) !void { try out.beginObject(); try out.objectField("events"); try out.write(summary.events); try out.objectField("allocations"); try out.write(summary.allocations); try out.objectField("frees"); try out.write(summary.frees); try out.objectField("live_allocations"); try out.write(summary.live_allocations); try out.objectField("allocated_bytes"); try out.write(summary.allocated_bytes); try out.objectField("freed_bytes"); try out.write(summary.freed_bytes); try out.objectField("live_bytes"); try out.write(summary.live_bytes); try out.objectField("high_water_live_bytes"); try out.write(summary.high_water_live_bytes); try out.objectField("retained_bytes"); try out.write(summary.retained_bytes); try out.objectField("high_water_retained_bytes"); try out.write(summary.high_water_retained_bytes); try out.objectField("completed_lifetimes"); try out.write(summary.completed_lifetimes); try out.objectField("lifetime_total_events"); try out.write(summary.lifetime_total_events); try out.objectField("lifetime_mean_events"); try out.write(summary.lifetime_mean_events); try out.objectField("lifetime_max_events"); try out.write(summary.lifetime_max_events); try out.objectField("lifetime_total_byte_events"); try out.write(summary.lifetime_total_byte_events); try out.objectField("lifetime_mean_byte_events"); try out.write(summary.lifetime_mean_byte_events); try out.objectField("lifetime_max_byte_events"); try out.write(summary.lifetime_max_byte_events); try out.endObject();}fn writeScopeJson(out: *pretty_json.Writer, scope: Scope) !void { try out.beginObject(); try out.objectField("scope"); try out.write(scope.scope); try out.objectField("retained_bytes"); try out.write(scope.retained_bytes); try out.objectField("high_water_retained_bytes"); try out.write(scope.high_water_retained_bytes); try out.objectField("live_bytes"); try out.write(scope.live_bytes); try out.objectField("high_water_live_bytes"); try out.write(scope.high_water_live_bytes); try out.objectField("allocated_bytes"); try out.write(scope.allocated_bytes); try out.objectField("freed_bytes"); try out.write(scope.freed_bytes); try out.objectField("allocations"); try out.write(scope.allocations); try out.objectField("frees"); try out.write(scope.frees); try out.objectField("live_allocations"); try out.write(scope.live_allocations); try out.objectField("completed_lifetimes"); try out.write(scope.completed_lifetimes); try out.objectField("lifetime_total_events"); try out.write(scope.lifetime_total_events); try out.objectField("lifetime_mean_events"); try out.write(scope.lifetime_mean_events); try out.objectField("lifetime_max_events"); try out.write(scope.lifetime_max_events); try out.objectField("lifetime_total_byte_events"); try out.write(scope.lifetime_total_byte_events); try out.objectField("lifetime_mean_byte_events"); try out.write(scope.lifetime_mean_byte_events); try out.objectField("lifetime_max_byte_events"); try out.write(scope.lifetime_max_byte_events); try out.endObject();}fn writeSourceJson(out: *pretty_json.Writer, source: Source) !void { try out.beginObject(); try out.objectField("return_address"); try out.write(source.return_address); try out.objectField("function"); try out.write(source.function); try out.objectField("location"); try out.write(source.location); try out.objectField("retained_bytes"); try out.write(source.retained_bytes); try out.objectField("high_water_retained_bytes"); try out.write(source.high_water_retained_bytes); try out.objectField("live_bytes"); try out.write(source.live_bytes); try out.objectField("high_water_live_bytes"); try out.write(source.high_water_live_bytes); try out.objectField("allocated_bytes"); try out.write(source.allocated_bytes); try out.objectField("freed_bytes"); try out.write(source.freed_bytes); try out.objectField("allocations"); try out.write(source.allocations); try out.objectField("frees"); try out.write(source.frees); try out.objectField("live_allocations"); try out.write(source.live_allocations); try out.objectField("completed_lifetimes"); try out.write(source.completed_lifetimes); try out.objectField("lifetime_total_events"); try out.write(source.lifetime_total_events); try out.objectField("lifetime_mean_events"); try out.write(source.lifetime_mean_events); try out.objectField("lifetime_max_events"); try out.write(source.lifetime_max_events); try out.objectField("lifetime_total_byte_events"); try out.write(source.lifetime_total_byte_events); try out.objectField("lifetime_mean_byte_events"); try out.write(source.lifetime_mean_byte_events); try out.objectField("lifetime_max_byte_events"); try out.write(source.lifetime_max_byte_events); try out.endObject();}fn writeMetricsJson(out: *pretty_json.Writer, metrics: []const memory.Metric, top: usize) !void { try out.beginArray(); const count = @min(top, metrics.len); for (metrics[0..count]) |row| { try out.beginObject(); try out.objectField("key"); try out.write(row.key); try out.objectField("label"); try out.write(row.label); try out.objectField("unit"); try out.write(row.unit.name()); try out.objectField("value"); try out.write(row.value); try out.objectField("sample_count"); try out.write(row.sample_count); try out.objectField("distribution"); try out.write(row.distribution.name()); try out.objectField("source_kind"); try out.write(row.source_kind); try out.objectField("source_path"); try out.write(row.source_path); try out.objectField("source_line"); try out.write(row.source_line); try out.endObject(); } try out.endArray();}fn workload_retained_score(workload: Workload) u64 { if (workload.accounting.unclassified_bytes) |bytes| return bytes; if (workload.max_rss_bytes) |bytes| return bytes; if (workload.trace) |trace| return trace.summary.retained_bytes; return 0;}fn workload_retained_descending(items: []const Workload, left: usize, right: usize) bool { const left_score = workload_retained_score(items[left]); const right_score = workload_retained_score(items[right]); if (left_score == right_score) return std.mem.lessThan(u8, items[left].name, items[right].name); return left_score > right_score;}fn metric_value_descending(items: []const memory.Metric, left: usize, right: usize) bool { if (items[left].value == items[right].value) { return std.mem.lessThan(u8, items[left].label, items[right].label); } return items[left].value > items[right].value;}const TraceRankContext = struct { workloads: []const Workload, sort: memtrace.analysis.Sort,};fn trace_workload_descending( context: TraceRankContext, left: usize, right: usize,) bool { const left_summary = context.workloads[left].trace.?.summary; const right_summary = context.workloads[right].trace.?.summary; if (traceSummaryOrder( context.sort, left_summary, right_summary, )) |order| { return order; } return std.mem.lessThan( u8, context.workloads[left].name, context.workloads[right].name, );}fn traceSummaryOrder( sort: memtrace.analysis.Sort, left: TraceSummary, right: TraceSummary,) ?bool { switch (sort) { .retained => { if (left.retained_bytes != right.retained_bytes) { return left.retained_bytes > right.retained_bytes; } if (left.high_water_retained_bytes != right.high_water_retained_bytes) { return left.high_water_retained_bytes > right.high_water_retained_bytes; } }, .traffic => { if (left.allocated_bytes != right.allocated_bytes) { return left.allocated_bytes > right.allocated_bytes; } if (left.allocations != right.allocations) { return left.allocations > right.allocations; } }, .lifetime => { if (left.lifetime_total_byte_events != right.lifetime_total_byte_events) { return left.lifetime_total_byte_events > right.lifetime_total_byte_events; } if (left.lifetime_total_events != right.lifetime_total_events) { return left.lifetime_total_events > right.lifetime_total_events; } if (left.lifetime_max_events != right.lifetime_max_events) { return left.lifetime_max_events > right.lifetime_max_events; } if (left.lifetime_mean_events != right.lifetime_mean_events) { return left.lifetime_mean_events > right.lifetime_mean_events; } }, } if (left.retained_bytes != right.retained_bytes) { return left.retained_bytes > right.retained_bytes; } if (left.allocated_bytes != right.allocated_bytes) { return left.allocated_bytes > right.allocated_bytes; } if (left.lifetime_total_byte_events != right.lifetime_total_byte_events) { return left.lifetime_total_byte_events > right.lifetime_total_byte_events; } if (left.lifetime_total_events != right.lifetime_total_events) { return left.lifetime_total_events > right.lifetime_total_events; } return null;}fn rankedTraceWorkloads( allocator: std.mem.Allocator, workloads: []const Workload, sort: memtrace.analysis.Sort,) ![]usize { var indices: std.ArrayList(usize) = .empty; for (workloads, 0..) |workload, index| { const trace = workload.trace orelse continue; if (!std.mem.eql(u8, trace.integrity.status, "complete")) continue; try indices.append(allocator, index); } const owned = try indices.toOwnedSlice(allocator); std.mem.sort( usize, owned, TraceRankContext{ .workloads = workloads, .sort = sort }, trace_workload_descending, ); return owned;}fn rankedWorkloads(allocator: std.mem.Allocator, workloads: []const Workload) ![]usize { const indices = try allocator.alloc(usize, workloads.len); for (indices, 0..) |*slot, index| slot.* = index; std.mem.sort(usize, indices, workloads, workload_retained_descending); return indices;}fn rankedMetrics(allocator: std.mem.Allocator, metrics: []const memory.Metric) ![]usize { const indices = try allocator.alloc(usize, metrics.len); for (indices, 0..) |*slot, index| slot.* = index; std.mem.sort(usize, indices, metrics, metric_value_descending); return indices;}fn fixtureIntegrity(status: []const u8) memtrace.CaptureIntegrity { return .{ .status = status, .action = "inspect trace integrity", .message = null, .event_count = 0, .sequenced_event_count = 0, .unsequenced_event_count = 0, .first_sequence = null, .last_sequence = null, .sequence_gap_count = 0, .missing_sequence_event_count = 0, .sequence_regression_count = 0, .start_event_count = 0, .stop_event_count = 0, .start_sequence = null, .stop_sequence = null, .unbalanced_event_count = 0, };}fn fixtureRankWorkload( name: []const u8, integrity_status: []const u8, summary: TraceSummary,) Workload { const empty_ranking = TraceRanking{ .owners = &.{}, .sources = &.{}, }; return .{ .name = name, .package = "lib/fixture", .step = "fixture-bench", .status = "passed", .max_rss_kib = null, .max_rss_bytes = null, .trace_path = name, .trace = .{ .path = name, .integrity = fixtureIntegrity(integrity_status), .summary = summary, .scopes = &.{}, .rankings = .{ .retained = empty_ranking, .traffic = empty_ranking, .lifetime = empty_ranking, }, }, .trace_fault = null, .memory_metrics = &.{}, .accounting = .{ .state = "measured", .rss_bytes = null, .attributed_retained_bytes = null, .unclassified_bytes = null, .coverage_percent = null, }, };}test "profiling memory rankings separate allocation pressure dimensions" { const workloads = [_]Workload{ fixtureRankWorkload("retained", "complete", .{ .retained_bytes = 300, .allocated_bytes = 100, .allocations = 10, .lifetime_total_events = 10, .lifetime_total_byte_events = 100, }), fixtureRankWorkload("traffic", "complete", .{ .retained_bytes = 20, .allocated_bytes = 500, .allocations = 50, .lifetime_total_events = 20, .lifetime_total_byte_events = 200, }), fixtureRankWorkload("lifetime", "complete", .{ .retained_bytes = 10, .allocated_bytes = 20, .allocations = 2, .lifetime_total_events = 1, .lifetime_total_byte_events = 900, }), fixtureRankWorkload("partial", "missing_stop_event", .{ .retained_bytes = 1000, .allocated_bytes = 1000, .allocations = 1000, .lifetime_total_events = 1000, .lifetime_total_byte_events = 1000, }), }; const retained = try rankedTraceWorkloads( std.testing.allocator, &workloads, .retained, ); defer std.testing.allocator.free(retained); const traffic = try rankedTraceWorkloads( std.testing.allocator, &workloads, .traffic, ); defer std.testing.allocator.free(traffic); const lifetime = try rankedTraceWorkloads( std.testing.allocator, &workloads, .lifetime, ); defer std.testing.allocator.free(lifetime); try std.testing.expectEqual(@as(usize, 3), retained.len); try std.testing.expectEqualStrings("retained", workloads[retained[0]].name); try std.testing.expectEqualStrings("traffic", workloads[traffic[0]].name); try std.testing.expectEqualStrings("lifetime", workloads[lifetime[0]].name);}const fixture_trace_start = "{\"v\":" ++ fixture_trace_version ++ ",\"seq\":1,\"kind\":\"trace.start\"}\n";const fixture_allocator = "{\"v\":" ++ fixture_trace_version ++ ",\"seq\":2,\"kind\":\"allocator\",\"allocator_id\":1," ++ "\"retains_freed_memory\":true}\n";const fixture_allocation = "{\"v\":" ++ fixture_trace_version ++ ",\"seq\":3,\"kind\":\"alloc\",\"allocator_id\":1," ++ "\"allocation_id\":1,\"address\":128,\"len\":64,\"scope\":\"root/phase\"," ++ "\"return_address\":2748}\n";const fixture_free = "{\"v\":" ++ fixture_trace_version ++ ",\"seq\":4,\"kind\":\"free\",\"allocator_id\":1," ++ "\"allocation_id\":1,\"address\":128,\"len\":64,\"scope\":\"root/phase\"," ++ "\"return_address\":2748}\n";const fixture_trace_stop_four = "{\"v\":" ++ fixture_trace_version ++ ",\"seq\":4,\"kind\":\"trace.stop\"}\n";const fixture_trace_stop_five = "{\"v\":" ++ fixture_trace_version ++ ",\"seq\":5,\"kind\":\"trace.stop\"}\n";fn reportFixtureWorkloadRow( allocator: std.mem.Allocator, run_id: []const u8, name: []const u8, package: []const u8, step: []const u8,) ![]u8 { var bytes: std.Io.Writer.Allocating = .init(allocator); errdefer bytes.deinit(); var json_writer = pretty_json.Writer.init(&bytes.writer, .minified); const row = try json_writer.object(); try row.fields(.{ .schema = record.workload_schema, .event = "workload", .run_id = run_id, }); try row.field("workload", .{ .name = name, .package = package, .step = step, .surface = "profile", }); try row.fields(.{ .scope = "whole-step", .causal = false, .tracy = false, .trace_allocations = true, }); try row.field("status", .{ .state = "passed", .exit_code = @as(u8, 0), }); try row.field("timing", .{ .wall_ns = @as(u8, 10) }); try row.field("resources", .{ .max_rss_kib = @as(u8, 1) }); const workload_root = try std.fmt.allocPrint( allocator, "zig-out/profiling/{s}/workloads/{s}", .{ run_id, name }, ); defer allocator.free(workload_root); const measurement_row = try row.object("measurement"); try measurement_row.fields(.{ .execution_count = @as(u8, 1), .acquisition_order = "fixed_plan_order", .workload_output_retention = "all_measured_executions", .measured_artifact_layout = "workload_root", .allocation_summary_retention = "disabled", }); try measurement_row.field("benchmark_process_domain", .{ .benchmark_surface = false, .direct_execution = false, .host_profiler = false, .tracy = false, .causal = false, .allocation_trace = true, .capture_control = false, }); try measurement_row.field("benchmark_process_reduction", .{ .schema = reducer.schema, .state = "not_applicable", .process_count = @as(u8, 1), .reason = "outside_direct_benchmark_domain", }); const executions = try measurement_row.array("executions"); const execution = try executions.object(); try execution.fields(.{ .index = @as(u8, 1), .spawn_state = "spawned", .pid = @as(u8, 101), .exit_code = @as(u8, 0), .wall_ns = @as(u8, 10), }); const artifacts = try execution.object("artifacts"); try artifacts.field("root", workload_root); inline for (.{ .{ "stdout", "/stdout.txt" }, .{ "stderr", "/stderr.txt" }, .{ "bench_jsonl", "/bench.jsonl" }, .{ "coz_jsonl", "/bench.coz.jsonl" }, .{ "coz_analysis", "/bench.coz.analysis.json" }, .{ "tracy_jsonl", "/bench.tracy.jsonl" }, .{ "tracy_summary", "/bench.tracy.summary.jsonl" }, }) |field| { try artifacts.stringParts( field[0], &.{ workload_root, field[1] }, ); } try artifacts.stringParts( "allocations", &.{ workload_root, "/allocations.jsonl" }, ); try artifacts.stringParts( "structured", &.{ workload_root, "/structured.jsonl" }, ); try artifacts.fields(.{ .structured_rows = @as(u8, 0), .structured_parse_errors = @as(u8, 0), }); try artifacts.end(); try execution.end(); try executions.end(); try measurement_row.end(); const workload_artifacts = try row.object("artifacts"); try workload_artifacts.field("root", workload_root); try workload_artifacts.end(); try row.endLine(); return try bytes.toOwnedSlice();}test "profiling memory report accounts rss against retained trace bytes" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena_state.deinit(); const allocator = arena_state.allocator(); const base = try std.fs.path.join(allocator, &.{ ".zig-cache", "tmp", tmp.sub_path[0..] }); const root = try std.fs.path.join(allocator, &.{ base, "run-a" }); const workload_dir = try std.fs.path.join(allocator, &.{ root, "workloads", "w" }); try sys.fs.createDirPath(workload_dir); const manifest_path = try std.fs.path.join(allocator, &.{ root, "manifest.json" }); try sys.fs.writeFile(manifest_path, \\{"schema":"tiny.profiling.run/v1","event":"manifest","run_id":"run-a","selection":{"suite":"smoke","filters":[]},"artifacts":{"root":"zig-out/profiling/run-a","results":"zig-out/profiling/run-a/results.jsonl","manifest":"zig-out/profiling/run-a/manifest.json"}} \\ ); const results_path = try std.fs.path.join(allocator, &.{ root, "results.jsonl" }); const run_start = \\{"schema":"tiny.profiling.run/v1","event":"run_start","run_id":"run-a","selection":{"suite":"smoke","filters":[]},"artifacts":{"root":"zig-out/profiling/run-a"}} \\ ; const workload_row = try reportFixtureWorkloadRow(allocator, "run-a", "w", "lib/w", "w-bench"); const results = try std.mem.concat(allocator, u8, &.{ run_start, workload_row }); try sys.fs.writeFile(results_path, results); const allocations_path = try std.fs.path.join(allocator, &.{ workload_dir, "allocations.jsonl" }); try sys.fs.writeFile( allocations_path, fixture_trace_start ++ fixture_allocator ++ fixture_allocation ++ fixture_free ++ fixture_trace_stop_five, ); const report = try load(allocator, .{ .input = root, .top = 4 }); try std.testing.expectEqual(@as(usize, 1), report.workloads.len); try std.testing.expectEqualStrings("measured", report.workloads[0].accounting.state); try std.testing.expectEqual(@as(u64, 1024), report.workloads[0].accounting.rss_bytes.?); try std.testing.expectEqual(@as(u64, 64), report.workloads[0].accounting.attributed_retained_bytes.?); try std.testing.expectEqual(@as(u64, 960), report.workloads[0].accounting.unclassified_bytes.?); try std.testing.expectEqual(@as(u64, 64), report.workloads[0].trace.?.summary.retained_bytes); try std.testing.expectEqual( @as(u64, 64), report.workloads[0].trace.?.summary.lifetime_total_byte_events, ); try std.testing.expectEqualStrings("root/phase", report.workloads[0].trace.?.scopes[0].scope); inline for (std.meta.tags(memtrace.analysis.Sort)) |sort| { const ranking = report.workloads[0].trace.?.rankings.get(sort); try std.testing.expectEqual(@as(usize, 1), ranking.owners.len); try std.testing.expectEqualStrings( "root/phase", ranking.owners[0].scope, ); try std.testing.expectEqual(@as(usize, 1), ranking.sources.len); try std.testing.expectEqual( @as(u64, 2748), ranking.sources[0].return_address, ); try std.testing.expectEqual( @as(u64, 64), ranking.sources[0].allocated_bytes, ); try std.testing.expect(ranking.sources[0].function == null); try std.testing.expect(ranking.sources[0].location == null); try std.testing.expectEqual( @as(u64, 1), ranking.sources[0].completed_lifetimes, ); } try std.testing.expectEqual( @as(u64, 64), report.workloads[0].trace.?.rankings.lifetime.sources[0] .lifetime_total_byte_events, ); var json_out: std.Io.Writer.Allocating = .init(allocator); try writeJson( allocator, &json_out.writer, report, .{ .input = root, .top = 4 }, ); const rendered = try json_out.toOwnedSlice(); try std.testing.expect(std.mem.indexOf( u8, rendered, "\"allocation_rankings\":{\"retained\":[{\"rank\":1,\"workload\":\"w\"", ) != null); try std.testing.expect(std.mem.indexOf( u8, rendered, "\"traffic\":{\"owners\":[{\"scope\":\"root/phase\"", ) != null); try std.testing.expect(std.mem.indexOf( u8, rendered, "\"lifetime\":{\"owners\":[{\"scope\":\"root/phase\"", ) != null); try std.testing.expect(std.mem.indexOf( u8, rendered, "\"sources\":[{\"return_address\":2748", ) != null); try std.testing.expect(std.mem.indexOf( u8, rendered, "\"lifetime_total_byte_events\":64", ) != null); try sys.fs.writeFile( allocations_path, fixture_trace_start ++ fixture_allocator ++ fixture_allocation, ); const partial = try load(allocator, .{ .input = root, .top = 4 }); try std.testing.expectEqualStrings("trace_partial", partial.workloads[0].accounting.state); try std.testing.expectEqualStrings( "missing_stop_event", partial.workloads[0].trace.?.integrity.status, ); try std.testing.expect(partial.workloads[0].accounting.coverage_percent == null);}test "profiling memory report names invalid trace rows instead of aborting" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena_state.deinit(); const allocator = arena_state.allocator(); const base = try std.fs.path.join(allocator, &.{ ".zig-cache", "tmp", tmp.sub_path[0..] }); const root = try std.fs.path.join(allocator, &.{ base, "run-b" }); const good_dir = try std.fs.path.join(allocator, &.{ root, "workloads", "good" }); const bad_dir = try std.fs.path.join(allocator, &.{ root, "workloads", "bad" }); try sys.fs.createDirPath(good_dir); try sys.fs.createDirPath(bad_dir); const manifest_path = try std.fs.path.join(allocator, &.{ root, "manifest.json" }); try sys.fs.writeFile(manifest_path, \\{"schema":"tiny.profiling.run/v1","event":"manifest","run_id":"run-b","selection":{"suite":"smoke","filters":[]},"artifacts":{"root":"zig-out/profiling/run-b","results":"zig-out/profiling/run-b/results.jsonl","manifest":"zig-out/profiling/run-b/manifest.json"}} \\ ); const results_path = try std.fs.path.join(allocator, &.{ root, "results.jsonl" }); const run_start = \\{"schema":"tiny.profiling.run/v1","event":"run_start","run_id":"run-b","selection":{"suite":"smoke","filters":[]},"artifacts":{"root":"zig-out/profiling/run-b"}} \\ ; const good_row = try reportFixtureWorkloadRow( allocator, "run-b", "good", "lib/good", "good-bench", ); const bad_row = try reportFixtureWorkloadRow(allocator, "run-b", "bad", "lib/bad", "bad-bench"); const results = try std.mem.concat(allocator, u8, &.{ run_start, good_row, bad_row }); try sys.fs.writeFile(results_path, results); const good_trace = try std.fs.path.join(allocator, &.{ good_dir, "allocations.jsonl" }); try sys.fs.writeFile( good_trace, fixture_trace_start ++ fixture_allocator ++ fixture_allocation ++ fixture_trace_stop_four, ); const bad_trace = try std.fs.path.join(allocator, &.{ bad_dir, "allocations.jsonl" }); try sys.fs.writeFile( bad_trace, "{\"v\":" ++ fixture_trace_version ++ ",\"seq\":1,\"kind\":\"trace.start\"}\n" ++ "{\"v\":" ++ fixture_trace_version ++ ",\"seq\":2,\"kind\":\"mystery\"}\n", ); const report = try load(allocator, .{ .input = root, .top = 4 }); try std.testing.expectEqual(@as(usize, 2), report.workloads.len); try std.testing.expectEqual(@as(usize, 1), report.totals.invalid_traces); try std.testing.expectEqual(@as(usize, 1), report.totals.traced_workloads); try std.testing.expectEqual(@as(usize, 1), report.totals.complete_traces); const good = report.workloads[0]; try std.testing.expectEqualStrings("good", good.name); try std.testing.expect(good.trace_fault == null); try std.testing.expect(good.trace != null); const bad = report.workloads[1]; try std.testing.expectEqualStrings("bad", bad.name); try std.testing.expectEqualStrings("trace_invalid", bad.accounting.state); try std.testing.expect(bad.trace == null); const fault = bad.trace_fault.?; try std.testing.expectEqual(@as(u64, 2), fault.line); try std.testing.expectEqualStrings("InvalidEventJson", fault.reason);}test "profiling memory report renders json schema" { var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena_state.deinit(); const allocator = arena_state.allocator(); const run_value = analyze.Run{ .ref = .{ .run_id = "run-a", .root = "root", .manifest_path = "root/manifest.json", .results_path = "root/results.jsonl" }, .workloads = &.{}, }; const report = Report{ .run = run_value, .workloads = &.{}, .totals = .{} }; var out: std.Io.Writer.Allocating = .init(allocator); const options = Options{ .input = "root" }; try writeJson(allocator, &out.writer, report, options); const rendered = try out.toOwnedSlice(); defer allocator.free(rendered); try std.testing.expect(std.mem.indexOf(u8, rendered, "\"schema\":\"tiny.profiling.memory-report/v1\"") != null);}Source: src/profiling/report/root.zig:14
zig
pub const memory = @import("memory.zig");Audit
| Definitions | 18 |
|---|---|
| Public names | 18 |
| Members | 46 |
| Version | 26.7.0 |
| Revision | daab053ee433 |