tiny.memtrace.stack.roots
Defined in stack.
API (5)
Actions
Public operations.
Types and contracts
Public types and contracts.
Source
Source: lib/memtrace/src/stack/root.zig:14
zig
pub const roots = roots_mod;Source: lib/memtrace/src/stack/roots.zig
zig
const std = @import("std");const sys = @import("sys");const observe = @import("alloc_observe");const pretty_json = @import("pretty").json;const memtrace = @import("../root.zig");const analyze_mod = @import("analyze.zig");const capture_mod = @import("capture.zig");const identity_mod = @import("identity.zig");const symbolize_mod = @import("symbolize.zig");const coverage_mod = memtrace.coverage;const event_mod = memtrace.event;const Allocator = std.mem.Allocator;pub const Format = enum { text, jsonl,};pub const Sort = enum { roots, requested_bytes, backing_bytes, high_water,};pub const Detail = enum { summary, sources, full,};pub const Options = struct { top: usize = std.math.maxInt(usize), frame_limit: usize = capture_mod.max_frames_limit, format: Format = .text, binary_path: ?[]const u8 = null, selection: analyze_mod.Selection = .allocations, sort: Sort = .high_water, detail: Detail = .full, window: analyze_mod.Window = .{},};const Metrics = struct { operations: u64 = 0, logical_operations: u64 = 0, backing_operations: u64 = 0, physical_operations: u64 = 0, backing_requested_bytes: u128 = 0, physical_requested_bytes: u128 = 0, max_depth: u32 = 0, fn record(self: *Metrics, event: event_mod.ReplayEvent) !void { self.operations = try std.math.add(u64, self.operations, 1); const bytes = requestBytes(event); switch (event.layer) { .logical_allocator => { self.logical_operations = try std.math.add( u64, self.logical_operations, 1, ); }, .backing_boundary => { self.backing_operations = try std.math.add( u64, self.backing_operations, 1, ); self.backing_requested_bytes = try std.math.add( u128, self.backing_requested_bytes, bytes, ); }, .physical_page => { self.physical_operations = try std.math.add( u64, self.physical_operations, 1, ); self.physical_requested_bytes = try std.math.add( u128, self.physical_requested_bytes, bytes, ); }, } } fn merge(self: *Metrics, other: Metrics) !void { self.operations = try std.math.add( u64, self.operations, other.operations, ); self.logical_operations = try std.math.add( u64, self.logical_operations, other.logical_operations, ); self.backing_operations = try std.math.add( u64, self.backing_operations, other.backing_operations, ); self.physical_operations = try std.math.add( u64, self.physical_operations, other.physical_operations, ); self.backing_requested_bytes = try std.math.add( u128, self.backing_requested_bytes, other.backing_requested_bytes, ); self.physical_requested_bytes = try std.math.add( u128, self.physical_requested_bytes, other.physical_requested_bytes, ); self.max_depth = @max(self.max_depth, other.max_depth); } fn mergeChild(self: *Metrics, child: Metrics) !void { try self.merge(child); self.max_depth = @max( self.max_depth, try std.math.add(u32, child.max_depth, 1), ); }};const RootKey = struct { stack_id: u32, kind: event_mod.Kind, succeeded: bool, layer: event_mod.Layer, producer: observe.Producer,};const Lifetime = struct { live_allocations: u64 = 0, live_bytes: u128 = 0, high_water_live_bytes: u128 = 0, fn allocate(self: *Lifetime, len: usize) !void { self.live_allocations = try std.math.add( u64, self.live_allocations, 1, ); self.live_bytes = try std.math.add(u128, self.live_bytes, len); self.high_water_live_bytes = @max( self.high_water_live_bytes, self.live_bytes, ); } fn free(self: *Lifetime, len: usize) !void { if (self.live_allocations == 0 or self.live_bytes < len) { return error.InvalidCausalRootLifetime; } self.live_allocations -= 1; self.live_bytes -= len; } fn resize(self: *Lifetime, old_len: usize, new_len: usize) !void { if (self.live_bytes < old_len) { return error.InvalidCausalRootLifetime; } self.live_bytes -= old_len; self.live_bytes = try std.math.add(u128, self.live_bytes, new_len); self.high_water_live_bytes = @max( self.high_water_live_bytes, self.live_bytes, ); }};const Counters = struct { roots: u64 = 0, root_requested_bytes: u128 = 0, metrics: Metrics = .{}, lifetime: Lifetime = .{}, fn record( self: *Counters, event: event_mod.ReplayEvent, metrics: Metrics, ) !void { self.roots = try std.math.add(u64, self.roots, 1); self.root_requested_bytes = try std.math.add( u128, self.root_requested_bytes, requestBytes(event), ); try self.metrics.merge(metrics); } fn merge(self: *Counters, other: Counters) !void { self.roots = try std.math.add(u64, self.roots, other.roots); self.root_requested_bytes = try std.math.add( u128, self.root_requested_bytes, other.root_requested_bytes, ); try self.metrics.merge(other.metrics); }};const Summary = struct { key: RootKey, counters: Counters, definition: analyze_mod.Definition,};const Totals = struct { roots: u64 = 0, successful: u64 = 0, failed: u64 = 0, root_requested_bytes: u128 = 0, metrics: Metrics = .{}, lifetime: Lifetime = .{},};const SourceKey = struct { kind: event_mod.Kind, succeeded: bool, layer: event_mod.Layer, producer: observe.Producer, site: u64, caller: u64,};const Source = struct { key: SourceKey, counters: Counters, unique_stacks: u32,};const LiveAllocation = struct { root: RootKey, source: SourceKey, len: usize,};const Display = struct { source_groups: usize, displayed_sources: usize, root_stacks: usize, displayed_stacks: usize,};const ChildEvidence = struct { operation_id: u64, sequence: u64, stack_id: u32, scope_id: u32, kind: event_mod.Kind, layer: event_mod.Layer, producer: observe.Producer, succeeded: bool, requested_bytes: usize, return_address: u64, fn fromEvent(event: event_mod.ReplayEvent) ChildEvidence { return .{ .operation_id = event.operation_id, .sequence = event.seq orelse 0, .stack_id = event.stack_id, .scope_id = event.scope_id, .kind = event.kind, .layer = event.layer, .producer = event.producer, .succeeded = event.succeeded, .requested_bytes = if (event.kind.isMemoryOperation()) requestBytes(event) else 0, .return_address = event.return_address, }; }};const Pending = struct { metrics: Metrics = .{}, child: ChildEvidence,};const DanglingParent = struct { operation_id: u64, child: ChildEvidence, site_address: u64,};const OperationLedger = struct { seen: std.DynamicBitSetUnmanaged = .{}, unique: u64 = 0, minimum: u64 = std.math.maxInt(u64), maximum: u64 = 0, last_id: u64 = 0, last_parent: u64 = 0, fn deinit(self: *OperationLedger, allocator: Allocator) void { self.seen.deinit(allocator); self.* = undefined; } fn record( self: *OperationLedger, allocator: Allocator, operation_id: u64, parent_operation_id: u64, ) !void { const index = std.math.cast(usize, operation_id) orelse return error.CausalOperationIdOverflow; try self.ensureCapacity(allocator, index); if (self.seen.isSet(index)) { if (operation_id != self.last_id) { return error.NonContiguousCausalOperationGroup; } if (parent_operation_id != self.last_parent) { return error.CausalOperationParentConflict; } return; } self.seen.set(index); self.unique = try std.math.add(u64, self.unique, 1); self.minimum = @min(self.minimum, operation_id); self.maximum = @max(self.maximum, operation_id); self.last_id = operation_id; self.last_parent = parent_operation_id; } fn ensureCapacity( self: *OperationLedger, allocator: Allocator, index: usize, ) !void { if (index < self.seen.bit_length) return; const required = try std.math.add(usize, index, 1); const doubled = std.math.mul( usize, @max(self.seen.bit_length, 1024), 2, ) catch std.math.maxInt(usize); try self.seen.resize(allocator, @max(required, doubled), false); } fn validate(self: OperationLedger) !void { if (self.unique == 0) return; const span = try std.math.add( u64, self.maximum - self.minimum, 1, ); if (span != self.unique) return error.NonContiguousCausalOperations; }};const Analyzer = struct { allocator: Allocator, stack: analyze_mod.Analyzer, pending: std.AutoHashMapUnmanaged(u64, Pending) = .{}, groups: std.AutoHashMapUnmanaged(RootKey, Counters) = .{}, source_lifetimes: std.AutoHashMapUnmanaged(SourceKey, Lifetime) = .{}, live_allocations: std.AutoHashMapUnmanaged(u64, LiveAllocation) = .{}, operation_ledger: OperationLedger = .{}, lifetime: Lifetime = .{}, finished: bool = false, fn init(allocator: Allocator, window: analyze_mod.Window) Analyzer { return .{ .allocator = allocator, .stack = analyze_mod.Analyzer.init(allocator, window), }; } fn deinit(self: *Analyzer) void { self.stack.deinit(); self.pending.deinit(self.allocator); self.groups.deinit(self.allocator); self.source_lifetimes.deinit(self.allocator); self.live_allocations.deinit(self.allocator); self.operation_ledger.deinit(self.allocator); self.* = undefined; } fn ingestJsonLine(self: *Analyzer, line: []const u8) !void { if (self.finished) return error.CausalRootAnalyzerFinished; const text = std.mem.trim(u8, line, " \t\r\n"); if (text.len == 0) return; try self.stack.ingestJsonLine(text); if (identity_mod.isMetadataLine(text) or coverage_mod.isMetadataLine(text) or capture_mod.isMetadataLine(text)) { return; } const event = try event_mod.parseReplayFast(text); const memory_operation = event.kind.isMemoryOperation(); if (!memory_operation and event.kind != .lifecycle) return; if (event.operation_id == 0) return error.MissingCausalOperation; if (event.parent_operation_id >= event.operation_id and event.parent_operation_id != 0) { return error.CausalOperationCycle; } if (memory_operation) try self.recordExistingLifetime(event); try self.operation_ledger.record( self.allocator, event.operation_id, event.parent_operation_id, ); var metrics = if (self.pending.fetchRemove(event.operation_id)) |entry| entry.value.metrics else Metrics{}; if (memory_operation) try metrics.record(event); if (event.parent_operation_id == 0) { if (!memory_operation) return; if (!try self.stack.includes(event)) return; return self.recordRoot(event, metrics); } const parent = try self.pending.getOrPut( self.allocator, event.parent_operation_id, ); if (!parent.found_existing) { parent.value_ptr.* = .{ .child = ChildEvidence.fromEvent(event), }; } if (memory_operation) { try parent.value_ptr.metrics.mergeChild(metrics); } else { try parent.value_ptr.metrics.merge(metrics); } } fn finish(self: *Analyzer) !void { if (self.finished) return error.CausalRootAnalyzerFinished; try self.stack.validate(); try self.finishCausal(); } fn finishCausal(self: *Analyzer) !void { if (self.finished) return error.CausalRootAnalyzerFinished; if (self.pending.count() != 0) return error.DanglingParentOperation; try self.operation_ledger.validate(); try self.validateLifetimes(); self.finished = true; } fn validateLifetimes(self: *const Analyzer) !void { var group_allocations: u64 = 0; var group_bytes: u128 = 0; var groups = self.groups.valueIterator(); while (groups.next()) |counters| { group_allocations = try std.math.add( u64, group_allocations, counters.lifetime.live_allocations, ); group_bytes = try std.math.add( u128, group_bytes, counters.lifetime.live_bytes, ); } var source_allocations: u64 = 0; var source_bytes: u128 = 0; var sources = self.source_lifetimes.valueIterator(); while (sources.next()) |lifetime| { source_allocations = try std.math.add( u64, source_allocations, lifetime.live_allocations, ); source_bytes = try std.math.add( u128, source_bytes, lifetime.live_bytes, ); } if (group_allocations != self.lifetime.live_allocations or source_allocations != self.lifetime.live_allocations or self.live_allocations.count() != self.lifetime.live_allocations or group_bytes != self.lifetime.live_bytes or source_bytes != self.lifetime.live_bytes) { return error.InvalidCausalRootLifetime; } } fn collectDangling( self: *const Analyzer, ) !std.ArrayListUnmanaged(DanglingParent) { var result = std.ArrayListUnmanaged(DanglingParent).empty; errdefer result.deinit(self.allocator); try result.ensureTotalCapacity(self.allocator, self.pending.count()); var pending = self.pending.iterator(); while (pending.next()) |entry| { const child = entry.value_ptr.child; const site_address = if (child.stack_id != 0) (self.stack.stackDefinition(child.stack_id) orelse return error.MissingStackDefinition).call_addresses[0] else source: { const return_address = std.math.cast( usize, child.return_address, ) orelse return error.InvalidDanglingParentSite; if (return_address == 0) return error.MissingDanglingParentSite; break :source capture_mod.callAddress(return_address); }; result.appendAssumeCapacity(.{ .operation_id = entry.key_ptr.*, .child = child, .site_address = site_address, }); } std.mem.sort( DanglingParent, result.items, {}, danglingParentLessThan, ); return result; } fn recordRoot( self: *Analyzer, event: event_mod.ReplayEvent, metrics: Metrics, ) !void { const key = RootKey{ .stack_id = event.stack_id, .kind = event.kind, .succeeded = event.succeeded, .layer = event.layer, .producer = event.producer, }; const entry = try self.groups.getOrPut(self.allocator, key); if (!entry.found_existing) entry.value_ptr.* = .{}; try entry.value_ptr.record(event, metrics); try self.recordLifetime(event, key); } fn recordLifetime( self: *Analyzer, event: event_mod.ReplayEvent, root: RootKey, ) !void { switch (event.kind) { .alloc => if (event.succeeded and event.tracked and event.len != 0) { try self.recordAllocation(event, root); }, else => {}, } } fn recordExistingLifetime( self: *Analyzer, event: event_mod.ReplayEvent, ) !void { switch (event.kind) { .free, .release => if (event.tracked and event.len != 0) { try self.recordFree(event); }, .resize => if (event.succeeded and event.tracked) { try self.recordResize(event); }, .remap => if (event.succeeded and event.tracked) { try self.recordRemap(event); }, else => {}, } } fn recordAllocation( self: *Analyzer, event: event_mod.ReplayEvent, root: RootKey, ) !void { const source = self.sourceKey(root); const allocation_key = allocationKey( event.allocation_id, event.address, ); if (allocation_key == 0) return error.MissingCausalAllocationIdentity; const live = try self.live_allocations.getOrPut( self.allocator, allocation_key, ); if (live.found_existing) return error.DuplicateCausalAllocation; live.value_ptr.* = .{ .root = root, .source = source, .len = event.len, }; try self.lifetime.allocate(event.len); try self.groups.getPtr(root).?.lifetime.allocate(event.len); const source_entry = try self.source_lifetimes.getOrPut( self.allocator, source, ); if (!source_entry.found_existing) source_entry.value_ptr.* = .{}; try source_entry.value_ptr.allocate(event.len); } fn recordFree( self: *Analyzer, event: event_mod.ReplayEvent, ) !void { const allocation_key = allocationKey( event.allocation_id, event.address, ); const removed = self.live_allocations.fetchRemove(allocation_key) orelse { return; }; try self.release(removed.value, removed.value.len); } fn recordResize( self: *Analyzer, event: event_mod.ReplayEvent, ) !void { const allocation_key = allocationKey( event.allocation_id, event.address, ); const live = self.live_allocations.getPtr(allocation_key) orelse { return; }; try self.resize(live.*, event.len); live.len = event.len; } fn recordRemap( self: *Analyzer, event: event_mod.ReplayEvent, ) !void { const old_key = allocationKey( event.allocation_id, event.old_address, ); const new_key = allocationKey( event.allocation_id, event.address, ); if (old_key == new_key) return self.recordResize(event); const removed = self.live_allocations.fetchRemove(old_key) orelse { return; }; const entry = try self.live_allocations.getOrPut( self.allocator, new_key, ); if (entry.found_existing) return error.DuplicateCausalAllocation; entry.value_ptr.* = removed.value; try self.resize(entry.value_ptr.*, event.len); entry.value_ptr.len = event.len; } fn release( self: *Analyzer, live: LiveAllocation, len: usize, ) !void { try self.lifetime.free(len); try self.groups.getPtr(live.root).?.lifetime.free(len); try self.source_lifetimes.getPtr(live.source).?.free(len); } fn resize( self: *Analyzer, live: LiveAllocation, new_len: usize, ) !void { try self.lifetime.resize(live.len, new_len); try self.groups.getPtr(live.root).?.lifetime.resize( live.len, new_len, ); try self.source_lifetimes.getPtr(live.source).?.resize( live.len, new_len, ); } fn sourceKey(self: *const Analyzer, root: RootKey) SourceKey { const definition = self.stack.stackDefinition(root.stack_id).?; return .{ .kind = root.kind, .succeeded = root.succeeded, .layer = root.layer, .producer = root.producer, .site = definition.call_addresses[0], .caller = callerAddress(definition), }; } fn collect( self: *const Analyzer, selection: analyze_mod.Selection, ) !std.ArrayListUnmanaged(Summary) { if (!self.finished) return error.CausalRootAnalyzerNotFinished; var result = std.ArrayListUnmanaged(Summary).empty; errdefer result.deinit(self.allocator); try result.ensureTotalCapacity(self.allocator, self.groups.count()); var groups = self.groups.iterator(); while (groups.next()) |entry| { if (!selection.includes(entry.key_ptr.kind)) continue; result.appendAssumeCapacity(.{ .key = entry.key_ptr.*, .counters = entry.value_ptr.*, .definition = self.stack.stackDefinition( entry.key_ptr.stack_id, ).?, }); } return result; } fn totals( self: *const Analyzer, selection: analyze_mod.Selection, ) !Totals { if (!self.finished) return error.CausalRootAnalyzerNotFinished; var result: Totals = .{}; var groups = self.groups.iterator(); while (groups.next()) |entry| { if (!selection.includes(entry.key_ptr.kind)) continue; result.roots = try std.math.add( u64, result.roots, entry.value_ptr.roots, ); result.root_requested_bytes = try std.math.add( u128, result.root_requested_bytes, entry.value_ptr.root_requested_bytes, ); if (entry.key_ptr.succeeded) { result.successful = try std.math.add( u64, result.successful, entry.value_ptr.roots, ); } else { result.failed = try std.math.add( u64, result.failed, entry.value_ptr.roots, ); } try result.metrics.merge(entry.value_ptr.metrics); } result.lifetime = self.lifetime; return result; }};pub fn writeFromPath( allocator: Allocator, events_path: []const u8, writer: *std.Io.Writer, options: Options,) !void { if (options.top == 0 or options.frame_limit == 0 or options.frame_limit > capture_mod.max_frames_limit) { return error.InvalidCausalRootReportLimit; } try options.window.validate(); var analyzer = Analyzer.init(allocator, options.window); defer analyzer.deinit(); try ingestPath(&analyzer, events_path); try analyzer.stack.validate(); var inferred_binary: ?[]u8 = null; defer if (inferred_binary) |path| allocator.free(path); const binary_path = options.binary_path orelse inferred: { inferred_binary = try identity_mod.artifactPathAlloc( allocator, events_path, ); break :inferred inferred_binary.?; }; const actual_digest = identity_mod.fileDigest( allocator, binary_path, ) catch |err| switch (err) { error.FileNotFound => return error.MissingExecutableArtifact, else => return err, }; const expected_digest = analyzer.stack.executable_digest.?; if (!std.mem.eql(u8, &actual_digest, &expected_digest)) { return error.ExecutableIdentityMismatch; } analyzer.finishCausal() catch |err| switch (err) { error.DanglingParentOperation => { try writeDanglingReport( allocator, writer, &analyzer, options, binary_path, expected_digest, ); return err; }, else => return err, }; var summaries = try analyzer.collect(options.selection); defer summaries.deinit(allocator); const totals = try analyzer.totals(options.selection); std.mem.sort(Summary, summaries.items, options.sort, summaryGreaterThan); const stack_limit = if (options.detail == .full) @min(options.top, summaries.items.len) else 0; var sources = try collectSources(allocator, &analyzer, summaries.items); defer sources.deinit(allocator); std.mem.sort(Source, sources.items, options.sort, sourceGreaterThan); const source_limit = if (options.detail == .summary) 0 else @min(options.top, sources.items.len); const display = Display{ .source_groups = sources.items.len, .displayed_sources = source_limit, .root_stacks = summaries.items.len, .displayed_stacks = stack_limit, }; var addresses = try collectAddresses( allocator, summaries.items[0..stack_limit], sources.items[0..source_limit], options.frame_limit, ); defer addresses.deinit(allocator); var symbols = if (addresses.items.len == 0) null else try symbolize_mod.resolveAlloc( allocator, binary_path, addresses.items, ); defer if (symbols) |*resolved| resolved.deinit(allocator); switch (options.format) { .text => try writeText( writer, &analyzer, totals, options.selection, options.sort, options.window, display, sources.items[0..source_limit], summaries.items[0..stack_limit], symbols, options.frame_limit, expected_digest, ), .jsonl => try writeJsonl( writer, &analyzer, totals, options.selection, options.sort, options.window, display, sources.items[0..source_limit], summaries.items[0..stack_limit], symbols, options.frame_limit, expected_digest, ), }}fn ingestPath(analyzer: *Analyzer, path: []const u8) !void { var file = try sys.fs.cwd().openFile(sys.fs.debugIo(), path, .{}); defer file.close(sys.fs.debugIo()); var buffer: [64 * 1024]u8 = undefined; var reader = file.reader(sys.fs.debugIo(), &buffer); 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; try analyzer.ingestJsonLine(actual); }}fn writeDanglingReport( allocator: Allocator, writer: *std.Io.Writer, analyzer: *const Analyzer, options: Options, binary_path: []const u8, digest: identity_mod.Digest,) !void { var dangling = try analyzer.collectDangling(); defer dangling.deinit(allocator); const limit = @min(options.top, dangling.items.len); var addresses = try collectDanglingAddresses( allocator, dangling.items[0..limit], ); defer addresses.deinit(allocator); var symbols = if (addresses.items.len == 0) null else try symbolize_mod.resolveAlloc( allocator, binary_path, addresses.items, ); defer if (symbols) |*resolved| resolved.deinit(allocator); switch (options.format) { .text => try writeDanglingText( writer, analyzer, dangling.items[0..limit], dangling.items.len, symbols, digest, ), .jsonl => try writeDanglingJsonl( writer, analyzer, dangling.items[0..limit], dangling.items.len, symbols, digest, ), }}fn collectDanglingAddresses( allocator: Allocator, dangling: []const DanglingParent,) !std.ArrayListUnmanaged(u64) { var seen = std.AutoHashMapUnmanaged(u64, void){}; defer seen.deinit(allocator); var addresses = std.ArrayListUnmanaged(u64).empty; errdefer addresses.deinit(allocator); try addresses.ensureTotalCapacity(allocator, dangling.len); for (dangling) |entry| { const address = entry.site_address; const result = try seen.getOrPut(allocator, address); if (result.found_existing) continue; addresses.appendAssumeCapacity(address); } std.mem.sort(u64, addresses.items, {}, lessThan); return addresses;}fn writeDanglingText( writer: *std.Io.Writer, analyzer: *const Analyzer, dangling: []const DanglingParent, total: usize, symbols: ?symbolize_mod.Symbols, digest: identity_mod.Digest,) !void { const digest_hex = std.fmt.bytesToHex(digest, .lower); try writer.print( "causal_roots_error code=dangling_parent_operation " ++ "diagnostic_universe=all_memory_operations missing_parents={d} " ++ "displayed_missing_parents={d} binary_sha256={s}\n", .{ total, dangling.len, digest_hex }, ); for (dangling) |entry| { const child = entry.child; try writer.print( "dangling_parent missing_parent_operation_id={d} " ++ "observed_child_operation_id={d} observed_child_sequence={d} " ++ "observed_child_scope_id={d} observed_child_operation={s} " ++ "observed_child_layer={s} observed_child_producer={s} " ++ "observed_child_outcome={s} observed_child_requested_bytes={d} " ++ "observed_child_stack_id={d} observed_child_scope=", .{ entry.operation_id, child.operation_id, child.sequence, child.scope_id, child.kind.tag(), child.layer.tag(), @tagName(child.producer), outcomeTag(child.succeeded), child.requested_bytes, child.stack_id, }, ); const scope = analyzer.stack.scopePath(child.scope_id) orelse "unknown"; try pretty_json.writeString(writer, scope); try writeTextAddress( writer, " observed_child_site", entry.site_address, symbols, ); try writer.writeByte('\n'); }}fn writeDanglingJsonl( writer: *std.Io.Writer, analyzer: *const Analyzer, dangling: []const DanglingParent, total: usize, symbols: ?symbolize_mod.Symbols, digest: identity_mod.Digest,) !void { var header_stream = pretty_json.Writer.init(writer, .minified); const header = try header_stream.object(); try header.field("kind", "causal_root_error"); try header.field("code", "dangling_parent_operation"); try header.field("diagnostic_universe", "all_memory_operations"); try header.field("missing_parents", total); try header.field("displayed_missing_parents", dangling.len); try header.hexString("binary_sha256", &digest); try header.endLine(); for (dangling) |entry| { try writeDanglingJsonEntry(writer, analyzer, entry, symbols); }}fn writeDanglingJsonEntry( writer: *std.Io.Writer, analyzer: *const Analyzer, entry: DanglingParent, symbols: ?symbolize_mod.Symbols,) !void { const child = entry.child; var stream = pretty_json.Writer.init(writer, .minified); const object = try stream.object(); try object.field("kind", "causal_root_dangling_parent"); try object.field("missing_parent_operation_id", entry.operation_id); try object.field("observed_child_operation_id", child.operation_id); try object.field("observed_child_sequence", child.sequence); try object.field("observed_child_scope_id", child.scope_id); try object.field("observed_child_scope", analyzer.stack.scopePath( child.scope_id, )); try object.field("observed_child_operation", child.kind.tag()); try object.field("observed_child_layer", child.layer.tag()); try object.field("observed_child_producer", @tagName(child.producer)); try object.field("observed_child_succeeded", child.succeeded); try object.field("observed_child_requested_bytes", child.requested_bytes); try object.field("observed_child_stack_id", child.stack_id); const address = entry.site_address; try object.field("observed_child_site_address", address); try writeJsonSymbol(object, "observed_child_site", address, symbols); try object.endLine();}fn collectSources( allocator: Allocator, analyzer: *const Analyzer, summaries: []const Summary,) !std.ArrayListUnmanaged(Source) { var counts = std.AutoHashMapUnmanaged(SourceKey, Source){}; defer counts.deinit(allocator); for (summaries) |summary| { const key = SourceKey{ .kind = summary.key.kind, .succeeded = summary.key.succeeded, .layer = summary.key.layer, .producer = summary.key.producer, .site = summary.definition.call_addresses[0], .caller = callerAddress(summary.definition), }; const entry = try counts.getOrPut(allocator, key); if (!entry.found_existing) { entry.value_ptr.* = .{ .key = key, .counters = .{}, .unique_stacks = 0, }; } try entry.value_ptr.counters.merge(summary.counters); entry.value_ptr.unique_stacks = try std.math.add( u32, entry.value_ptr.unique_stacks, 1, ); } var sources = std.ArrayListUnmanaged(Source).empty; errdefer sources.deinit(allocator); try sources.ensureTotalCapacity(allocator, counts.count()); var values = counts.valueIterator(); while (values.next()) |source| sources.appendAssumeCapacity(source.*); for (sources.items) |*source| { source.counters.lifetime = analyzer.source_lifetimes.get( source.key, ) orelse .{}; } return sources;}fn collectAddresses( allocator: Allocator, summaries: []const Summary, sources: []const Source, frame_limit: usize,) !std.ArrayListUnmanaged(u64) { var seen = std.AutoHashMapUnmanaged(u64, void){}; defer seen.deinit(allocator); var addresses = std.ArrayListUnmanaged(u64).empty; errdefer addresses.deinit(allocator); for (sources) |source| { try appendAddress(allocator, &seen, &addresses, source.key.site); if (source.key.caller != 0) { try appendAddress( allocator, &seen, &addresses, source.key.caller, ); } } for (summaries) |summary| { const limit = @min( frame_limit, summary.definition.call_addresses.len, ); for (summary.definition.call_addresses[0..limit]) |address| { try appendAddress(allocator, &seen, &addresses, address); } } std.mem.sort(u64, addresses.items, {}, lessThan); return addresses;}fn appendAddress( allocator: Allocator, seen: *std.AutoHashMapUnmanaged(u64, void), addresses: *std.ArrayListUnmanaged(u64), address: u64,) !void { const entry = try seen.getOrPut(allocator, address); if (entry.found_existing) return; try addresses.append(allocator, address);}fn callerAddress(definition: analyze_mod.Definition) u64 { const site = definition.call_addresses[0]; for (definition.call_addresses[1..], 1..) |address, index| { if (address != site) continue; const caller_index = index + 1; if (caller_index < definition.call_addresses.len) { return definition.call_addresses[caller_index]; } return 0; } return 0;}fn summaryGreaterThan(sort: Sort, left: Summary, right: Summary) bool { if (counterOrder(sort, left.counters, right.counters)) |order| { return order; } if (left.counters.roots != right.counters.roots) { return left.counters.roots > right.counters.roots; } if (left.counters.root_requested_bytes != right.counters.root_requested_bytes) { return left.counters.root_requested_bytes > right.counters.root_requested_bytes; } if (left.counters.metrics.operations != right.counters.metrics.operations) { return left.counters.metrics.operations > right.counters.metrics.operations; } return keyLessThan(left.key, right.key);}fn sourceGreaterThan(sort: Sort, left: Source, right: Source) bool { if (counterOrder(sort, left.counters, right.counters)) |order| { return order; } if (left.counters.roots != right.counters.roots) { return left.counters.roots > right.counters.roots; } if (left.counters.root_requested_bytes != right.counters.root_requested_bytes) { return left.counters.root_requested_bytes > right.counters.root_requested_bytes; } if (left.key.site != right.key.site) return left.key.site < right.key.site; if (left.key.caller != right.key.caller) { return left.key.caller < right.key.caller; } return sourceKeyLessThan(left.key, right.key);}fn counterOrder( sort: Sort, left: Counters, right: Counters,) ?bool { const left_value: u128 = switch (sort) { .roots => left.roots, .requested_bytes => left.root_requested_bytes, .backing_bytes => left.metrics.backing_requested_bytes, .high_water => left.lifetime.high_water_live_bytes, }; const right_value: u128 = switch (sort) { .roots => right.roots, .requested_bytes => right.root_requested_bytes, .backing_bytes => right.metrics.backing_requested_bytes, .high_water => right.lifetime.high_water_live_bytes, }; if (left_value == right_value) return null; return left_value > right_value;}fn keyLessThan(left: RootKey, right: RootKey) bool { if (left.kind != right.kind) { return @backingInt(left.kind) < @backingInt(right.kind); } if (left.layer != right.layer) { return @backingInt(left.layer) < @backingInt(right.layer); } if (left.producer != right.producer) { return @backingInt(left.producer) < @backingInt(right.producer); } if (left.succeeded != right.succeeded) return left.succeeded; return left.stack_id < right.stack_id;}fn sourceKeyLessThan(left: SourceKey, right: SourceKey) bool { if (left.kind != right.kind) { return @backingInt(left.kind) < @backingInt(right.kind); } if (left.layer != right.layer) { return @backingInt(left.layer) < @backingInt(right.layer); } if (left.producer != right.producer) { return @backingInt(left.producer) < @backingInt(right.producer); } return left.succeeded and !right.succeeded;}fn selectionTag(selection: analyze_mod.Selection) []const u8 { return switch (selection) { .allocations => "allocations", .all => "all", };}fn sortTag(sort: Sort) []const u8 { return switch (sort) { .roots => "roots", .requested_bytes => "requested_bytes", .backing_bytes => "backing_bytes", .high_water => "high_water", };}fn allocationKey(allocation_id: u64, address: u64) u64 { return if (allocation_id != 0) allocation_id else address;}fn outcomeTag(succeeded: bool) []const u8 { return if (succeeded) "success" else "failure";}fn requestBytes(event: event_mod.ReplayEvent) usize { return switch (event.kind) { .free, .release, .unmap => event.old_len, .alloc, .resize, .remap, .map, .protect, .discard, .decommit, .advise => event.len, else => unreachable, };}fn lessThan(_: void, left: u64, right: u64) bool { return left < right;}fn danglingParentLessThan( _: void, left: DanglingParent, right: DanglingParent,) bool { if (left.operation_id != right.operation_id) { return left.operation_id < right.operation_id; } if (left.child.sequence != right.child.sequence) { return left.child.sequence < right.child.sequence; } return left.child.operation_id < right.child.operation_id;}fn writeText( writer: *std.Io.Writer, analyzer: *const Analyzer, totals: Totals, selection: analyze_mod.Selection, sort: Sort, window: analyze_mod.Window, display: Display, sources: []const Source, summaries: []const Summary, maybe_symbols: ?symbolize_mod.Symbols, frame_limit: usize, digest: identity_mod.Digest,) !void { const digest_hex = std.fmt.bytesToHex(digest, .lower); const coverage = analyzer.stack.coverage.?; try writer.print( "causal_roots status={s} universe={s} selection={s} sort={s} " ++ "roots={d} " ++ "successful={d} failed={d} root_requested_bytes={d} " ++ "live_allocations={d} live_bytes={d} " ++ "high_water_live_bytes={d} " ++ "causal_operations={d} logical_operations={d} " ++ "backing_operations={d} physical_operations={d} " ++ "backing_requested_bytes={d} physical_requested_bytes={d} " ++ "max_depth={d} source_groups={d} displayed_sources={d} " ++ "root_stacks={d} displayed_stacks={d} binary_sha256={s}", .{ coverage.statusTag(), coverage.universe.tag(), selectionTag(selection), sortTag(sort), totals.roots, totals.successful, totals.failed, totals.root_requested_bytes, totals.lifetime.live_allocations, totals.lifetime.live_bytes, totals.lifetime.high_water_live_bytes, totals.metrics.operations, totals.metrics.logical_operations, totals.metrics.backing_operations, totals.metrics.physical_operations, totals.metrics.backing_requested_bytes, totals.metrics.physical_requested_bytes, totals.metrics.max_depth, display.source_groups, display.displayed_sources, display.root_stacks, display.displayed_stacks, digest_hex, }, ); try writeTextWindow(writer, window, "root_operation"); try writer.writeByte('\n'); try writeCoverage(writer, coverage); for (sources) |source| { try writer.print( "root_source layer={s} producer={s} operation={s} outcome={s} " ++ "roots={d} root_requested_bytes={d} live_allocations={d} " ++ "live_bytes={d} high_water_live_bytes={d} " ++ "causal_operations={d} " ++ "logical_operations={d} backing_operations={d} " ++ "physical_operations={d} backing_requested_bytes={d} " ++ "physical_requested_bytes={d} max_depth={d} unique_stacks={d}", .{ source.key.layer.tag(), @tagName(source.key.producer), source.key.kind.tag(), outcomeTag(source.key.succeeded), source.counters.roots, source.counters.root_requested_bytes, source.counters.lifetime.live_allocations, source.counters.lifetime.live_bytes, source.counters.lifetime.high_water_live_bytes, source.counters.metrics.operations, source.counters.metrics.logical_operations, source.counters.metrics.backing_operations, source.counters.metrics.physical_operations, source.counters.metrics.backing_requested_bytes, source.counters.metrics.physical_requested_bytes, source.counters.metrics.max_depth, source.unique_stacks, }, ); try writeTextAddress( writer, " site", source.key.site, maybe_symbols, ); if (source.key.caller != 0) { try writeTextAddress( writer, " caller", source.key.caller, maybe_symbols, ); } else { try writer.writeAll(" caller=unavailable"); } try writer.writeByte('\n'); } for (summaries) |summary| { const displayed = @min( frame_limit, summary.definition.call_addresses.len, ); try writer.print( "root_stack id={d} layer={s} producer={s} operation={s} " ++ "outcome={s} roots={d} root_requested_bytes={d} " ++ "live_allocations={d} live_bytes={d} " ++ "high_water_live_bytes={d} " ++ "causal_operations={d} logical_operations={d} " ++ "backing_operations={d} physical_operations={d} " ++ "backing_requested_bytes={d} physical_requested_bytes={d} " ++ "max_depth={d} captured_frames={d} displayed_frames={d}\n", .{ summary.key.stack_id, summary.key.layer.tag(), @tagName(summary.key.producer), summary.key.kind.tag(), outcomeTag(summary.key.succeeded), summary.counters.roots, summary.counters.root_requested_bytes, summary.counters.lifetime.live_allocations, summary.counters.lifetime.live_bytes, summary.counters.lifetime.high_water_live_bytes, summary.counters.metrics.operations, summary.counters.metrics.logical_operations, summary.counters.metrics.backing_operations, summary.counters.metrics.physical_operations, summary.counters.metrics.backing_requested_bytes, summary.counters.metrics.physical_requested_bytes, summary.counters.metrics.max_depth, summary.definition.call_addresses.len, displayed, }, ); try writeTextFrames( writer, summary.definition, displayed, maybe_symbols, ); }}fn writeCoverage( writer: *std.Io.Writer, coverage: coverage_mod.Manifest,) !void { try writer.print( "coverage child_allocator_fast_paths={s} sys_memory_operations={s} " ++ "direct_os_memory_operations={s} unowned_allocator_producers={s} " ++ "foreign_allocations={s} observer_control={s} " ++ "observer_control_operations={d} zero_length_operations={s} " ++ "predispatch_failures={s}\n", .{ coverage.child_allocator_fast_paths.tag(), coverage.sys_memory_operations.tag(), coverage.direct_os_memory_operations.tag(), coverage.unowned_allocator_producers.tag(), coverage.foreign_allocations.tag(), coverage.observer_control.tag(), coverage.observer_control_operations, coverage.zero_length_operations.tag(), coverage.predispatch_failures.tag(), }, );}fn writeTextFrames( writer: *std.Io.Writer, definition: analyze_mod.Definition, displayed: usize, maybe_symbols: ?symbolize_mod.Symbols,) !void { for (definition.call_addresses[0..displayed], 0..) |address, frame_index| { const resolved = if (maybe_symbols) |symbols| symbols.find(address) else &.{}; try writer.print( " frame={d} kind={s} call_address=0x{x}", .{ frame_index, if (frame_index == 0) "root_site" else "physical", address, }, ); if (resolved.len != 0) { try writer.writeAll(" function="); try pretty_json.writeString(writer, resolved[0].function); try writer.writeAll(" location="); try pretty_json.writeString(writer, resolved[0].location); } try writer.writeByte('\n'); for (resolved[1..], 1..) |inline_frame, inline_index| { try writer.print(" inline={d} function=", .{inline_index}); try pretty_json.writeString(writer, inline_frame.function); try writer.writeAll(" location="); try pretty_json.writeString(writer, inline_frame.location); try writer.writeByte('\n'); } }}fn writeTextAddress( writer: *std.Io.Writer, prefix: []const u8, address: u64, maybe_symbols: ?symbolize_mod.Symbols,) !void { try writer.print("{s}_address=0x{x}", .{ prefix, address }); const resolved = if (maybe_symbols) |symbols| symbols.find(address) else &.{}; if (resolved.len == 0) return; try writer.print("{s}_function=", .{prefix}); try pretty_json.writeString(writer, resolved[0].function); try writer.print("{s}_location=", .{prefix}); try pretty_json.writeString(writer, resolved[0].location); if (resolved.len == 1) return; const owner = resolved[resolved.len - 1]; try writer.print("{s}_owner_function=", .{prefix}); try pretty_json.writeString(writer, owner.function); try writer.print("{s}_owner_location=", .{prefix}); try pretty_json.writeString(writer, owner.location);}fn writeTextWindow( writer: *std.Io.Writer, window: analyze_mod.Window, anchor: []const u8,) !void { try writer.writeAll(" window_scope="); if (window.scope) |scope| { try pretty_json.writeString(writer, scope); } else { try writer.writeAll("all"); } try writer.writeAll(" window_scope_match=subtree window_first_sequence="); try writeOptionalSequence(writer, window.first_sequence); try writer.writeAll(" window_last_sequence="); try writeOptionalSequence(writer, window.last_sequence); try writer.print(" window_sequence_bounds=inclusive window_anchor={s}", .{ anchor, });}fn writeOptionalSequence(writer: *std.Io.Writer, sequence: ?u64) !void { if (sequence) |value| { try writer.print("{d}", .{value}); } else { try writer.writeAll("all"); }}fn writeJsonWindow( object: pretty_json.Object, window: analyze_mod.Window, anchor: []const u8,) !void { try object.field("window_scope", window.scope); try object.field("window_scope_match", "subtree"); try object.field("window_first_sequence", window.first_sequence); try object.field("window_last_sequence", window.last_sequence); try object.field("window_sequence_bounds", "inclusive"); try object.field("window_anchor", anchor);}fn writeCounterFields(object: pretty_json.Object, counters: anytype) !void { try object.field("roots", counters.roots); try object.field("root_requested_bytes", counters.root_requested_bytes); try object.field("live_allocations", counters.lifetime.live_allocations); try object.field("live_bytes", counters.lifetime.live_bytes); try object.field("high_water_live_bytes", counters.lifetime.high_water_live_bytes); try object.field("causal_operations", counters.metrics.operations); try object.field("logical_operations", counters.metrics.logical_operations); try object.field("backing_operations", counters.metrics.backing_operations); try object.field("physical_operations", counters.metrics.physical_operations); try object.field("backing_requested_bytes", counters.metrics.backing_requested_bytes); try object.field("physical_requested_bytes", counters.metrics.physical_requested_bytes); try object.field("max_depth", counters.metrics.max_depth);}fn writeJsonl( writer: *std.Io.Writer, analyzer: *const Analyzer, totals: Totals, selection: analyze_mod.Selection, sort: Sort, window: analyze_mod.Window, display: Display, sources: []const Source, summaries: []const Summary, maybe_symbols: ?symbolize_mod.Symbols, frame_limit: usize, digest: identity_mod.Digest,) !void { const coverage = analyzer.stack.coverage.?; var summary_stream = pretty_json.Writer.init(writer, .minified); const header = try summary_stream.object(); try header.field("kind", "causal_root_summary"); try header.field("status", coverage.statusTag()); try header.field("universe", coverage.universe.tag()); try header.field("selection", selectionTag(selection)); try header.field("sort", sortTag(sort)); try header.field("roots", totals.roots); try header.field("successful", totals.successful); try header.field("failed", totals.failed); try header.field("root_requested_bytes", totals.root_requested_bytes); try header.field("live_allocations", totals.lifetime.live_allocations); try header.field("live_bytes", totals.lifetime.live_bytes); try header.field("high_water_live_bytes", totals.lifetime.high_water_live_bytes); try header.field("causal_operations", totals.metrics.operations); try header.field("logical_operations", totals.metrics.logical_operations); try header.field("backing_operations", totals.metrics.backing_operations); try header.field("physical_operations", totals.metrics.physical_operations); try header.field("backing_requested_bytes", totals.metrics.backing_requested_bytes); try header.field("physical_requested_bytes", totals.metrics.physical_requested_bytes); try header.field("max_depth", totals.metrics.max_depth); try header.field("source_groups", display.source_groups); try header.field("displayed_sources", display.displayed_sources); try header.field("root_stacks", display.root_stacks); try header.field("displayed_stacks", display.displayed_stacks); try header.hexString("binary_sha256", &digest); try writeJsonWindow(header, window, "root_operation"); try header.field("child_allocator_fast_paths", coverage.child_allocator_fast_paths.tag()); try header.field("sys_memory_operations", coverage.sys_memory_operations.tag()); try header.field("direct_os_memory_operations", coverage.direct_os_memory_operations.tag()); try header.field("unowned_allocator_producers", coverage.unowned_allocator_producers.tag()); try header.field("foreign_allocations", coverage.foreign_allocations.tag()); try header.field("observer_control", coverage.observer_control.tag()); try header.field("observer_control_operations", coverage.observer_control_operations); try header.field("zero_length_operations", coverage.zero_length_operations.tag()); try header.field("predispatch_failures", coverage.predispatch_failures.tag()); try header.endLine(); for (sources) |source| { var stream = pretty_json.Writer.init(writer, .minified); const object = try stream.object(); try object.field("kind", "causal_root_source"); try object.field("layer", source.key.layer.tag()); try object.field("producer", @tagName(source.key.producer)); try object.field("operation", source.key.kind.tag()); try object.field("succeeded", source.key.succeeded); try writeCounterFields(object, source.counters); try object.field("unique_stacks", source.unique_stacks); try object.field("site_address", source.key.site); const caller_address: ?u64 = if (source.key.caller == 0) null else source.key.caller; try object.field("caller_address", caller_address); try writeJsonSymbol( object, "site", source.key.site, maybe_symbols, ); if (source.key.caller != 0) { try writeJsonSymbol( object, "caller", source.key.caller, maybe_symbols, ); } try object.endLine(); } for (summaries) |summary| { const displayed = @min( frame_limit, summary.definition.call_addresses.len, ); var stream = pretty_json.Writer.init(writer, .minified); const object = try stream.object(); try object.field("kind", "causal_root_stack"); try object.field("stack_id", summary.key.stack_id); try object.field("layer", summary.key.layer.tag()); try object.field("producer", @tagName(summary.key.producer)); try object.field("operation", summary.key.kind.tag()); try object.field("succeeded", summary.key.succeeded); try writeCounterFields(object, summary.counters); try object.field("captured_frames", summary.definition.call_addresses.len); try object.field("displayed_frames", displayed); try object.endLine(); for ( summary.definition.call_addresses[0..displayed], 0.., ) |address, frame_index| { const resolved = if (maybe_symbols) |symbols| symbols.find(address) else &.{}; if (resolved.len == 0) { try writeJsonFrame( writer, summary.key.stack_id, frame_index, address, 0, "", "", ); continue; } for (resolved, 0..) |inline_frame, inline_index| { try writeJsonFrame( writer, summary.key.stack_id, frame_index, address, inline_index, inline_frame.function, inline_frame.location, ); } } }}fn writeJsonSymbol( object: pretty_json.Object, prefix: []const u8, address: u64, maybe_symbols: ?symbolize_mod.Symbols,) !void { const resolved = if (maybe_symbols) |symbols| symbols.find(address) else &.{}; if (resolved.len == 0) return; try object.fieldParts(&.{ prefix, "_function" }, resolved[0].function); try object.fieldParts(&.{ prefix, "_location" }, resolved[0].location); if (resolved.len == 1) return; const owner = resolved[resolved.len - 1]; try object.fieldParts(&.{ prefix, "_owner_function" }, owner.function); try object.fieldParts(&.{ prefix, "_owner_location" }, owner.location);}fn writeJsonFrame( writer: *std.Io.Writer, stack_id: u32, frame_index: usize, address: u64, inline_index: usize, function: []const u8, location: []const u8,) !void { var stream = pretty_json.Writer.init(writer, .minified); const object = try stream.object(); try object.field("kind", "causal_root_stack_frame"); try object.field("stack_id", stack_id); try object.field("frame", frame_index); try object.field("frame_kind", if (frame_index == 0) "root_site" else "physical"); try object.field("call_address", address); try object.field("inline", inline_index); try object.field("function", function); try object.field("location", location); try object.endLine();}fn fixture(analyzer: *Analyzer, dangling: bool) !void { const digest: identity_mod.Digest = @splat(0xaa); var input = std.Io.Writer.Allocating.init(std.testing.allocator); defer input.deinit(); try identity_mod.writeMetadata(&input.writer, digest); try coverage_mod.writeMetadata( &input.writer, coverage_mod.ownedProducerManifest(), ); var addresses = [_]usize{ 1, 2 }; try capture_mod.writeDefinition(&input.writer, 1, .{ .addresses = &addresses, .truncated = false, .unwind_failed = false, .missing_return_address = false, .collision_next = 0, }); try (event_mod.Event{ .seq = 1, .kind = .trace_start, }).writeJsonLine(&input.writer, null, null); try (event_mod.Event{ .seq = 2, .kind = .map, .len = 128, .stack_id = 1, .layer = .physical_page, .operation_id = 3, .parent_operation_id = 2, .producer = .sys_memory, }).writeJsonLine(&input.writer, null, null); try (event_mod.Event{ .seq = 3, .kind = .alloc, .len = 64, .stack_id = 1, .operation_id = 2, .parent_operation_id = 1, }).writeJsonLine(&input.writer, null, null); if (!dangling) { try (event_mod.Event{ .seq = 4, .kind = .alloc, .allocation_id = 1, .address = 4096, .len = 16, .stack_id = 1, .layer = .logical_allocator, .operation_id = 1, .producer = .arena, }).writeJsonLine(&input.writer, null, null); try (event_mod.Event{ .seq = 5, .kind = .alloc, .allocation_id = 2, .address = 8192, .len = 32, .stack_id = 1, .layer = .logical_allocator, .operation_id = 4, .producer = .arena, }).writeJsonLine(&input.writer, null, null); } try (event_mod.Event{ .seq = if (dangling) 4 else 6, .kind = .trace_stop, }).writeJsonLine(&input.writer, null, null); var lines = std.mem.splitScalar(u8, input.written(), '\n'); while (lines.next()) |line| try analyzer.ingestJsonLine(line);}test "causal roots collapse nested allocator layers" { var analyzer = Analyzer.init(std.testing.allocator, .{}); defer analyzer.deinit(); try fixture(&analyzer, false); try analyzer.finish(); const totals = try analyzer.totals(.allocations); try std.testing.expectEqual(@as(u64, 2), totals.roots); try std.testing.expectEqual(@as(u128, 48), totals.root_requested_bytes); try std.testing.expectEqual(@as(u64, 4), totals.metrics.operations); try std.testing.expectEqual(@as(u64, 2), totals.metrics.logical_operations); try std.testing.expectEqual(@as(u64, 1), totals.metrics.backing_operations); try std.testing.expectEqual(@as(u64, 1), totals.metrics.physical_operations); try std.testing.expectEqual(@as(u32, 2), totals.metrics.max_depth); try std.testing.expectEqual(@as(u64, 2), totals.lifetime.live_allocations); try std.testing.expectEqual(@as(u128, 48), totals.lifetime.live_bytes); try std.testing.expectEqual( @as(u128, 48), totals.lifetime.high_water_live_bytes, );}test "causal roots reject a missing parent operation" { var analyzer = Analyzer.init(std.testing.allocator, .{}); defer analyzer.deinit(); try fixture(&analyzer, true); try std.testing.expectError( error.DanglingParentOperation, analyzer.finish(), );}test "causal roots identify each missing parent through its observed child site" { var analyzer = Analyzer.init(std.testing.allocator, .{}); defer analyzer.deinit(); try fixture(&analyzer, true); try analyzer.stack.validate(); var dangling = try analyzer.collectDangling(); defer dangling.deinit(std.testing.allocator); try std.testing.expectEqual(@as(usize, 1), dangling.items.len); try std.testing.expectEqual(@as(u64, 1), dangling.items[0].operation_id); try std.testing.expectEqual(@as(u64, 2), dangling.items[0].child.operation_id); try std.testing.expectEqual(@as(u64, 1), dangling.items[0].site_address); var output = std.Io.Writer.Allocating.init(std.testing.allocator); defer output.deinit(); const digest: identity_mod.Digest = @splat(0xaa); try writeDanglingText( &output.writer, &analyzer, dangling.items, dangling.items.len, null, digest, ); try std.testing.expect(std.mem.indexOf( u8, output.written(), "missing_parent_operation_id=1 observed_child_operation_id=2", ) != null); try std.testing.expect(std.mem.indexOf( u8, output.written(), "observed_child_site_address=0x1", ) != null);}test "causal root window anchors a complete tree at its root operation" { var analyzer = Analyzer.init(std.testing.allocator, .{ .scope = "root/phase", .first_sequence = 4, .last_sequence = 4, }); defer analyzer.deinit(); try filteredRootFixture(&analyzer); try analyzer.finish(); const totals = try analyzer.totals(.allocations); try std.testing.expectEqual(@as(u64, 1), totals.roots); try std.testing.expectEqual(@as(u128, 16), totals.root_requested_bytes); try std.testing.expectEqual(@as(u64, 2), totals.metrics.operations); try std.testing.expectEqual(@as(u64, 1), totals.metrics.logical_operations); try std.testing.expectEqual(@as(u64, 1), totals.metrics.backing_operations); try std.testing.expectEqual(@as(u128, 64), totals.metrics.backing_requested_bytes);}fn filteredRootFixture(analyzer: *Analyzer) !void { const digest: identity_mod.Digest = @splat(0xaa); var input = std.Io.Writer.Allocating.init(std.testing.allocator); defer input.deinit(); try identity_mod.writeMetadata(&input.writer, digest); try coverage_mod.writeMetadata( &input.writer, coverage_mod.ownedProducerManifest(), ); var addresses = [_]usize{ 1, 2 }; try capture_mod.writeDefinition(&input.writer, 1, .{ .addresses = &addresses, .truncated = false, .unwind_failed = false, .missing_return_address = false, .collision_next = 0, }); const lines = [_][]const u8{ "{\"v\":3,\"seq\":1,\"kind\":\"trace.start\"}", "{\"v\":3,\"seq\":2,\"kind\":\"scope.enter\",\"scope_id\":1," ++ "\"scope\":\"root/phase\"}", "{\"v\":3,\"seq\":3,\"kind\":\"alloc\",\"scope_id\":1,\"len\":64," ++ "\"stack_id\":1,\"operation_id\":2,\"parent_operation_id\":1}", "{\"v\":3,\"seq\":4,\"kind\":\"alloc\",\"scope_id\":1," ++ "\"allocation_id\":1,\"address\":4096,\"len\":16,\"stack_id\":1," ++ "\"layer\":\"logical_allocator\",\"operation_id\":1," ++ "\"producer\":\"arena\"}", "{\"v\":3,\"seq\":5,\"kind\":\"scope.exit\",\"scope_id\":1}", "{\"v\":3,\"seq\":6,\"kind\":\"scope.enter\",\"scope_id\":2," ++ "\"scope\":\"root/other\"}", "{\"v\":3,\"seq\":7,\"kind\":\"alloc\",\"scope_id\":2,\"len\":128," ++ "\"stack_id\":1,\"operation_id\":4,\"parent_operation_id\":3}", "{\"v\":3,\"seq\":8,\"kind\":\"alloc\",\"scope_id\":2," ++ "\"allocation_id\":2,\"address\":8192,\"len\":32,\"stack_id\":1," ++ "\"layer\":\"logical_allocator\",\"operation_id\":3," ++ "\"producer\":\"arena\"}", "{\"v\":3,\"seq\":9,\"kind\":\"free\",\"scope_id\":2,\"address\":99," ++ "\"len\":8,\"old_len\":8,\"stack_id\":1,\"operation_id\":6," ++ "\"parent_operation_id\":5}", "{\"v\":3,\"seq\":10,\"kind\":\"release\",\"scope_id\":2," ++ "\"address\":99,\"len\":8,\"old_len\":8,\"stack_id\":1," ++ "\"operation_id\":5}", "{\"v\":3,\"seq\":11,\"kind\":\"lifecycle\",\"scope_id\":2," ++ "\"return_address\":2,\"operation_id\":5}", "{\"v\":3,\"seq\":12,\"kind\":\"scope.exit\",\"scope_id\":2}", "{\"v\":3,\"seq\":13,\"kind\":\"trace.stop\"}", }; var metadata = std.mem.splitScalar(u8, input.written(), '\n'); while (metadata.next()) |line| try analyzer.ingestJsonLine(line); for (lines) |line| try analyzer.ingestJsonLine(line);}test "causal root source ordering is irreflexive" { const source = Source{ .key = .{ .kind = .alloc, .succeeded = true, .layer = .logical_allocator, .producer = .arena, .site = 1, .caller = 2, }, .counters = .{}, .unique_stacks = 1, }; try std.testing.expect(!sourceGreaterThan(.high_water, source, source));}test "causal root lifetime tracks resize and release" { var lifetime = Lifetime{}; try lifetime.allocate(16); try lifetime.resize(16, 40); try lifetime.resize(40, 24); try lifetime.free(24); try std.testing.expectEqual(@as(u64, 0), lifetime.live_allocations); try std.testing.expectEqual(@as(u128, 0), lifetime.live_bytes); try std.testing.expectEqual( @as(u128, 40), lifetime.high_water_live_bytes, );}test "causal operation ledger validates contiguous multi-event groups" { var ledger = OperationLedger{}; defer ledger.deinit(std.testing.allocator); try ledger.record(std.testing.allocator, 2, 1); try ledger.record(std.testing.allocator, 2, 1); try ledger.record(std.testing.allocator, 1, 0); try ledger.validate(); try std.testing.expectEqual(@as(u64, 2), ledger.unique); try std.testing.expectError( error.NonContiguousCausalOperationGroup, ledger.record(std.testing.allocator, 2, 1), );}test "causal operation ledger rejects a parent conflict within one group" { var ledger = OperationLedger{}; defer ledger.deinit(std.testing.allocator); try ledger.record(std.testing.allocator, 2, 1); try std.testing.expectError( error.CausalOperationParentConflict, ledger.record(std.testing.allocator, 2, 0), );}Complete call list for stack.roots.writeFromPath
11 direct calls.
lib.memtrace.src.stack.roots.Analyzer.collect[method] — private source atlib/memtrace/src/stack/roots.zig:707in nearest public ownertiny.memtrace.stack.rootslib.memtrace.src.stack.roots.Analyzer.deinit[method] — private source atlib/memtrace/src/stack/roots.zig:381in nearest public ownertiny.memtrace.stack.rootslib.memtrace.src.stack.roots.Analyzer.finishCausal[method] — private source atlib/memtrace/src/stack/roots.zig:450in nearest public ownertiny.memtrace.stack.rootslib.memtrace.src.stack.roots.Analyzer.init[function] — private source atlib/memtrace/src/stack/roots.zig:374in nearest public ownertiny.memtrace.stack.rootslib.memtrace.src.stack.roots.Analyzer.totals[method] — private source atlib/memtrace/src/stack/roots.zig:729in nearest public ownertiny.memtrace.stack.rootslib.memtrace.src.stack.roots.collectAddresses[function] — private source atlib/memtrace/src/stack/roots.zig:1113in nearest public ownertiny.memtrace.stack.rootslib.memtrace.src.stack.roots.collectSources[function] — private source atlib/memtrace/src/stack/roots.zig:1069in nearest public ownertiny.memtrace.stack.rootslib.memtrace.src.stack.roots.ingestPath[function] — private source atlib/memtrace/src/stack/roots.zig:890in nearest public ownertiny.memtrace.stack.rootslib.memtrace.src.stack.roots.writeDanglingReport[function] — private source atlib/memtrace/src/stack/roots.zig:905in nearest public ownertiny.memtrace.stack.rootslib.memtrace.src.stack.roots.writeJsonl[function] — private source atlib/memtrace/src/stack/roots.zig:1597in nearest public ownertiny.memtrace.stack.rootslib.memtrace.src.stack.roots.writeText[function] — private source atlib/memtrace/src/stack/roots.zig:1309in nearest public ownertiny.memtrace.stack.roots
Audit
| Definitions | 6 |
|---|---|
| Public names | 6 |
| Members | 17 |
| Version | 26.7.0 |
| Revision | daab053ee433 |