lib/memtrace/src/analysis.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

   1 const std = @import("std");
   2 const observe = @import("alloc_observe");
   3 const pretty_json = @import("pretty").json;
   4 const sys = @import("sys");
   5 const allocations = @import("allocations.zig");
   6 const coverage_mod = @import("coverage.zig");
   7 const event_mod = @import("event.zig");
   8 const mappings = @import("mappings.zig");
   9 const stack_mod = @import("stack/root.zig");
  10 const tracer_mod = @import("tracer.zig");
  11 
  12 const Allocator = std.mem.Allocator;
  13 
  14 pub const Sort = enum {
  15     retained,
  16     traffic,
  17     lifetime,
  18 
  19     pub fn parse(value: []const u8) ?Sort {
  20         if (std.mem.eql(u8, value, "retained")) return .retained;
  21         if (std.mem.eql(u8, value, "traffic")) return .traffic;
  22         if (std.mem.eql(u8, value, "lifetime")) return .lifetime;
  23         return null;
  24     }
  25 
  26     pub fn name(self: Sort) []const u8 {
  27         return @tagName(self);
  28     }
  29 };
  30 
  31 pub const SummaryOptions = struct {
  32     top: usize = 24,
  33     min_bytes: usize = 0,
  34     include_zero_live: bool = false,
  35     include_sites: bool = false,
  36     site_detail_return_address: ?u64 = null,
  37     site_symbol_binary: ?[]const u8 = null,
  38     layer: event_mod.LayerFilter = .backing,
  39     sort: Sort = .retained,
  40 };
  41 
  42 pub const SnapshotOptions = struct {
  43     top: usize = 24,
  44     min_bytes: usize = 0,
  45     include_zero_live: bool = false,
  46     site_symbol_binary: ?[]const u8 = null,
  47     layer: event_mod.LayerFilter = .backing,
  48 };
  49 
  50 pub const Summary = struct {
  51     events: u64 = 0,
  52     allocations: u64 = 0,
  53     frees: u64 = 0,
  54     resizes: u64 = 0,
  55     remaps: u64 = 0,
  56     live_allocations: u64 = 0,
  57     allocated_bytes: u64 = 0,
  58     freed_bytes: u64 = 0,
  59     live_bytes: u64 = 0,
  60     high_water_live_bytes: u64 = 0,
  61     retained_bytes: u64 = 0,
  62     high_water_retained_bytes: u64 = 0,
  63     completed_lifetimes: u64 = 0,
  64     lifetime_total_events: u64 = 0,
  65     lifetime_mean_events: u64 = 0,
  66     lifetime_max_events: u64 = 0,
  67     lifetime_total_byte_events: u64 = 0,
  68     lifetime_mean_byte_events: u64 = 0,
  69     lifetime_max_byte_events: u64 = 0,
  70     failed_allocations: u64 = 0,
  71     failed_resizes: u64 = 0,
  72     failed_remaps: u64 = 0,
  73     unmatched_frees: u64 = 0,
  74     unmatched_resizes: u64 = 0,
  75 };
  76 
  77 pub const Scope = struct {
  78     scope: []const u8,
  79     retained_bytes: u64,
  80     high_water_retained_bytes: u64,
  81     live_bytes: u64,
  82     high_water_live_bytes: u64,
  83     allocated_bytes: u64,
  84     freed_bytes: u64,
  85     allocations: u64,
  86     frees: u64,
  87     live_allocations: u64,
  88     completed_lifetimes: u64,
  89     lifetime_total_events: u64,
  90     lifetime_mean_events: u64,
  91     lifetime_max_events: u64,
  92     lifetime_total_byte_events: u64 = 0,
  93     lifetime_mean_byte_events: u64 = 0,
  94     lifetime_max_byte_events: u64 = 0,
  95 };
  96 
  97 pub const Source = struct {
  98     return_address: u64,
  99     function: ?[]const u8,
 100     location: ?[]const u8,
 101     retained_bytes: u64,
 102     high_water_retained_bytes: u64,
 103     live_bytes: u64,
 104     high_water_live_bytes: u64,
 105     allocated_bytes: u64,
 106     freed_bytes: u64,
 107     allocations: u64,
 108     frees: u64,
 109     live_allocations: u64,
 110     completed_lifetimes: u64,
 111     lifetime_total_events: u64,
 112     lifetime_mean_events: u64,
 113     lifetime_max_events: u64,
 114     lifetime_total_byte_events: u64 = 0,
 115     lifetime_mean_byte_events: u64 = 0,
 116     lifetime_max_byte_events: u64 = 0,
 117 };
 118 
 119 pub const Ranking = struct {
 120     scopes: []Scope = &.{},
 121     sources: []Source = &.{},
 122 };
 123 
 124 pub const Rankings = struct {
 125     retained: Ranking = .{},
 126     traffic: Ranking = .{},
 127     lifetime: Ranking = .{},
 128 
 129     pub fn get(self: Rankings, sort: Sort) Ranking {
 130         return switch (sort) {
 131             .retained => self.retained,
 132             .traffic => self.traffic,
 133             .lifetime => self.lifetime,
 134         };
 135     }
 136 
 137     fn getPtr(self: *Rankings, sort: Sort) *Ranking {
 138         return switch (sort) {
 139             .retained => &self.retained,
 140             .traffic => &self.traffic,
 141             .lifetime => &self.lifetime,
 142         };
 143     }
 144 };
 145 
 146 pub const Snapshot = struct {
 147     allocator: Allocator,
 148     integrity: CaptureIntegrity,
 149     summary: Summary,
 150     rankings: Rankings = .{},
 151     symbol_storage: ?[]u8 = null,
 152 
 153     pub fn deinit(self: *Snapshot) void {
 154         inline for (std.meta.tags(Sort)) |sort| {
 155             const ranking = self.rankings.getPtr(sort);
 156             for (ranking.scopes) |scope| self.allocator.free(scope.scope);
 157             if (ranking.scopes.len != 0) self.allocator.free(ranking.scopes);
 158             if (ranking.sources.len != 0) self.allocator.free(ranking.sources);
 159         }
 160         if (self.symbol_storage) |storage| self.allocator.free(storage);
 161         self.* = undefined;
 162     }
 163 };
 164 
 165 const Retention = enum {
 166     releases_freed_memory,
 167     retains_freed_memory,
 168 };
 169 
 170 const Counters = struct {
 171     allocations: u64 = 0,
 172     frees: u64 = 0,
 173     resizes: u64 = 0,
 174     remaps: u64 = 0,
 175     failed_allocations: u64 = 0,
 176     failed_resizes: u64 = 0,
 177     failed_remaps: u64 = 0,
 178     unmatched_frees: u64 = 0,
 179     unmatched_resizes: u64 = 0,
 180     unmatched_remaps: u64 = 0,
 181     bulk_invalidated_requests: u64 = 0,
 182     bulk_invalidated_bytes: usize = 0,
 183     untracked_requests: u64 = 0,
 184     untracked_request_bytes: usize = 0,
 185     lifecycle_events: u64 = 0,
 186     live_allocations: usize = 0,
 187     allocated_bytes: usize = 0,
 188     freed_bytes: usize = 0,
 189     live_bytes: usize = 0,
 190     high_water_live_bytes: usize = 0,
 191     retained_bytes: usize = 0,
 192     high_water_retained_bytes: usize = 0,
 193     completed_lifetimes: u64 = 0,
 194     lifetime_total_events: u64 = 0,
 195     lifetime_max_events: u64 = 0,
 196     lifetime_total_byte_events: u64 = 0,
 197     lifetime_max_byte_events: u64 = 0,
 198 };
 199 
 200 const PhysicalCounters = struct {
 201     maps: u64 = 0,
 202     unmaps: u64 = 0,
 203     protects: u64 = 0,
 204     discards: u64 = 0,
 205     decommits: u64 = 0,
 206     advises: u64 = 0,
 207     failed_maps: u64 = 0,
 208     failed_unmaps: u64 = 0,
 209     failed_protects: u64 = 0,
 210     failed_discards: u64 = 0,
 211     failed_decommits: u64 = 0,
 212     failed_advises: u64 = 0,
 213     mapped_bytes: usize = 0,
 214     unmapped_bytes: usize = 0,
 215     displaced_bytes: usize = 0,
 216     untracked_unmap_bytes: usize = 0,
 217     live_mapped_bytes: usize = 0,
 218     high_water_mapped_bytes: usize = 0,
 219 };
 220 
 221 const IntegrityCounters = struct {
 222     sequenced_events: u64 = 0,
 223     unsequenced_events: u64 = 0,
 224     sequence_gaps: u64 = 0,
 225     missing_sequence_events: u64 = 0,
 226     sequence_regressions: u64 = 0,
 227     start_events: u64 = 0,
 228     stop_events: u64 = 0,
 229     unmatched_scope_exits: u64 = 0,
 230     active_scope_events: u64 = 0,
 231 };
 232 
 233 pub const capture_integrity_method = "memtrace_event_sequence_and_lifecycle_v1";
 234 
 235 pub const CaptureIntegrity = struct {
 236     status: []const u8,
 237     action: []const u8,
 238     message: ?[]const u8,
 239     event_count: u64,
 240     sequenced_event_count: u64,
 241     unsequenced_event_count: u64,
 242     first_sequence: ?u64,
 243     last_sequence: ?u64,
 244     sequence_gap_count: u64,
 245     missing_sequence_event_count: u64,
 246     sequence_regression_count: u64,
 247     start_event_count: u64,
 248     stop_event_count: u64,
 249     start_sequence: ?u64,
 250     stop_sequence: ?u64,
 251     unbalanced_event_count: u64,
 252 };
 253 
 254 const ScopeCounters = struct {
 255     allocations: u64 = 0,
 256     frees: u64 = 0,
 257     allocated_bytes: usize = 0,
 258     freed_bytes: usize = 0,
 259     bulk_invalidated_requests: u64 = 0,
 260     bulk_invalidated_bytes: usize = 0,
 261     untracked_requests: u64 = 0,
 262     untracked_request_bytes: usize = 0,
 263     live_allocations: usize = 0,
 264     live_bytes: usize = 0,
 265     high_water_live_bytes: usize = 0,
 266     retained_bytes: usize = 0,
 267     high_water_retained_bytes: usize = 0,
 268     completed_lifetimes: u64 = 0,
 269     lifetime_total_events: u64 = 0,
 270     lifetime_max_events: u64 = 0,
 271     lifetime_total_byte_events: u64 = 0,
 272     lifetime_max_byte_events: u64 = 0,
 273 };
 274 
 275 const AllocatorState = struct {
 276     retention: Retention = .releases_freed_memory,
 277     layer: event_mod.Layer = .backing_boundary,
 278     lifecycle_instrumented: bool = false,
 279     prefix_complete: bool = true,
 280 };
 281 
 282 const LifecycleCoverage = struct {
 283     instrumented: u64 = 0,
 284     uninstrumented: u64 = 0,
 285     not_required: u64 = 0,
 286     prefix_complete: u64 = 0,
 287     prefix_partial: u64 = 0,
 288 
 289     fn status(self: LifecycleCoverage) []const u8 {
 290         if (self.instrumented == 0 and
 291             self.uninstrumented == 0 and
 292             self.not_required == 0)
 293         {
 294             return "none";
 295         }
 296         if (self.uninstrumented == 0 and self.prefix_partial == 0) {
 297             return "complete";
 298         }
 299         return "partial";
 300     }
 301 };
 302 
 303 const AllocationRecord = struct {
 304     allocator_id: u32,
 305     len: usize,
 306     scope: []const u8,
 307     return_address: u64,
 308     allocation_event: u64,
 309     segment_event: u64,
 310     byte_events: u64,
 311 };
 312 
 313 const ScopeSummary = struct {
 314     path: []const u8,
 315     counters: ScopeCounters,
 316 };
 317 
 318 const ParsedEvent = event_mod.ReplayEvent;
 319 
 320 const SiteCounters = struct {
 321     allocations: u64 = 0,
 322     frees: u64 = 0,
 323     allocated_bytes: usize = 0,
 324     freed_bytes: usize = 0,
 325     bulk_invalidated_requests: u64 = 0,
 326     bulk_invalidated_bytes: usize = 0,
 327     untracked_requests: u64 = 0,
 328     untracked_request_bytes: usize = 0,
 329     live_allocations: usize = 0,
 330     live_bytes: usize = 0,
 331     high_water_live_bytes: usize = 0,
 332     retained_bytes: usize = 0,
 333     high_water_retained_bytes: usize = 0,
 334     completed_lifetimes: u64 = 0,
 335     lifetime_total_events: u64 = 0,
 336     lifetime_max_events: u64 = 0,
 337     lifetime_total_byte_events: u64 = 0,
 338     lifetime_max_byte_events: u64 = 0,
 339 };
 340 
 341 const SiteSummary = struct {
 342     return_address: u64,
 343     counters: SiteCounters,
 344 };
 345 
 346 const SiteSizeSummary = struct {
 347     len: usize,
 348     counters: SiteCounters,
 349 };
 350 
 351 const SiteScopeSummary = struct {
 352     scope: []const u8,
 353     counters: SiteCounters,
 354 };
 355 
 356 pub const Analyzer = struct {
 357     allocator: Allocator,
 358     allocators: std.AutoHashMapUnmanaged(u32, AllocatorState) = .{},
 359     allocations: allocations.Map(AllocationRecord) = .{},
 360     scopes: std.StringHashMapUnmanaged(ScopeCounters) = .{},
 361     sites: std.AutoHashMapUnmanaged(u64, SiteCounters) = .{},
 362     site_detail_sizes: std.AutoHashMapUnmanaged(usize, SiteCounters) = .{},
 363     site_detail_scopes: std.StringHashMapUnmanaged(SiteCounters) = .{},
 364     site_detail_return_address: ?u64 = null,
 365     active_scopes: std.AutoHashMapUnmanaged(u32, u64) = .{},
 366     scope_paths: std.AutoHashMapUnmanaged(u32, []const u8) = .{},
 367     track_sites: bool = true,
 368     layer_filter: event_mod.LayerFilter = .backing,
 369     counters: Counters = .{},
 370     physical: PhysicalCounters = .{},
 371     mappings: mappings.Ledger = .{},
 372     integrity_counters: IntegrityCounters = .{},
 373     events: u64 = 0,
 374     first_sequence: ?u64 = null,
 375     last_sequence: ?u64 = null,
 376     start_sequence: ?u64 = null,
 377     stop_sequence: ?u64 = null,
 378 
 379     pub fn init(allocator: Allocator) Analyzer {
 380         return .{ .allocator = allocator };
 381     }
 382 
 383     pub fn initForLayer(
 384         allocator: Allocator,
 385         layer_filter: event_mod.LayerFilter,
 386     ) Analyzer {
 387         return .{
 388             .allocator = allocator,
 389             .layer_filter = layer_filter,
 390         };
 391     }
 392 
 393     pub fn deinit(self: *Analyzer) void {
 394         var iterator = self.scopes.iterator();
 395         while (iterator.next()) |entry| self.allocator.free(entry.key_ptr.*);
 396         self.scopes.deinit(self.allocator);
 397         self.sites.deinit(self.allocator);
 398         self.site_detail_sizes.deinit(self.allocator);
 399         self.site_detail_scopes.deinit(self.allocator);
 400         self.active_scopes.deinit(self.allocator);
 401         self.scope_paths.deinit(self.allocator);
 402         self.allocators.deinit(self.allocator);
 403         self.allocations.deinit(self.allocator);
 404         self.mappings.deinit(self.allocator);
 405         self.* = undefined;
 406     }
 407 
 408     pub fn ingestJsonlBytes(self: *Analyzer, bytes: []const u8) !void {
 409         var lines = std.mem.splitScalar(u8, bytes, '\n');
 410         while (lines.next()) |line| try self.ingestJsonLine(line);
 411     }
 412 
 413     pub fn ingestJsonLine(self: *Analyzer, line: []const u8) !void {
 414         const text = std.mem.trim(u8, line, " \t\r\n");
 415         if (text.len == 0) return;
 416         if (stack_mod.isMetadataLine(text) or coverage_mod.isMetadataLine(text)) return;
 417 
 418         if (event_mod.parseReplayFast(text)) |parsed| {
 419             try self.ingestParsedEvent(parsed);
 420             return;
 421         } else |err| switch (err) {
 422             error.UnsupportedFastEvent => {},
 423             else => return err,
 424         }
 425 
 426         try self.ingestGenericJsonLine(text);
 427     }
 428 
 429     fn ingestParsedEvent(self: *Analyzer, parsed: ParsedEvent) !void {
 430         const scope = try self.resolveScope(parsed);
 431         const key = switch (parsed.kind) {
 432             .remap => allocationKey(parsed.allocation_id, parsed.old_address),
 433             else => allocationKey(parsed.allocation_id, parsed.address),
 434         };
 435 
 436         self.events +|= 1;
 437         self.recordSequence(parsed.seq);
 438         switch (parsed.kind) {
 439             .trace_start => {
 440                 self.integrity_counters.start_events +|= 1;
 441                 if (self.start_sequence == null) self.start_sequence = parsed.seq;
 442             },
 443             .trace_stop => {
 444                 self.integrity_counters.stop_events +|= 1;
 445                 self.stop_sequence = parsed.seq;
 446             },
 447             .allocator => if (self.layer_filter.includes(parsed.layer)) {
 448                 try self.recordAllocator(
 449                     parsed.allocator_id,
 450                     parsed.retains_freed_memory,
 451                     parsed.layer,
 452                     parsed.lifecycle_instrumented,
 453                     parsed.observation_prefix_complete,
 454                 );
 455             },
 456             .scope_enter => try self.recordScopeEnter(parsed.scope_id, scope),
 457             .scope_exit => self.recordScopeExit(parsed.scope_id),
 458             .alloc => if (self.layer_filter.includes(parsed.layer)) {
 459                 if (parsed.succeeded) {
 460                     if (parsed.tracked) {
 461                         try self.recordAllocation(
 462                             parsed.allocator_id,
 463                             key,
 464                             scope,
 465                             parsed.return_address,
 466                             parsed.len,
 467                         );
 468                     } else {
 469                         try self.recordUntrackedAllocation(
 470                             scope,
 471                             parsed.return_address,
 472                             parsed.len,
 473                         );
 474                     }
 475                 } else {
 476                     self.counters.failed_allocations +|= 1;
 477                 }
 478             },
 479             .free => if (self.layer_filter.includes(parsed.layer)) {
 480                 if (parsed.tracked) {
 481                     try self.recordFree(
 482                         key,
 483                         parsed.allocator_id,
 484                         scope,
 485                         parsed.len,
 486                     );
 487                 } else {
 488                     self.counters.unmatched_frees +|= 1;
 489                 }
 490             },
 491             .release => if (self.layer_filter.includes(parsed.layer)) {
 492                 try self.recordRelease(key);
 493             },
 494             .resize => if (self.layer_filter.includes(parsed.layer)) {
 495                 if (parsed.succeeded) {
 496                     if (parsed.tracked) {
 497                         try self.recordResize(
 498                             key,
 499                             parsed.allocator_id,
 500                             scope,
 501                             parsed.old_len,
 502                             parsed.len,
 503                         );
 504                     } else {
 505                         self.counters.unmatched_resizes +|= 1;
 506                     }
 507                 } else {
 508                     self.counters.failed_resizes +|= 1;
 509                     if (!parsed.tracked) {
 510                         self.counters.unmatched_resizes +|= 1;
 511                     }
 512                 }
 513             },
 514             .remap => if (self.layer_filter.includes(parsed.layer)) {
 515                 if (parsed.succeeded) {
 516                     if (parsed.tracked) {
 517                         try self.recordRemap(
 518                             key,
 519                             allocationKey(
 520                                 parsed.allocation_id,
 521                                 parsed.address,
 522                             ),
 523                             parsed.allocator_id,
 524                             scope,
 525                             parsed.old_len,
 526                             parsed.len,
 527                         );
 528                     } else if (parsed.allocation_id != 0) {
 529                         try self.recordLostTrackingRemap(
 530                             key,
 531                             parsed.len,
 532                         );
 533                     } else {
 534                         self.counters.unmatched_remaps +|= 1;
 535                     }
 536                 } else {
 537                     self.counters.failed_remaps +|= 1;
 538                     if (!parsed.tracked) {
 539                         self.counters.unmatched_remaps +|= 1;
 540                     }
 541                 }
 542             },
 543             .lifecycle => if (self.layer_filter.includes(parsed.layer)) {
 544                 self.counters.lifecycle_events +|= 1;
 545                 if (self.allocators.getPtr(parsed.allocator_id)) |allocator| {
 546                     allocator.prefix_complete = true;
 547                 }
 548             },
 549             .map => if (self.layer_filter.includes(parsed.layer)) {
 550                 try self.recordPhysicalMap(parsed);
 551             },
 552             .unmap => if (self.layer_filter.includes(parsed.layer)) {
 553                 try self.recordPhysicalUnmap(parsed);
 554             },
 555             .protect => if (self.layer_filter.includes(parsed.layer)) {
 556                 self.recordPhysicalEffect(parsed.succeeded, .protect);
 557             },
 558             .discard => if (self.layer_filter.includes(parsed.layer)) {
 559                 self.recordPhysicalEffect(parsed.succeeded, .discard);
 560             },
 561             .decommit => if (self.layer_filter.includes(parsed.layer)) {
 562                 self.recordPhysicalEffect(parsed.succeeded, .decommit);
 563             },
 564             .advise => if (self.layer_filter.includes(parsed.layer)) {
 565                 self.recordPhysicalEffect(parsed.succeeded, .advise);
 566             },
 567             .snapshot, .census => {},
 568         }
 569     }
 570 
 571     fn recordPhysicalMap(self: *Analyzer, parsed: ParsedEvent) !void {
 572         if (!parsed.succeeded) {
 573             self.physical.failed_maps +|= 1;
 574             return;
 575         }
 576         const address = std.math.cast(usize, parsed.address) orelse
 577             return error.InvalidMappingRange;
 578         const displaced = try self.mappings.map(
 579             self.allocator,
 580             address,
 581             parsed.len,
 582         );
 583         self.physical.maps +|= 1;
 584         self.physical.mapped_bytes +|= parsed.len;
 585         self.physical.displaced_bytes +|= displaced;
 586         self.updatePhysicalLiveBytes();
 587     }
 588 
 589     fn recordPhysicalUnmap(self: *Analyzer, parsed: ParsedEvent) !void {
 590         if (!parsed.succeeded) {
 591             self.physical.failed_unmaps +|= 1;
 592             return;
 593         }
 594         const address = std.math.cast(usize, parsed.address) orelse
 595             return error.InvalidMappingRange;
 596         const removal = try self.mappings.unmap(
 597             self.allocator,
 598             address,
 599             parsed.len,
 600         );
 601         self.physical.unmaps +|= 1;
 602         self.physical.unmapped_bytes +|= parsed.len;
 603         self.physical.untracked_unmap_bytes +|= removal.untrackedBytes();
 604         self.updatePhysicalLiveBytes();
 605     }
 606 
 607     const PhysicalEffect = enum {
 608         protect,
 609         discard,
 610         decommit,
 611         advise,
 612     };
 613 
 614     fn recordPhysicalEffect(
 615         self: *Analyzer,
 616         succeeded: bool,
 617         effect: PhysicalEffect,
 618     ) void {
 619         const counter = switch (effect) {
 620             .protect => if (succeeded)
 621                 &self.physical.protects
 622             else
 623                 &self.physical.failed_protects,
 624             .discard => if (succeeded)
 625                 &self.physical.discards
 626             else
 627                 &self.physical.failed_discards,
 628             .decommit => if (succeeded)
 629                 &self.physical.decommits
 630             else
 631                 &self.physical.failed_decommits,
 632             .advise => if (succeeded)
 633                 &self.physical.advises
 634             else
 635                 &self.physical.failed_advises,
 636         };
 637         counter.* +|= 1;
 638     }
 639 
 640     fn updatePhysicalLiveBytes(self: *Analyzer) void {
 641         self.physical.live_mapped_bytes = self.mappings.mapped_bytes;
 642         self.physical.high_water_mapped_bytes = @max(
 643             self.physical.high_water_mapped_bytes,
 644             self.physical.live_mapped_bytes,
 645         );
 646     }
 647 
 648     fn ingestGenericJsonLine(self: *Analyzer, text: []const u8) !void {
 649         var parsed = std.json.parseFromSlice(
 650             std.json.Value,
 651             self.allocator,
 652             text,
 653             .{},
 654         ) catch |err| switch (err) {
 655             error.OutOfMemory => return err,
 656             else => return error.InvalidEventJson,
 657         };
 658         defer parsed.deinit();
 659 
 660         const object = switch (parsed.value) {
 661             .object => |object| object,
 662             else => return error.InvalidEventJson,
 663         };
 664         try self.ingestParsedEvent(try event_mod.replayFromJsonObject(object));
 665     }
 666 
 667     pub fn captureIntegrity(self: *const Analyzer) CaptureIntegrity {
 668         var result: CaptureIntegrity = .{
 669             .status = "complete",
 670             .action = "none",
 671             .message = null,
 672             .event_count = self.events,
 673             .sequenced_event_count = self.integrity_counters.sequenced_events,
 674             .unsequenced_event_count = self.integrity_counters.unsequenced_events,
 675             .first_sequence = self.first_sequence,
 676             .last_sequence = self.last_sequence,
 677             .sequence_gap_count = self.integrity_counters.sequence_gaps,
 678             .missing_sequence_event_count = self.integrity_counters.missing_sequence_events,
 679             .sequence_regression_count = self.integrity_counters.sequence_regressions,
 680             .start_event_count = self.integrity_counters.start_events,
 681             .stop_event_count = self.integrity_counters.stop_events,
 682             .start_sequence = self.start_sequence,
 683             .stop_sequence = self.stop_sequence,
 684             .unbalanced_event_count = self.unbalancedEventCount(),
 685         };
 686         classifyCaptureIntegrity(&result);
 687         return result;
 688     }
 689 
 690     fn recordSequence(self: *Analyzer, maybe_sequence: ?u64) void {
 691         const sequence = maybe_sequence orelse {
 692             self.integrity_counters.unsequenced_events +|= 1;
 693             return;
 694         };
 695         if (sequence == 0) {
 696             self.integrity_counters.unsequenced_events +|= 1;
 697             return;
 698         }
 699         self.integrity_counters.sequenced_events +|= 1;
 700         const previous = self.last_sequence orelse {
 701             self.first_sequence = sequence;
 702             self.last_sequence = sequence;
 703             if (sequence > 1) {
 704                 self.integrity_counters.sequence_gaps +|= 1;
 705                 self.integrity_counters.missing_sequence_events +|= sequence - 1;
 706             }
 707             return;
 708         };
 709         if (sequence <= previous) {
 710             self.integrity_counters.sequence_regressions +|= 1;
 711         } else if (sequence - previous > 1) {
 712             self.integrity_counters.sequence_gaps +|= 1;
 713             self.integrity_counters.missing_sequence_events +|= sequence - previous - 1;
 714         }
 715         self.last_sequence = sequence;
 716     }
 717 
 718     fn recordScopeEnter(
 719         self: *Analyzer,
 720         scope_id: u32,
 721         scope: []const u8,
 722     ) !void {
 723         const owned_scope = try self.internScope(scope);
 724         const path = try self.scope_paths.getOrPut(self.allocator, scope_id);
 725         if (!path.found_existing) {
 726             path.value_ptr.* = owned_scope;
 727         } else if (!std.mem.eql(u8, path.value_ptr.*, owned_scope)) {
 728             return error.ScopeIdentityConflict;
 729         }
 730         const result = try self.active_scopes.getOrPut(self.allocator, scope_id);
 731         if (!result.found_existing) result.value_ptr.* = 0;
 732         result.value_ptr.* +|= 1;
 733         self.integrity_counters.active_scope_events +|= 1;
 734     }
 735 
 736     fn recordScopeExit(self: *Analyzer, scope_id: u32) void {
 737         const count = self.active_scopes.getPtr(scope_id) orelse {
 738             self.integrity_counters.unmatched_scope_exits +|= 1;
 739             return;
 740         };
 741         if (count.* == 0) {
 742             self.integrity_counters.unmatched_scope_exits +|= 1;
 743             return;
 744         }
 745         count.* -= 1;
 746         self.integrity_counters.active_scope_events -= 1;
 747         if (count.* == 0) _ = self.active_scopes.remove(scope_id);
 748     }
 749 
 750     fn unbalancedEventCount(self: *const Analyzer) u64 {
 751         var count = self.counters.unmatched_frees +|
 752             self.counters.unmatched_resizes +|
 753             self.counters.unmatched_remaps +|
 754             self.integrity_counters.unmatched_scope_exits;
 755         count +|= self.integrity_counters.active_scope_events;
 756         return count;
 757     }
 758 
 759     pub fn snapshot(
 760         self: *Analyzer,
 761         allocator: Allocator,
 762         options: SnapshotOptions,
 763     ) !Snapshot {
 764         if (options.layer != self.layer_filter) {
 765             return error.LayerSelectionMismatch;
 766         }
 767         var scope_summaries: [3]std.ArrayListUnmanaged(ScopeSummary) = .{
 768             .empty,
 769             .empty,
 770             .empty,
 771         };
 772         defer for (&scope_summaries) |*summaries| {
 773             summaries.deinit(self.allocator);
 774         };
 775         var site_summaries: [3]std.ArrayListUnmanaged(SiteSummary) = .{
 776             .empty,
 777             .empty,
 778             .empty,
 779         };
 780         defer for (&site_summaries) |*summaries| {
 781             summaries.deinit(self.allocator);
 782         };
 783 
 784         inline for (std.meta.tags(Sort)) |sort| {
 785             const summary_options = SummaryOptions{
 786                 .top = options.top,
 787                 .min_bytes = options.min_bytes,
 788                 .include_zero_live = options.include_zero_live,
 789                 .include_sites = true,
 790                 .site_symbol_binary = options.site_symbol_binary,
 791                 .layer = options.layer,
 792                 .sort = sort,
 793             };
 794             scope_summaries[@backingInt(sort)] =
 795                 try self.collectScopeSummaries(summary_options);
 796             site_summaries[@backingInt(sort)] =
 797                 try self.collectSiteSummaries(summary_options);
 798         }
 799 
 800         var addresses = std.ArrayListUnmanaged(u64).empty;
 801         defer addresses.deinit(allocator);
 802         inline for (std.meta.tags(Sort)) |sort| {
 803             const sites = site_summaries[@backingInt(sort)].items;
 804             const limit = @min(options.top, sites.len);
 805             for (sites[0..limit]) |site| {
 806                 if (std.mem.indexOfScalar(
 807                     u64,
 808                     addresses.items,
 809                     site.return_address,
 810                 ) == null) {
 811                     try addresses.append(allocator, site.return_address);
 812                 }
 813             }
 814         }
 815 
 816         var maybe_symbols = if (options.site_symbol_binary) |binary|
 817             try resolveSiteAddressesAlloc(allocator, binary, addresses.items)
 818         else
 819             null;
 820         defer if (maybe_symbols) |*symbols| symbols.deinit(allocator);
 821 
 822         var result = Snapshot{
 823             .allocator = allocator,
 824             .integrity = self.captureIntegrity(),
 825             .summary = summarySnapshot(self),
 826         };
 827         errdefer result.deinit();
 828         inline for (std.meta.tags(Sort)) |sort| {
 829             const scopes = scope_summaries[@backingInt(sort)].items;
 830             const sites = site_summaries[@backingInt(sort)].items;
 831             const ranking = result.rankings.getPtr(sort);
 832             ranking.scopes = try snapshotScopes(
 833                 allocator,
 834                 scopes[0..@min(options.top, scopes.len)],
 835             );
 836             ranking.sources = try snapshotSources(
 837                 allocator,
 838                 sites[0..@min(options.top, sites.len)],
 839                 if (maybe_symbols) |*symbols| symbols else null,
 840             );
 841         }
 842         if (maybe_symbols) |*symbols| {
 843             result.symbol_storage = symbols.stdout;
 844             symbols.ranges.deinit(allocator);
 845             symbols.frames.deinit(allocator);
 846             symbols.* = undefined;
 847             maybe_symbols = null;
 848         }
 849         return result;
 850     }
 851 
 852     pub fn writeSummary(self: *Analyzer, writer: *std.Io.Writer, options: SummaryOptions) !void {
 853         if (options.layer != self.layer_filter) {
 854             return error.LayerSelectionMismatch;
 855         }
 856         var summaries = try self.collectScopeSummaries(options);
 857         defer summaries.deinit(self.allocator);
 858 
 859         try writeCaptureIntegrityText(writer, self.captureIntegrity());
 860         if (options.layer == .logical) {
 861             try writer.print(
 862                 "memtrace layer={s} sort={s} events={d} allocations={d} " ++
 863                     "frees={d} " ++
 864                     "open_requests={d} requested_bytes={d} " ++
 865                     "explicitly_closed_bytes={d} open_request_bytes={d} " ++
 866                     "high_water_open_request_bytes={d} " ++
 867                     "bulk_invalidated_requests={d} bulk_invalidated_bytes={d} " ++
 868                     "untracked_requests={d} untracked_request_bytes={d}\n",
 869                 .{
 870                     options.layer.tag(),
 871                     options.sort.name(),
 872                     self.events,
 873                     self.counters.allocations,
 874                     self.counters.frees,
 875                     self.counters.live_allocations,
 876                     self.counters.allocated_bytes,
 877                     self.counters.freed_bytes,
 878                     self.counters.live_bytes,
 879                     self.counters.high_water_live_bytes,
 880                     self.counters.bulk_invalidated_requests,
 881                     self.counters.bulk_invalidated_bytes,
 882                     self.counters.untracked_requests,
 883                     self.counters.untracked_request_bytes,
 884                 },
 885             );
 886             const coverage = self.lifecycleCoverage();
 887             try writer.print(
 888                 "memtrace lifecycle_coverage={s} instrumented_producers={d} " ++
 889                     "uninstrumented_producers={d} " ++
 890                     "lifecycle_not_required_producers={d} " ++
 891                     "prefix_complete_producers={d} prefix_partial_producers={d} " ++
 892                     "lifecycle_events={d}\n",
 893                 .{
 894                     coverage.status(),
 895                     coverage.instrumented,
 896                     coverage.uninstrumented,
 897                     coverage.not_required,
 898                     coverage.prefix_complete,
 899                     coverage.prefix_partial,
 900                     self.counters.lifecycle_events,
 901                 },
 902             );
 903         } else {
 904             try writer.print(
 905                 "memtrace layer={s} sort={s} events={d} allocations={d} " ++
 906                     "frees={d} live_allocations={d} allocated_bytes={d} " ++
 907                     "freed_bytes={d} live_bytes={d} " ++
 908                     "high_water_live_bytes={d}\n",
 909                 .{
 910                     options.layer.tag(),
 911                     options.sort.name(),
 912                     self.events,
 913                     self.counters.allocations,
 914                     self.counters.frees,
 915                     self.counters.live_allocations,
 916                     self.counters.allocated_bytes,
 917                     self.counters.freed_bytes,
 918                     self.counters.live_bytes,
 919                     self.counters.high_water_live_bytes,
 920                 },
 921             );
 922             try writer.print(
 923                 "memtrace retained_bytes={d} high_water_retained_bytes={d}\n",
 924                 .{
 925                     self.counters.retained_bytes,
 926                     self.counters.high_water_retained_bytes,
 927                 },
 928             );
 929         }
 930         try writer.print(
 931             "memtrace lifetimes completed={d} total_events={d} mean_events={d} " ++
 932                 "max_events={d} total_byte_events={d} " ++
 933                 "mean_byte_events={d} max_byte_events={d}\n",
 934             .{
 935                 self.counters.completed_lifetimes,
 936                 self.counters.lifetime_total_events,
 937                 meanLifetimeEvents(self.counters),
 938                 self.counters.lifetime_max_events,
 939                 self.counters.lifetime_total_byte_events,
 940                 meanLifetimeByteEvents(self.counters),
 941                 self.counters.lifetime_max_byte_events,
 942             },
 943         );
 944         if (self.counters.failed_allocations != 0 or
 945             self.counters.failed_resizes != 0 or
 946             self.counters.failed_remaps != 0 or
 947             self.counters.unmatched_frees != 0 or
 948             self.counters.unmatched_resizes != 0 or
 949             self.counters.unmatched_remaps != 0)
 950         {
 951             try writer.print(
 952                 "memtrace anomalies failed_allocations={d} failed_resizes={d} " ++
 953                     "failed_remaps={d} unmatched_frees={d} unmatched_resizes={d} " ++
 954                     "unmatched_remaps={d}\n",
 955                 .{
 956                     self.counters.failed_allocations,
 957                     self.counters.failed_resizes,
 958                     self.counters.failed_remaps,
 959                     self.counters.unmatched_frees,
 960                     self.counters.unmatched_resizes,
 961                     self.counters.unmatched_remaps,
 962                 },
 963             );
 964         }
 965         if (options.layer.includes(.physical_page)) {
 966             try self.writePhysicalSummary(writer);
 967         }
 968         const limit = @min(options.top, summaries.items.len);
 969         for (summaries.items[0..limit]) |summary| {
 970             if (options.layer == .logical) {
 971                 try writer.print(
 972                     "{s} open_request_bytes={d} " ++
 973                         "high_water_open_request_bytes={d} requested_bytes={d} " ++
 974                         "explicitly_closed_bytes={d} allocations={d} frees={d} " ++
 975                         "open_requests={d} bulk_invalidated_requests={d} " ++
 976                         "bulk_invalidated_bytes={d} untracked_requests={d} " ++
 977                         "untracked_request_bytes={d}\n",
 978                     .{
 979                         summary.path,
 980                         summary.counters.live_bytes,
 981                         summary.counters.high_water_live_bytes,
 982                         summary.counters.allocated_bytes,
 983                         summary.counters.freed_bytes,
 984                         summary.counters.allocations,
 985                         summary.counters.frees,
 986                         summary.counters.live_allocations,
 987                         summary.counters.bulk_invalidated_requests,
 988                         summary.counters.bulk_invalidated_bytes,
 989                         summary.counters.untracked_requests,
 990                         summary.counters.untracked_request_bytes,
 991                     },
 992                 );
 993             } else {
 994                 try writer.print(
 995                     "{s} retained_bytes={d} high_water_retained_bytes={d} " ++
 996                         "live_bytes={d} high_water_live_bytes={d} " ++
 997                         "allocated_bytes={d} freed_bytes={d} allocations={d} " ++
 998                         "frees={d} live_allocations={d} completed_lifetimes={d} " ++
 999                         "lifetime_total_events={d} lifetime_mean_events={d} " ++
1000                         "lifetime_max_events={d} " ++
1001                         "lifetime_total_byte_events={d} " ++
1002                         "lifetime_mean_byte_events={d} " ++
1003                         "lifetime_max_byte_events={d}\n",
1004                     .{
1005                         summary.path,
1006                         summary.counters.retained_bytes,
1007                         summary.counters.high_water_retained_bytes,
1008                         summary.counters.live_bytes,
1009                         summary.counters.high_water_live_bytes,
1010                         summary.counters.allocated_bytes,
1011                         summary.counters.freed_bytes,
1012                         summary.counters.allocations,
1013                         summary.counters.frees,
1014                         summary.counters.live_allocations,
1015                         summary.counters.completed_lifetimes,
1016                         summary.counters.lifetime_total_events,
1017                         meanLifetimeEvents(summary.counters),
1018                         summary.counters.lifetime_max_events,
1019                         summary.counters.lifetime_total_byte_events,
1020                         meanLifetimeByteEvents(summary.counters),
1021                         summary.counters.lifetime_max_byte_events,
1022                     },
1023                 );
1024             }
1025         }
1026         if (options.include_sites) try self.writeSiteSummary(writer, options);
1027         if (options.site_detail_return_address) |return_address| try self.writeSiteDetailSummary(writer, options, return_address);
1028     }
1029 
1030     pub fn writeSummaryJsonl(self: *Analyzer, writer: *std.Io.Writer, options: SummaryOptions) !void {
1031         if (options.layer != self.layer_filter) {
1032             return error.LayerSelectionMismatch;
1033         }
1034         var summaries = try self.collectScopeSummaries(options);
1035         defer summaries.deinit(self.allocator);
1036 
1037         var stream = pretty_json.Writer.init(writer, .minified);
1038         const object = try stream.object();
1039         try object.field("kind", "summary");
1040         try object.field("layer", options.layer.tag());
1041         try object.field("sort", options.sort.name());
1042         try object.field("events", self.events);
1043         try object.field("allocations", self.counters.allocations);
1044         try object.field("frees", self.counters.frees);
1045         try object.field("resizes", self.counters.resizes);
1046         try object.field("remaps", self.counters.remaps);
1047         try object.field("allocated_bytes", self.counters.allocated_bytes);
1048         if (options.layer == .logical) {
1049             try object.field("open_requests", self.counters.live_allocations);
1050             try object.field("requested_bytes", self.counters.allocated_bytes);
1051             try object.field(
1052                 "explicitly_closed_bytes",
1053                 self.counters.freed_bytes,
1054             );
1055             try object.field("open_request_bytes", self.counters.live_bytes);
1056             try object.field(
1057                 "high_water_open_request_bytes",
1058                 self.counters.high_water_live_bytes,
1059             );
1060             try object.field(
1061                 "bulk_invalidated_requests",
1062                 self.counters.bulk_invalidated_requests,
1063             );
1064             try object.field(
1065                 "bulk_invalidated_bytes",
1066                 self.counters.bulk_invalidated_bytes,
1067             );
1068             try object.field(
1069                 "untracked_requests",
1070                 self.counters.untracked_requests,
1071             );
1072             try object.field(
1073                 "untracked_request_bytes",
1074                 self.counters.untracked_request_bytes,
1075             );
1076             try object.field(
1077                 "lifecycle_events",
1078                 self.counters.lifecycle_events,
1079             );
1080             const coverage = self.lifecycleCoverage();
1081             try object.field("lifecycle_coverage", coverage.status());
1082             try object.field(
1083                 "lifecycle_instrumented_producers",
1084                 coverage.instrumented,
1085             );
1086             try object.field(
1087                 "lifecycle_uninstrumented_producers",
1088                 coverage.uninstrumented,
1089             );
1090             try object.field(
1091                 "lifecycle_not_required_producers",
1092                 coverage.not_required,
1093             );
1094             try object.field(
1095                 "prefix_complete_producers",
1096                 coverage.prefix_complete,
1097             );
1098             try object.field(
1099                 "prefix_partial_producers",
1100                 coverage.prefix_partial,
1101             );
1102         } else {
1103             try object.field("live_allocations", self.counters.live_allocations);
1104             try object.field("freed_bytes", self.counters.freed_bytes);
1105             try object.field("live_bytes", self.counters.live_bytes);
1106             try object.field(
1107                 "high_water_live_bytes",
1108                 self.counters.high_water_live_bytes,
1109             );
1110             try object.field("retained_bytes", self.counters.retained_bytes);
1111             try object.field(
1112                 "high_water_retained_bytes",
1113                 self.counters.high_water_retained_bytes,
1114             );
1115         }
1116         try object.field("completed_lifetimes", self.counters.completed_lifetimes);
1117         try object.field("lifetime_total_events", self.counters.lifetime_total_events);
1118         try object.field("lifetime_mean_events", meanLifetimeEvents(self.counters));
1119         try object.field("lifetime_max_events", self.counters.lifetime_max_events);
1120         try object.field(
1121             "lifetime_total_byte_events",
1122             self.counters.lifetime_total_byte_events,
1123         );
1124         try object.field(
1125             "lifetime_mean_byte_events",
1126             meanLifetimeByteEvents(self.counters),
1127         );
1128         try object.field(
1129             "lifetime_max_byte_events",
1130             self.counters.lifetime_max_byte_events,
1131         );
1132         try object.field("failed_allocations", self.counters.failed_allocations);
1133         try object.field("failed_resizes", self.counters.failed_resizes);
1134         try object.field("failed_remaps", self.counters.failed_remaps);
1135         try object.field("unmatched_frees", self.counters.unmatched_frees);
1136         try object.field("unmatched_resizes", self.counters.unmatched_resizes);
1137         try object.field("unmatched_remaps", self.counters.unmatched_remaps);
1138         if (options.layer.includes(.physical_page)) {
1139             try self.writePhysicalSummaryJson(object);
1140         }
1141         try writeCaptureIntegrityJson(object, self.captureIntegrity());
1142         try object.endLine();
1143         const limit = @min(options.top, summaries.items.len);
1144         for (summaries.items[0..limit]) |summary| {
1145             var row_stream = pretty_json.Writer.init(writer, .minified);
1146             const row = try row_stream.object();
1147             try row.field("kind", "scope");
1148             try row.field("sort", options.sort.name());
1149             try row.field("scope", summary.path);
1150             try writeCounterFields(
1151                 row,
1152                 summary.counters,
1153                 options.layer == .logical,
1154             );
1155             try row.endLine();
1156         }
1157         if (options.include_sites) try self.writeSiteSummaryJsonl(writer, options);
1158         if (options.site_detail_return_address) |return_address| try self.writeSiteDetailSummaryJsonl(writer, options, return_address);
1159     }
1160 
1161     fn writePhysicalSummary(
1162         self: *const Analyzer,
1163         writer: *std.Io.Writer,
1164     ) !void {
1165         try writer.print(
1166             "memtrace physical maps={d} unmaps={d} protects={d} discards={d} " ++
1167                 "decommits={d} advises={d} mapped_bytes={d} unmapped_bytes={d} " ++
1168                 "displaced_bytes={d} untracked_unmap_bytes={d} " ++
1169                 "live_mapped_bytes={d} high_water_mapped_bytes={d} " ++
1170                 "live_ranges={d}\n",
1171             .{
1172                 self.physical.maps,
1173                 self.physical.unmaps,
1174                 self.physical.protects,
1175                 self.physical.discards,
1176                 self.physical.decommits,
1177                 self.physical.advises,
1178                 self.physical.mapped_bytes,
1179                 self.physical.unmapped_bytes,
1180                 self.physical.displaced_bytes,
1181                 self.physical.untracked_unmap_bytes,
1182                 self.physical.live_mapped_bytes,
1183                 self.physical.high_water_mapped_bytes,
1184                 self.mappings.count(),
1185             },
1186         );
1187         if (self.physical.failed_maps != 0 or
1188             self.physical.failed_unmaps != 0 or
1189             self.physical.failed_protects != 0 or
1190             self.physical.failed_discards != 0 or
1191             self.physical.failed_decommits != 0 or
1192             self.physical.failed_advises != 0)
1193         {
1194             try writer.print(
1195                 "memtrace physical_failures maps={d} unmaps={d} protects={d} " ++
1196                     "discards={d} decommits={d} advises={d}\n",
1197                 .{
1198                     self.physical.failed_maps,
1199                     self.physical.failed_unmaps,
1200                     self.physical.failed_protects,
1201                     self.physical.failed_discards,
1202                     self.physical.failed_decommits,
1203                     self.physical.failed_advises,
1204                 },
1205             );
1206         }
1207     }
1208 
1209     fn writePhysicalSummaryJson(
1210         self: *const Analyzer,
1211         object: pretty_json.Object,
1212     ) !void {
1213         const physical = try object.object("physical");
1214         try physical.field("maps", self.physical.maps);
1215         try physical.field("unmaps", self.physical.unmaps);
1216         try physical.field("protects", self.physical.protects);
1217         try physical.field("discards", self.physical.discards);
1218         try physical.field("decommits", self.physical.decommits);
1219         try physical.field("advises", self.physical.advises);
1220         try physical.field("failed_maps", self.physical.failed_maps);
1221         try physical.field("failed_unmaps", self.physical.failed_unmaps);
1222         try physical.field("failed_protects", self.physical.failed_protects);
1223         try physical.field("failed_discards", self.physical.failed_discards);
1224         try physical.field("failed_decommits", self.physical.failed_decommits);
1225         try physical.field("failed_advises", self.physical.failed_advises);
1226         try physical.field("mapped_bytes", self.physical.mapped_bytes);
1227         try physical.field("unmapped_bytes", self.physical.unmapped_bytes);
1228         try physical.field("displaced_bytes", self.physical.displaced_bytes);
1229         try physical.field("untracked_unmap_bytes", self.physical.untracked_unmap_bytes);
1230         try physical.field("live_mapped_bytes", self.physical.live_mapped_bytes);
1231         try physical.field("high_water_mapped_bytes", self.physical.high_water_mapped_bytes);
1232         try physical.field("live_ranges", self.mappings.count());
1233         try physical.end();
1234     }
1235 
1236     fn collectScopeSummaries(
1237         self: *Analyzer,
1238         options: SummaryOptions,
1239     ) !std.ArrayListUnmanaged(ScopeSummary) {
1240         var summaries = std.ArrayListUnmanaged(ScopeSummary).empty;
1241         errdefer summaries.deinit(self.allocator);
1242         var iterator = self.scopes.iterator();
1243         while (iterator.next()) |entry| {
1244             const counters = entry.value_ptr.*;
1245             const invisible = !options.include_zero_live and counters.live_bytes == 0 and
1246                 counters.high_water_live_bytes == 0 and counters.retained_bytes == 0;
1247             if (invisible) continue;
1248             const below_floor = counters.live_bytes < options.min_bytes and
1249                 counters.high_water_live_bytes < options.min_bytes and
1250                 counters.retained_bytes < options.min_bytes;
1251             if (below_floor) continue;
1252             try summaries.append(self.allocator, .{
1253                 .path = entry.key_ptr.*,
1254                 .counters = counters,
1255             });
1256         }
1257         std.mem.sort(
1258             ScopeSummary,
1259             summaries.items,
1260             options.sort,
1261             scopeSummaryGreaterThan,
1262         );
1263         return summaries;
1264     }
1265 
1266     fn recordAllocator(
1267         self: *Analyzer,
1268         allocator_id: u32,
1269         retains_freed_memory: bool,
1270         layer: event_mod.Layer,
1271         lifecycle_instrumented: bool,
1272         prefix_complete: bool,
1273     ) !void {
1274         try self.allocators.put(self.allocator, allocator_id, .{
1275             .retention = if (retains_freed_memory) .retains_freed_memory else .releases_freed_memory,
1276             .layer = layer,
1277             .lifecycle_instrumented = lifecycle_instrumented,
1278             .prefix_complete = prefix_complete,
1279         });
1280     }
1281 
1282     fn recordAllocation(self: *Analyzer, allocator_id: u32, key: u64, scope: []const u8, return_address: u64, len: usize) !void {
1283         const owned_scope = try self.internScope(scope);
1284         try self.allocations.put(self.allocator, key, .{
1285             .allocator_id = allocator_id,
1286             .len = len,
1287             .scope = owned_scope,
1288             .return_address = return_address,
1289             .allocation_event = self.events,
1290             .segment_event = self.events,
1291             .byte_events = 0,
1292         });
1293         self.applyAllocation(allocator_id, owned_scope, len);
1294         if (self.track_sites) {
1295             try self.applySiteAllocation(
1296                 allocator_id,
1297                 return_address,
1298                 len,
1299             );
1300         }
1301         if (self.site_detail_return_address == return_address) {
1302             try self.applySiteDetailAllocation(
1303                 allocator_id,
1304                 owned_scope,
1305                 len,
1306             );
1307         }
1308     }
1309 
1310     fn recordUntrackedAllocation(
1311         self: *Analyzer,
1312         scope: []const u8,
1313         return_address: u64,
1314         len: usize,
1315     ) !void {
1316         const owned_scope = try self.internScope(scope);
1317         self.counters.allocations +|= 1;
1318         self.counters.allocated_bytes +|= len;
1319         self.counters.untracked_requests +|= 1;
1320         self.counters.untracked_request_bytes +|= len;
1321         const scope_counters = self.scopes.getPtr(owned_scope).?;
1322         scope_counters.allocations +|= 1;
1323         scope_counters.allocated_bytes +|= len;
1324         scope_counters.untracked_requests +|= 1;
1325         scope_counters.untracked_request_bytes +|= len;
1326         if (self.track_sites) {
1327             const site = try self.siteCounters(return_address);
1328             site.allocations +|= 1;
1329             site.allocated_bytes +|= len;
1330             site.untracked_requests +|= 1;
1331             site.untracked_request_bytes +|= len;
1332         }
1333     }
1334 
1335     fn recordFree(self: *Analyzer, key: u64, allocator_id: u32, scope: []const u8, len: usize) !void {
1336         if (self.allocations.fetchRemove(key)) |removed| {
1337             self.clearDrainedAllocations();
1338             self.applyLifetime(
1339                 removed.value.scope,
1340                 removed.value.return_address,
1341                 removed.value.len,
1342                 completedLifetimeEvents(removed.value.allocation_event, self.events),
1343                 removed.value.byte_events +|
1344                     completedLifetimeByteEvents(
1345                         removed.value.len,
1346                         removed.value.segment_event,
1347                         self.events,
1348                     ),
1349             );
1350             self.applyFree(removed.value.allocator_id, removed.value.scope, removed.value.len);
1351             if (self.track_sites) self.applySiteFree(removed.value.allocator_id, removed.value.return_address, removed.value.len);
1352             if (self.site_detail_return_address == removed.value.return_address) self.applySiteDetailFree(removed.value.allocator_id, removed.value.scope, removed.value.len);
1353             return;
1354         }
1355         self.counters.unmatched_frees += 1;
1356         const owned_scope = try self.internScope(scope);
1357         self.applyFree(allocator_id, owned_scope, len);
1358     }
1359 
1360     fn recordRelease(self: *Analyzer, key: u64) !void {
1361         const removed = self.allocations.fetchRemove(key) orelse {
1362             self.counters.unmatched_frees +|= 1;
1363             return;
1364         };
1365         self.clearDrainedAllocations();
1366         self.applyLifetime(
1367             removed.value.scope,
1368             removed.value.return_address,
1369             removed.value.len,
1370             completedLifetimeEvents(
1371                 removed.value.allocation_event,
1372                 self.events,
1373             ),
1374             removed.value.byte_events +|
1375                 completedLifetimeByteEvents(
1376                     removed.value.len,
1377                     removed.value.segment_event,
1378                     self.events,
1379                 ),
1380         );
1381         self.applyBulkInvalidation(
1382             removed.value.allocator_id,
1383             removed.value.scope,
1384             removed.value.len,
1385         );
1386         if (self.track_sites) {
1387             self.applySiteBulkInvalidation(
1388                 removed.value.return_address,
1389                 removed.value.len,
1390             );
1391         }
1392         if (self.site_detail_return_address == removed.value.return_address) {
1393             self.applySiteDetailBulkInvalidation(
1394                 removed.value.scope,
1395                 removed.value.len,
1396             );
1397         }
1398     }
1399 
1400     fn recordResize(self: *Analyzer, key: u64, allocator_id: u32, scope: []const u8, old_len: usize, new_len: usize) !void {
1401         self.counters.resizes += 1;
1402         try self.resizeAllocation(
1403             key,
1404             allocator_id,
1405             scope,
1406             old_len,
1407             new_len,
1408             false,
1409         );
1410     }
1411 
1412     fn resizeAllocation(
1413         self: *Analyzer,
1414         key: u64,
1415         allocator_id: u32,
1416         scope: []const u8,
1417         old_len: usize,
1418         new_len: usize,
1419         is_remap: bool,
1420     ) !void {
1421         if (self.allocations.getPtr(key)) |record| {
1422             accrueLifetimeByteEvents(record, self.events);
1423             self.applyResize(record.allocator_id, record.scope, record.len, new_len);
1424             if (self.track_sites) self.applySiteResize(record.allocator_id, record.return_address, record.len, new_len);
1425             if (self.site_detail_return_address == record.return_address) try self.applySiteDetailResize(record.allocator_id, record.scope, record.len, new_len);
1426             record.len = new_len;
1427             return;
1428         }
1429         _ = allocator_id;
1430         _ = scope;
1431         _ = old_len;
1432         if (is_remap) {
1433             self.counters.unmatched_remaps += 1;
1434         } else {
1435             self.counters.unmatched_resizes += 1;
1436         }
1437     }
1438 
1439     fn recordRemap(self: *Analyzer, old_key: u64, new_key: u64, allocator_id: u32, scope: []const u8, old_len: usize, new_len: usize) !void {
1440         self.counters.remaps += 1;
1441         if (old_key == new_key) {
1442             try self.resizeAllocation(
1443                 old_key,
1444                 allocator_id,
1445                 scope,
1446                 old_len,
1447                 new_len,
1448                 true,
1449             );
1450             return;
1451         }
1452         if (self.allocations.fetchRemove(old_key)) |removed| {
1453             var record = removed.value;
1454             self.clearDrainedAllocations();
1455             accrueLifetimeByteEvents(&record, self.events);
1456             self.applyResize(record.allocator_id, record.scope, record.len, new_len);
1457             if (self.track_sites) self.applySiteResize(record.allocator_id, record.return_address, record.len, new_len);
1458             if (self.site_detail_return_address == record.return_address) try self.applySiteDetailResize(record.allocator_id, record.scope, record.len, new_len);
1459             record.len = new_len;
1460             try self.allocations.put(self.allocator, new_key, record);
1461             return;
1462         }
1463         self.counters.unmatched_remaps += 1;
1464     }
1465 
1466     fn recordLostTrackingRemap(
1467         self: *Analyzer,
1468         key: u64,
1469         new_len: usize,
1470     ) !void {
1471         const removed = self.allocations.fetchRemove(key) orelse {
1472             self.counters.unmatched_remaps +|= 1;
1473             return;
1474         };
1475         self.clearDrainedAllocations();
1476         const record = removed.value;
1477         self.counters.resizes +|= 1;
1478         self.applyResize(
1479             record.allocator_id,
1480             record.scope,
1481             record.len,
1482             new_len,
1483         );
1484         self.applyLostTracking(record.scope, new_len);
1485         if (self.track_sites) {
1486             self.applySiteResize(
1487                 record.allocator_id,
1488                 record.return_address,
1489                 record.len,
1490                 new_len,
1491             );
1492             self.applySiteLostTracking(record.return_address, new_len);
1493         }
1494         if (self.site_detail_return_address == record.return_address) {
1495             try self.applySiteDetailResize(
1496                 record.allocator_id,
1497                 record.scope,
1498                 record.len,
1499                 new_len,
1500             );
1501             self.applySiteDetailLostTracking(record.scope, new_len);
1502         }
1503     }
1504 
1505     fn applyAllocation(self: *Analyzer, allocator_id: u32, scope: []const u8, len: usize) void {
1506         self.counters.allocations += 1;
1507         self.counters.live_allocations += 1;
1508         self.counters.allocated_bytes += len;
1509         self.counters.live_bytes += len;
1510         self.counters.high_water_live_bytes = @max(self.counters.high_water_live_bytes, self.counters.live_bytes);
1511         const tracks_retained = self.allocatorLayer(allocator_id) !=
1512             .logical_allocator;
1513         if (tracks_retained) {
1514             self.counters.retained_bytes += len;
1515             self.counters.high_water_retained_bytes = @max(
1516                 self.counters.high_water_retained_bytes,
1517                 self.counters.retained_bytes,
1518             );
1519         }
1520 
1521         const counters = self.scopes.getPtr(scope) orelse return;
1522         counters.allocations += 1;
1523         counters.live_allocations += 1;
1524         counters.allocated_bytes += len;
1525         counters.live_bytes += len;
1526         counters.high_water_live_bytes = @max(counters.high_water_live_bytes, counters.live_bytes);
1527         if (tracks_retained) {
1528             counters.retained_bytes += len;
1529             counters.high_water_retained_bytes = @max(
1530                 counters.high_water_retained_bytes,
1531                 counters.retained_bytes,
1532             );
1533         }
1534     }
1535 
1536     fn applyFree(self: *Analyzer, allocator_id: u32, scope: []const u8, len: usize) void {
1537         self.counters.frees += 1;
1538         if (self.counters.live_allocations > 0) self.counters.live_allocations -= 1;
1539         self.counters.freed_bytes += len;
1540         if (self.counters.live_bytes >= len) self.counters.live_bytes -= len else self.counters.live_bytes = 0;
1541         const releases = self.allocatorReleases(allocator_id);
1542         if (releases and self.counters.retained_bytes >= len) self.counters.retained_bytes -= len;
1543 
1544         const counters = self.scopes.getPtr(scope) orelse return;
1545         counters.frees += 1;
1546         if (counters.live_allocations > 0) counters.live_allocations -= 1;
1547         counters.freed_bytes += len;
1548         if (counters.live_bytes >= len) counters.live_bytes -= len else counters.live_bytes = 0;
1549         if (releases and counters.retained_bytes >= len) counters.retained_bytes -= len;
1550     }
1551 
1552     fn applyBulkInvalidation(
1553         self: *Analyzer,
1554         allocator_id: u32,
1555         scope: []const u8,
1556         len: usize,
1557     ) void {
1558         _ = allocator_id;
1559         self.counters.bulk_invalidated_requests +|= 1;
1560         self.counters.bulk_invalidated_bytes +|= len;
1561         if (self.counters.live_allocations > 0) {
1562             self.counters.live_allocations -= 1;
1563         }
1564         if (self.counters.live_bytes >= len) {
1565             self.counters.live_bytes -= len;
1566         } else {
1567             self.counters.live_bytes = 0;
1568         }
1569         const counters = self.scopes.getPtr(scope) orelse return;
1570         counters.bulk_invalidated_requests +|= 1;
1571         counters.bulk_invalidated_bytes +|= len;
1572         if (counters.live_allocations > 0) counters.live_allocations -= 1;
1573         if (counters.live_bytes >= len) {
1574             counters.live_bytes -= len;
1575         } else {
1576             counters.live_bytes = 0;
1577         }
1578     }
1579 
1580     fn applyLostTracking(
1581         self: *Analyzer,
1582         scope: []const u8,
1583         len: usize,
1584     ) void {
1585         applyCountersLostTracking(&self.counters, len);
1586         if (self.scopes.getPtr(scope)) |counters| {
1587             applyCountersLostTracking(counters, len);
1588         }
1589     }
1590 
1591     fn applyLifetime(
1592         self: *Analyzer,
1593         scope: []const u8,
1594         return_address: u64,
1595         len: usize,
1596         lifetime_events: u64,
1597         lifetime_byte_events: u64,
1598     ) void {
1599         applyCompletedLifetime(
1600             &self.counters,
1601             lifetime_events,
1602             lifetime_byte_events,
1603         );
1604         if (self.scopes.getPtr(scope)) |counters| {
1605             applyCompletedLifetime(
1606                 counters,
1607                 lifetime_events,
1608                 lifetime_byte_events,
1609             );
1610         }
1611         if (self.track_sites) {
1612             if (self.sites.getPtr(return_address)) |counters| {
1613                 applyCompletedLifetime(
1614                     counters,
1615                     lifetime_events,
1616                     lifetime_byte_events,
1617                 );
1618             }
1619         }
1620         if (self.site_detail_return_address == return_address) {
1621             if (self.site_detail_sizes.getPtr(len)) |counters| {
1622                 applyCompletedLifetime(
1623                     counters,
1624                     lifetime_events,
1625                     lifetime_byte_events,
1626                 );
1627             }
1628             if (self.site_detail_scopes.getPtr(scope)) |counters| {
1629                 applyCompletedLifetime(
1630                     counters,
1631                     lifetime_events,
1632                     lifetime_byte_events,
1633                 );
1634             }
1635         }
1636     }
1637 
1638     fn applyResize(self: *Analyzer, allocator_id: u32, scope: []const u8, old_len: usize, new_len: usize) void {
1639         if (new_len >= old_len) {
1640             const delta = new_len - old_len;
1641             self.counters.allocated_bytes += delta;
1642             self.counters.live_bytes += delta;
1643             self.counters.high_water_live_bytes = @max(self.counters.high_water_live_bytes, self.counters.live_bytes);
1644             const tracks_retained = self.allocatorLayer(allocator_id) !=
1645                 .logical_allocator;
1646             if (tracks_retained) {
1647                 self.counters.retained_bytes += delta;
1648                 self.counters.high_water_retained_bytes = @max(
1649                     self.counters.high_water_retained_bytes,
1650                     self.counters.retained_bytes,
1651                 );
1652             }
1653             if (self.scopes.getPtr(scope)) |counters| {
1654                 counters.allocated_bytes += delta;
1655                 counters.live_bytes += delta;
1656                 counters.high_water_live_bytes = @max(counters.high_water_live_bytes, counters.live_bytes);
1657                 if (tracks_retained) {
1658                     counters.retained_bytes += delta;
1659                     counters.high_water_retained_bytes = @max(
1660                         counters.high_water_retained_bytes,
1661                         counters.retained_bytes,
1662                     );
1663                 }
1664             }
1665         } else {
1666             const delta = old_len - new_len;
1667             self.counters.freed_bytes += delta;
1668             if (self.counters.live_bytes >= delta) self.counters.live_bytes -= delta else self.counters.live_bytes = 0;
1669             const releases = self.allocatorReleases(allocator_id);
1670             if (releases and self.counters.retained_bytes >= delta) self.counters.retained_bytes -= delta;
1671             if (self.scopes.getPtr(scope)) |counters| {
1672                 counters.freed_bytes += delta;
1673                 if (counters.live_bytes >= delta) counters.live_bytes -= delta else counters.live_bytes = 0;
1674                 if (releases and counters.retained_bytes >= delta) counters.retained_bytes -= delta;
1675             }
1676         }
1677     }
1678 
1679     fn applySiteAllocation(
1680         self: *Analyzer,
1681         allocator_id: u32,
1682         return_address: u64,
1683         len: usize,
1684     ) !void {
1685         const counters = try self.siteCounters(return_address);
1686         counters.allocations += 1;
1687         counters.live_allocations += 1;
1688         counters.allocated_bytes += len;
1689         counters.live_bytes += len;
1690         counters.high_water_live_bytes = @max(counters.high_water_live_bytes, counters.live_bytes);
1691         if (self.allocatorLayer(allocator_id) != .logical_allocator) {
1692             counters.retained_bytes += len;
1693             counters.high_water_retained_bytes = @max(
1694                 counters.high_water_retained_bytes,
1695                 counters.retained_bytes,
1696             );
1697         }
1698     }
1699 
1700     fn applySiteFree(self: *Analyzer, allocator_id: u32, return_address: u64, len: usize) void {
1701         const counters = self.sites.getPtr(return_address) orelse return;
1702         counters.frees += 1;
1703         if (counters.live_allocations > 0) counters.live_allocations -= 1;
1704         counters.freed_bytes += len;
1705         if (counters.live_bytes >= len) counters.live_bytes -= len else counters.live_bytes = 0;
1706         if (self.allocatorReleases(allocator_id) and counters.retained_bytes >= len) counters.retained_bytes -= len;
1707     }
1708 
1709     fn applySiteBulkInvalidation(
1710         self: *Analyzer,
1711         return_address: u64,
1712         len: usize,
1713     ) void {
1714         const counters = self.sites.getPtr(return_address) orelse return;
1715         counters.bulk_invalidated_requests +|= 1;
1716         counters.bulk_invalidated_bytes +|= len;
1717         if (counters.live_allocations > 0) counters.live_allocations -= 1;
1718         if (counters.live_bytes >= len) {
1719             counters.live_bytes -= len;
1720         } else {
1721             counters.live_bytes = 0;
1722         }
1723     }
1724 
1725     fn applySiteLostTracking(
1726         self: *Analyzer,
1727         return_address: u64,
1728         len: usize,
1729     ) void {
1730         const counters = self.sites.getPtr(return_address) orelse return;
1731         applyCountersLostTracking(counters, len);
1732     }
1733 
1734     fn applySiteResize(self: *Analyzer, allocator_id: u32, return_address: u64, old_len: usize, new_len: usize) void {
1735         const counters = self.sites.getPtr(return_address) orelse return;
1736         if (new_len >= old_len) {
1737             const delta = new_len - old_len;
1738             counters.allocated_bytes += delta;
1739             counters.live_bytes += delta;
1740             counters.high_water_live_bytes = @max(counters.high_water_live_bytes, counters.live_bytes);
1741             counters.retained_bytes += delta;
1742             counters.high_water_retained_bytes = @max(counters.high_water_retained_bytes, counters.retained_bytes);
1743         } else {
1744             const delta = old_len - new_len;
1745             counters.freed_bytes += delta;
1746             if (counters.live_bytes >= delta) counters.live_bytes -= delta else counters.live_bytes = 0;
1747             if (self.allocatorReleases(allocator_id) and counters.retained_bytes >= delta) counters.retained_bytes -= delta;
1748         }
1749     }
1750 
1751     fn siteCounters(self: *Analyzer, return_address: u64) !*SiteCounters {
1752         const entry = try self.sites.getOrPut(self.allocator, return_address);
1753         if (!entry.found_existing) entry.value_ptr.* = .{};
1754         return entry.value_ptr;
1755     }
1756 
1757     fn applySiteDetailAllocation(
1758         self: *Analyzer,
1759         allocator_id: u32,
1760         scope: []const u8,
1761         len: usize,
1762     ) !void {
1763         const tracks_retained = self.allocatorLayer(allocator_id) !=
1764             .logical_allocator;
1765         applyDetailAllocation(
1766             try self.siteDetailSizeCounters(len),
1767             len,
1768             tracks_retained,
1769         );
1770         applyDetailAllocation(
1771             try self.siteDetailScopeCounters(scope),
1772             len,
1773             tracks_retained,
1774         );
1775     }
1776 
1777     fn applySiteDetailFree(self: *Analyzer, allocator_id: u32, scope: []const u8, len: usize) void {
1778         const releases = self.allocatorReleases(allocator_id);
1779         if (self.site_detail_sizes.getPtr(len)) |counters| applyDetailFree(counters, len, releases);
1780         if (self.site_detail_scopes.getPtr(scope)) |counters| applyDetailFree(counters, len, releases);
1781     }
1782 
1783     fn applySiteDetailResize(self: *Analyzer, allocator_id: u32, scope: []const u8, old_len: usize, new_len: usize) !void {
1784         if (old_len == new_len) return;
1785         self.applySiteDetailFree(allocator_id, scope, old_len);
1786         try self.applySiteDetailAllocation(allocator_id, scope, new_len);
1787     }
1788 
1789     fn applySiteDetailBulkInvalidation(
1790         self: *Analyzer,
1791         scope: []const u8,
1792         len: usize,
1793     ) void {
1794         if (self.site_detail_sizes.getPtr(len)) |counters| {
1795             applyDetailBulkInvalidation(counters, len);
1796         }
1797         if (self.site_detail_scopes.getPtr(scope)) |counters| {
1798             applyDetailBulkInvalidation(counters, len);
1799         }
1800     }
1801 
1802     fn applySiteDetailLostTracking(
1803         self: *Analyzer,
1804         scope: []const u8,
1805         len: usize,
1806     ) void {
1807         if (self.site_detail_sizes.getPtr(len)) |counters| {
1808             applyCountersLostTracking(counters, len);
1809         }
1810         if (self.site_detail_scopes.getPtr(scope)) |counters| {
1811             applyCountersLostTracking(counters, len);
1812         }
1813     }
1814 
1815     fn siteDetailSizeCounters(self: *Analyzer, len: usize) !*SiteCounters {
1816         const entry = try self.site_detail_sizes.getOrPut(self.allocator, len);
1817         if (!entry.found_existing) entry.value_ptr.* = .{};
1818         return entry.value_ptr;
1819     }
1820 
1821     fn siteDetailScopeCounters(self: *Analyzer, scope: []const u8) !*SiteCounters {
1822         const entry = try self.site_detail_scopes.getOrPut(self.allocator, scope);
1823         if (!entry.found_existing) entry.value_ptr.* = .{};
1824         return entry.value_ptr;
1825     }
1826 
1827     fn applyDetailAllocation(
1828         counters: *SiteCounters,
1829         len: usize,
1830         tracks_retained: bool,
1831     ) void {
1832         counters.allocations += 1;
1833         counters.live_allocations += 1;
1834         counters.allocated_bytes += len;
1835         counters.live_bytes += len;
1836         counters.high_water_live_bytes = @max(counters.high_water_live_bytes, counters.live_bytes);
1837         if (tracks_retained) {
1838             counters.retained_bytes += len;
1839             counters.high_water_retained_bytes = @max(
1840                 counters.high_water_retained_bytes,
1841                 counters.retained_bytes,
1842             );
1843         }
1844     }
1845 
1846     fn applyDetailFree(counters: *SiteCounters, len: usize, releases: bool) void {
1847         counters.frees += 1;
1848         if (counters.live_allocations > 0) counters.live_allocations -= 1;
1849         counters.freed_bytes += len;
1850         if (counters.live_bytes >= len) counters.live_bytes -= len else counters.live_bytes = 0;
1851         if (releases and counters.retained_bytes >= len) counters.retained_bytes -= len;
1852     }
1853 
1854     fn applyDetailBulkInvalidation(
1855         counters: *SiteCounters,
1856         len: usize,
1857     ) void {
1858         counters.bulk_invalidated_requests +|= 1;
1859         counters.bulk_invalidated_bytes +|= len;
1860         if (counters.live_allocations > 0) counters.live_allocations -= 1;
1861         if (counters.live_bytes >= len) {
1862             counters.live_bytes -= len;
1863         } else {
1864             counters.live_bytes = 0;
1865         }
1866     }
1867 
1868     fn writeSiteSummary(self: *Analyzer, writer: *std.Io.Writer, options: SummaryOptions) !void {
1869         var summaries = try self.collectSiteSummaries(options);
1870         defer summaries.deinit(self.allocator);
1871         const limit = @min(options.top, summaries.items.len);
1872         var maybe_symbols = if (options.site_symbol_binary) |binary|
1873             try resolveSiteSummarySymbolsAlloc(
1874                 self.allocator,
1875                 binary,
1876                 summaries.items[0..limit],
1877             )
1878         else
1879             null;
1880         defer if (maybe_symbols) |*symbols| symbols.deinit(self.allocator);
1881         for (summaries.items[0..limit]) |summary| {
1882             const symbol = symbol: {
1883                 const symbols = if (maybe_symbols) |*value|
1884                     value
1885                 else
1886                     break :symbol null;
1887                 const frames = symbols.find(summary.return_address);
1888                 break :symbol if (frames.len == 0) null else frames[0];
1889             };
1890             try writer.print(
1891                 "site return_address=0x{x}",
1892                 .{summary.return_address},
1893             );
1894             if (symbol) |resolved| {
1895                 try writer.writeAll(" symbol=");
1896                 var symbol_stream = pretty_json.Writer.init(writer, .minified);
1897                 try symbol_stream.write(resolved.function);
1898                 try writer.writeAll(" location=");
1899                 var location_stream = pretty_json.Writer.init(writer, .minified);
1900                 try location_stream.write(resolved.location);
1901             }
1902             try writer.print(
1903                 " retained_bytes={d} high_water_retained_bytes={d} live_bytes={d} high_water_live_bytes={d} allocated_bytes={d} freed_bytes={d} allocations={d} frees={d} live_allocations={d} completed_lifetimes={d} lifetime_total_events={d} lifetime_mean_events={d} lifetime_max_events={d} lifetime_total_byte_events={d} lifetime_mean_byte_events={d} lifetime_max_byte_events={d}\n",
1904                 .{
1905                     summary.counters.retained_bytes,
1906                     summary.counters.high_water_retained_bytes,
1907                     summary.counters.live_bytes,
1908                     summary.counters.high_water_live_bytes,
1909                     summary.counters.allocated_bytes,
1910                     summary.counters.freed_bytes,
1911                     summary.counters.allocations,
1912                     summary.counters.frees,
1913                     summary.counters.live_allocations,
1914                     summary.counters.completed_lifetimes,
1915                     summary.counters.lifetime_total_events,
1916                     meanLifetimeEvents(summary.counters),
1917                     summary.counters.lifetime_max_events,
1918                     summary.counters.lifetime_total_byte_events,
1919                     meanLifetimeByteEvents(summary.counters),
1920                     summary.counters.lifetime_max_byte_events,
1921                 },
1922             );
1923         }
1924     }
1925 
1926     fn writeSiteDetailSummary(self: *Analyzer, writer: *std.Io.Writer, options: SummaryOptions, return_address: u64) !void {
1927         try writer.print("site_detail return_address=0x{x}\n", .{return_address});
1928 
1929         var sizes = try self.collectSiteSizeSummaries(options.sort);
1930         defer sizes.deinit(self.allocator);
1931         const size_limit = @min(options.top, sizes.items.len);
1932         for (sizes.items[0..size_limit]) |summary| {
1933             try writer.print(
1934                 "site_size len={d} retained_bytes={d} high_water_retained_bytes={d} live_bytes={d} high_water_live_bytes={d} allocated_bytes={d} freed_bytes={d} allocations={d} frees={d} live_allocations={d} completed_lifetimes={d} lifetime_total_events={d} lifetime_mean_events={d} lifetime_max_events={d} lifetime_total_byte_events={d} lifetime_mean_byte_events={d} lifetime_max_byte_events={d}\n",
1935                 .{
1936                     summary.len,
1937                     summary.counters.retained_bytes,
1938                     summary.counters.high_water_retained_bytes,
1939                     summary.counters.live_bytes,
1940                     summary.counters.high_water_live_bytes,
1941                     summary.counters.allocated_bytes,
1942                     summary.counters.freed_bytes,
1943                     summary.counters.allocations,
1944                     summary.counters.frees,
1945                     summary.counters.live_allocations,
1946                     summary.counters.completed_lifetimes,
1947                     summary.counters.lifetime_total_events,
1948                     meanLifetimeEvents(summary.counters),
1949                     summary.counters.lifetime_max_events,
1950                     summary.counters.lifetime_total_byte_events,
1951                     meanLifetimeByteEvents(summary.counters),
1952                     summary.counters.lifetime_max_byte_events,
1953                 },
1954             );
1955         }
1956 
1957         var scopes = try self.collectSiteScopeSummaries(options.sort);
1958         defer scopes.deinit(self.allocator);
1959         const scope_limit = @min(options.top, scopes.items.len);
1960         for (scopes.items[0..scope_limit]) |summary| {
1961             try writer.writeAll("site_scope scope=");
1962             var stream = pretty_json.Writer.init(writer, .minified);
1963             try stream.write(summary.scope);
1964             try writer.print(
1965                 " retained_bytes={d} high_water_retained_bytes={d} live_bytes={d} high_water_live_bytes={d} allocated_bytes={d} freed_bytes={d} allocations={d} frees={d} live_allocations={d} completed_lifetimes={d} lifetime_total_events={d} lifetime_mean_events={d} lifetime_max_events={d} lifetime_total_byte_events={d} lifetime_mean_byte_events={d} lifetime_max_byte_events={d}\n",
1966                 .{
1967                     summary.counters.retained_bytes,
1968                     summary.counters.high_water_retained_bytes,
1969                     summary.counters.live_bytes,
1970                     summary.counters.high_water_live_bytes,
1971                     summary.counters.allocated_bytes,
1972                     summary.counters.freed_bytes,
1973                     summary.counters.allocations,
1974                     summary.counters.frees,
1975                     summary.counters.live_allocations,
1976                     summary.counters.completed_lifetimes,
1977                     summary.counters.lifetime_total_events,
1978                     meanLifetimeEvents(summary.counters),
1979                     summary.counters.lifetime_max_events,
1980                     summary.counters.lifetime_total_byte_events,
1981                     meanLifetimeByteEvents(summary.counters),
1982                     summary.counters.lifetime_max_byte_events,
1983                 },
1984             );
1985         }
1986     }
1987 
1988     fn writeSiteSummaryJsonl(self: *Analyzer, writer: *std.Io.Writer, options: SummaryOptions) !void {
1989         var summaries = try self.collectSiteSummaries(options);
1990         defer summaries.deinit(self.allocator);
1991         const limit = @min(options.top, summaries.items.len);
1992         var maybe_symbols = if (options.site_symbol_binary) |binary|
1993             try resolveSiteSummarySymbolsAlloc(
1994                 self.allocator,
1995                 binary,
1996                 summaries.items[0..limit],
1997             )
1998         else
1999             null;
2000         defer if (maybe_symbols) |*symbols| symbols.deinit(self.allocator);
2001         for (summaries.items[0..limit]) |summary| {
2002             const symbol = symbol: {
2003                 const symbols = if (maybe_symbols) |*value|
2004                     value
2005                 else
2006                     break :symbol null;
2007                 const frames = symbols.find(summary.return_address);
2008                 break :symbol if (frames.len == 0) null else frames[0];
2009             };
2010             var stream = pretty_json.Writer.init(writer, .minified);
2011             const object = try stream.object();
2012             try object.field("kind", "site");
2013             try object.field("sort", options.sort.name());
2014             try object.field("return_address", summary.return_address);
2015             if (symbol) |resolved| {
2016                 try object.field("symbol", resolved.function);
2017                 try object.field("location", resolved.location);
2018             }
2019             try writeCounterFields(
2020                 object,
2021                 summary.counters,
2022                 options.layer == .logical,
2023             );
2024             try object.endLine();
2025         }
2026     }
2027 
2028     fn writeSiteDetailSummaryJsonl(self: *Analyzer, writer: *std.Io.Writer, options: SummaryOptions, return_address: u64) !void {
2029         var detail_stream = pretty_json.Writer.init(writer, .minified);
2030         const detail = try detail_stream.object();
2031         try detail.field("kind", "site_detail");
2032         try detail.field("sort", options.sort.name());
2033         try detail.field("return_address", return_address);
2034         try detail.endLine();
2035 
2036         var sizes = try self.collectSiteSizeSummaries(options.sort);
2037         defer sizes.deinit(self.allocator);
2038         const size_limit = @min(options.top, sizes.items.len);
2039         for (sizes.items[0..size_limit]) |summary| {
2040             var stream = pretty_json.Writer.init(writer, .minified);
2041             const object = try stream.object();
2042             try object.field("kind", "site_size");
2043             try object.field("sort", options.sort.name());
2044             try object.field("return_address", return_address);
2045             try object.field("len", summary.len);
2046             try writeCounterFields(
2047                 object,
2048                 summary.counters,
2049                 options.layer == .logical,
2050             );
2051             try object.endLine();
2052         }
2053 
2054         var scopes = try self.collectSiteScopeSummaries(options.sort);
2055         defer scopes.deinit(self.allocator);
2056         const scope_limit = @min(options.top, scopes.items.len);
2057         for (scopes.items[0..scope_limit]) |summary| {
2058             var stream = pretty_json.Writer.init(writer, .minified);
2059             const object = try stream.object();
2060             try object.field("kind", "site_scope");
2061             try object.field("sort", options.sort.name());
2062             try object.field("return_address", return_address);
2063             try object.field("scope", summary.scope);
2064             try writeCounterFields(
2065                 object,
2066                 summary.counters,
2067                 options.layer == .logical,
2068             );
2069             try object.endLine();
2070         }
2071     }
2072 
2073     fn collectSiteSummaries(self: *Analyzer, options: SummaryOptions) !std.ArrayListUnmanaged(SiteSummary) {
2074         var summaries = std.ArrayListUnmanaged(SiteSummary).empty;
2075         var iterator = self.sites.iterator();
2076         while (iterator.next()) |entry| {
2077             const counters = entry.value_ptr.*;
2078             if (!options.include_zero_live and counters.live_bytes == 0 and counters.high_water_live_bytes == 0 and counters.retained_bytes == 0) continue;
2079             if (counters.live_bytes < options.min_bytes and counters.high_water_live_bytes < options.min_bytes and counters.retained_bytes < options.min_bytes) continue;
2080             try summaries.append(self.allocator, .{
2081                 .return_address = entry.key_ptr.*,
2082                 .counters = counters,
2083             });
2084         }
2085         std.mem.sort(
2086             SiteSummary,
2087             summaries.items,
2088             options.sort,
2089             siteSummaryGreaterThan,
2090         );
2091         return summaries;
2092     }
2093 
2094     fn collectSiteSizeSummaries(
2095         self: *Analyzer,
2096         sort: Sort,
2097     ) !std.ArrayListUnmanaged(SiteSizeSummary) {
2098         var summaries = std.ArrayListUnmanaged(SiteSizeSummary).empty;
2099         var iterator = self.site_detail_sizes.iterator();
2100         while (iterator.next()) |entry| {
2101             try summaries.append(self.allocator, .{
2102                 .len = entry.key_ptr.*,
2103                 .counters = entry.value_ptr.*,
2104             });
2105         }
2106         std.mem.sort(
2107             SiteSizeSummary,
2108             summaries.items,
2109             sort,
2110             siteSizeSummaryGreaterThan,
2111         );
2112         return summaries;
2113     }
2114 
2115     fn collectSiteScopeSummaries(
2116         self: *Analyzer,
2117         sort: Sort,
2118     ) !std.ArrayListUnmanaged(SiteScopeSummary) {
2119         var summaries = std.ArrayListUnmanaged(SiteScopeSummary).empty;
2120         var iterator = self.site_detail_scopes.iterator();
2121         while (iterator.next()) |entry| {
2122             try summaries.append(self.allocator, .{
2123                 .scope = entry.key_ptr.*,
2124                 .counters = entry.value_ptr.*,
2125             });
2126         }
2127         std.mem.sort(
2128             SiteScopeSummary,
2129             summaries.items,
2130             sort,
2131             siteScopeSummaryGreaterThan,
2132         );
2133         return summaries;
2134     }
2135 
2136     fn allocatorReleases(self: *Analyzer, allocator_id: u32) bool {
2137         const state = self.allocators.get(allocator_id) orelse return true;
2138         return state.retention == .releases_freed_memory;
2139     }
2140 
2141     fn allocatorLayer(
2142         self: *Analyzer,
2143         allocator_id: u32,
2144     ) event_mod.Layer {
2145         const state = self.allocators.get(allocator_id) orelse
2146             return .backing_boundary;
2147         return state.layer;
2148     }
2149 
2150     fn lifecycleCoverage(self: *Analyzer) LifecycleCoverage {
2151         var coverage: LifecycleCoverage = .{};
2152         var allocators = self.allocators.valueIterator();
2153         while (allocators.next()) |allocator| {
2154             if (allocator.layer != .logical_allocator) continue;
2155             if (allocator.retention == .releases_freed_memory) {
2156                 coverage.not_required +|= 1;
2157             } else if (allocator.lifecycle_instrumented) {
2158                 coverage.instrumented +|= 1;
2159             } else {
2160                 coverage.uninstrumented +|= 1;
2161             }
2162             if (allocator.prefix_complete) {
2163                 coverage.prefix_complete +|= 1;
2164             } else {
2165                 coverage.prefix_partial +|= 1;
2166             }
2167         }
2168         return coverage;
2169     }
2170 
2171     fn clearDrainedAllocations(self: *Analyzer) void {
2172         if (self.allocations.count() == 0) self.allocations.clearRetainingCapacity();
2173     }
2174 
2175     fn internScope(self: *Analyzer, scope: []const u8) ![]const u8 {
2176         const entry = try self.scopes.getOrPut(self.allocator, scope);
2177         if (!entry.found_existing) {
2178             const owned = try self.allocator.dupe(u8, scope);
2179             entry.key_ptr.* = owned;
2180             entry.value_ptr.* = .{};
2181         }
2182         return entry.key_ptr.*;
2183     }
2184 
2185     fn resolveScope(
2186         self: *const Analyzer,
2187         parsed: ParsedEvent,
2188     ) ![]const u8 {
2189         if (parsed.scope.len != 0) return parsed.scope;
2190         if (parsed.scope_id == 0) return "root";
2191         return self.scope_paths.get(parsed.scope_id) orelse
2192             error.MissingScopeDefinition;
2193     }
2194 };
2195 
2196 fn summarySnapshot(analyzer: *const Analyzer) Summary {
2197     const counters = analyzer.counters;
2198     return .{
2199         .events = analyzer.events,
2200         .allocations = counters.allocations,
2201         .frees = counters.frees,
2202         .resizes = counters.resizes,
2203         .remaps = counters.remaps,
2204         .live_allocations = @intCast(counters.live_allocations),
2205         .allocated_bytes = @intCast(counters.allocated_bytes),
2206         .freed_bytes = @intCast(counters.freed_bytes),
2207         .live_bytes = @intCast(counters.live_bytes),
2208         .high_water_live_bytes = @intCast(counters.high_water_live_bytes),
2209         .retained_bytes = @intCast(counters.retained_bytes),
2210         .high_water_retained_bytes = @intCast(
2211             counters.high_water_retained_bytes,
2212         ),
2213         .completed_lifetimes = counters.completed_lifetimes,
2214         .lifetime_total_events = counters.lifetime_total_events,
2215         .lifetime_mean_events = meanLifetimeEvents(counters),
2216         .lifetime_max_events = counters.lifetime_max_events,
2217         .lifetime_total_byte_events = counters.lifetime_total_byte_events,
2218         .lifetime_mean_byte_events = meanLifetimeByteEvents(counters),
2219         .lifetime_max_byte_events = counters.lifetime_max_byte_events,
2220         .failed_allocations = counters.failed_allocations,
2221         .failed_resizes = counters.failed_resizes,
2222         .failed_remaps = counters.failed_remaps,
2223         .unmatched_frees = counters.unmatched_frees,
2224         .unmatched_resizes = counters.unmatched_resizes,
2225     };
2226 }
2227 
2228 fn snapshotScopes(
2229     allocator: Allocator,
2230     summaries: []const ScopeSummary,
2231 ) ![]Scope {
2232     if (summaries.len == 0) return &.{};
2233     const result = try allocator.alloc(Scope, summaries.len);
2234     var initialized: usize = 0;
2235     errdefer {
2236         for (result[0..initialized]) |scope| allocator.free(scope.scope);
2237         allocator.free(result);
2238     }
2239     for (summaries, result) |summary, *scope| {
2240         scope.* = scopeSnapshot(
2241             try allocator.dupe(u8, summary.path),
2242             summary.counters,
2243         );
2244         initialized += 1;
2245     }
2246     return result;
2247 }
2248 
2249 fn snapshotSources(
2250     allocator: Allocator,
2251     summaries: []const SiteSummary,
2252     maybe_symbols: ?*const stack_mod.symbolize.Symbols,
2253 ) ![]Source {
2254     if (summaries.len == 0) return &.{};
2255     const result = try allocator.alloc(Source, summaries.len);
2256     errdefer allocator.free(result);
2257     for (summaries, result) |summary, *source| {
2258         const symbol = symbol: {
2259             const symbols = maybe_symbols orelse break :symbol null;
2260             const frames = symbols.find(summary.return_address);
2261             break :symbol if (frames.len == 0) null else frames[0];
2262         };
2263         source.* = sourceSnapshot(
2264             summary.return_address,
2265             if (symbol) |resolved| resolved.function else null,
2266             if (symbol) |resolved| resolved.location else null,
2267             summary.counters,
2268         );
2269     }
2270     return result;
2271 }
2272 
2273 fn scopeSnapshot(scope: []const u8, counters: anytype) Scope {
2274     return .{
2275         .scope = scope,
2276         .retained_bytes = @intCast(counters.retained_bytes),
2277         .high_water_retained_bytes = @intCast(
2278             counters.high_water_retained_bytes,
2279         ),
2280         .live_bytes = @intCast(counters.live_bytes),
2281         .high_water_live_bytes = @intCast(counters.high_water_live_bytes),
2282         .allocated_bytes = @intCast(counters.allocated_bytes),
2283         .freed_bytes = @intCast(counters.freed_bytes),
2284         .allocations = counters.allocations,
2285         .frees = counters.frees,
2286         .live_allocations = @intCast(counters.live_allocations),
2287         .completed_lifetimes = counters.completed_lifetimes,
2288         .lifetime_total_events = counters.lifetime_total_events,
2289         .lifetime_mean_events = meanLifetimeEvents(counters),
2290         .lifetime_max_events = counters.lifetime_max_events,
2291         .lifetime_total_byte_events = counters.lifetime_total_byte_events,
2292         .lifetime_mean_byte_events = meanLifetimeByteEvents(counters),
2293         .lifetime_max_byte_events = counters.lifetime_max_byte_events,
2294     };
2295 }
2296 
2297 fn sourceSnapshot(
2298     return_address: u64,
2299     function: ?[]const u8,
2300     location: ?[]const u8,
2301     counters: anytype,
2302 ) Source {
2303     const scope = scopeSnapshot("", counters);
2304     return .{
2305         .return_address = return_address,
2306         .function = function,
2307         .location = location,
2308         .retained_bytes = scope.retained_bytes,
2309         .high_water_retained_bytes = scope.high_water_retained_bytes,
2310         .live_bytes = scope.live_bytes,
2311         .high_water_live_bytes = scope.high_water_live_bytes,
2312         .allocated_bytes = scope.allocated_bytes,
2313         .freed_bytes = scope.freed_bytes,
2314         .allocations = scope.allocations,
2315         .frees = scope.frees,
2316         .live_allocations = scope.live_allocations,
2317         .completed_lifetimes = scope.completed_lifetimes,
2318         .lifetime_total_events = scope.lifetime_total_events,
2319         .lifetime_mean_events = scope.lifetime_mean_events,
2320         .lifetime_max_events = scope.lifetime_max_events,
2321         .lifetime_total_byte_events = scope.lifetime_total_byte_events,
2322         .lifetime_mean_byte_events = scope.lifetime_mean_byte_events,
2323         .lifetime_max_byte_events = scope.lifetime_max_byte_events,
2324     };
2325 }
2326 
2327 fn classifyCaptureIntegrity(result: *CaptureIntegrity) void {
2328     if (result.event_count == 0) return setIntegrity(
2329         result,
2330         "no_events",
2331         "capture_trace_events",
2332         "memtrace contains no events",
2333     );
2334     if (result.sequence_regression_count != 0) return setIntegrity(
2335         result,
2336         "non_monotonic_sequence",
2337         "inspect_trace_writer",
2338         "memtrace sequence is non-monotonic; treat the summary as corrupt evidence",
2339     );
2340     if (result.missing_sequence_event_count != 0) return setIntegrity(
2341         result,
2342         "sequence_gaps",
2343         "inspect_recorder_capacity_or_writer_failures",
2344         "memtrace sequence has gaps; treat the summary as partial evidence",
2345     );
2346     if (result.unsequenced_event_count != 0) return setIntegrity(
2347         result,
2348         "missing_sequence_metadata",
2349         "recapture_with_sequence_metadata",
2350         "one or more memtrace events lack sequence metadata; completeness is unknown",
2351     );
2352     classifyCaptureLifecycle(result);
2353 }
2354 
2355 fn classifyCaptureLifecycle(result: *CaptureIntegrity) void {
2356     if (result.start_event_count > 1 or result.stop_event_count > 1) {
2357         return setIntegrity(
2358             result,
2359             "multiple_trace_sessions",
2360             "capture_one_trace_session",
2361             "memtrace mixes multiple trace lifecycles; completeness is ambiguous",
2362         );
2363     }
2364     if (result.start_event_count == 0) return setIntegrity(
2365         result,
2366         "missing_start_event",
2367         "capture_complete_trace_lifecycle",
2368         "memtrace does not contain its start event; treat it as partial evidence",
2369     );
2370     if (result.stop_event_count == 0) return setIntegrity(
2371         result,
2372         "missing_stop_event",
2373         "capture_complete_trace_lifecycle",
2374         "memtrace does not contain its stop event; terminal state is partial evidence",
2375     );
2376     if (result.start_sequence != result.first_sequence or
2377         result.stop_sequence != result.last_sequence)
2378     {
2379         return setIntegrity(
2380             result,
2381             "lifecycle_not_bounded",
2382             "capture_complete_trace_lifecycle",
2383             "memtrace start and stop events do not bound the event sequence",
2384         );
2385     }
2386     if (result.unbalanced_event_count != 0) setIntegrity(
2387         result,
2388         "unbalanced_events",
2389         "inspect_allocation_and_scope_lifecycles",
2390         "memtrace contains unmatched allocation or scope lifecycle events",
2391     );
2392 }
2393 
2394 fn setIntegrity(
2395     result: *CaptureIntegrity,
2396     status: []const u8,
2397     action: []const u8,
2398     message: []const u8,
2399 ) void {
2400     result.status = status;
2401     result.action = action;
2402     result.message = message;
2403 }
2404 
2405 fn writeCounterFields(
2406     object: pretty_json.Object,
2407     counters: anytype,
2408     logical: bool,
2409 ) !void {
2410     if (logical) {
2411         try object.field("open_request_bytes", counters.live_bytes);
2412         try object.field(
2413             "high_water_open_request_bytes",
2414             counters.high_water_live_bytes,
2415         );
2416         try object.field(
2417             "bulk_invalidated_requests",
2418             counters.bulk_invalidated_requests,
2419         );
2420         try object.field(
2421             "bulk_invalidated_bytes",
2422             counters.bulk_invalidated_bytes,
2423         );
2424         try object.field("untracked_requests", counters.untracked_requests);
2425         try object.field(
2426             "untracked_request_bytes",
2427             counters.untracked_request_bytes,
2428         );
2429     } else {
2430         try object.field("retained_bytes", counters.retained_bytes);
2431         try object.field(
2432             "high_water_retained_bytes",
2433             counters.high_water_retained_bytes,
2434         );
2435         try object.field("live_bytes", counters.live_bytes);
2436         try object.field(
2437             "high_water_live_bytes",
2438             counters.high_water_live_bytes,
2439         );
2440     }
2441     try object.field("allocated_bytes", counters.allocated_bytes);
2442     if (logical) {
2443         try object.field("requested_bytes", counters.allocated_bytes);
2444         try object.field(
2445             "explicitly_closed_bytes",
2446             counters.freed_bytes,
2447         );
2448     } else {
2449         try object.field("freed_bytes", counters.freed_bytes);
2450     }
2451     try object.field("allocations", counters.allocations);
2452     try object.field("frees", counters.frees);
2453     if (logical) {
2454         try object.field("open_requests", counters.live_allocations);
2455     } else {
2456         try object.field("live_allocations", counters.live_allocations);
2457     }
2458     try object.field("completed_lifetimes", counters.completed_lifetimes);
2459     try object.field("lifetime_total_events", counters.lifetime_total_events);
2460     try object.field("lifetime_mean_events", meanLifetimeEvents(counters));
2461     try object.field("lifetime_max_events", counters.lifetime_max_events);
2462     try object.field(
2463         "lifetime_total_byte_events",
2464         counters.lifetime_total_byte_events,
2465     );
2466     try object.field(
2467         "lifetime_mean_byte_events",
2468         meanLifetimeByteEvents(counters),
2469     );
2470     try object.field(
2471         "lifetime_max_byte_events",
2472         counters.lifetime_max_byte_events,
2473     );
2474 }
2475 
2476 fn writeCaptureIntegrityText(
2477     writer: *std.Io.Writer,
2478     integrity: CaptureIntegrity,
2479 ) !void {
2480     try writer.print(
2481         "memtrace capture_integrity={s} events={d} sequenced_events={d} " ++
2482             "unsequenced_events={d} sequence_gaps={d} missing_sequence_events={d} " ++
2483             "sequence_regressions={d} start_events={d} stop_events={d} " ++
2484             "unbalanced_events={d}",
2485         .{
2486             integrity.status,
2487             integrity.event_count,
2488             integrity.sequenced_event_count,
2489             integrity.unsequenced_event_count,
2490             integrity.sequence_gap_count,
2491             integrity.missing_sequence_event_count,
2492             integrity.sequence_regression_count,
2493             integrity.start_event_count,
2494             integrity.stop_event_count,
2495             integrity.unbalanced_event_count,
2496         },
2497     );
2498     try writer.writeAll(" first_sequence=");
2499     try writeOptionalU64Text(writer, integrity.first_sequence);
2500     try writer.writeAll(" last_sequence=");
2501     try writeOptionalU64Text(writer, integrity.last_sequence);
2502     try writer.writeAll(" start_sequence=");
2503     try writeOptionalU64Text(writer, integrity.start_sequence);
2504     try writer.writeAll(" stop_sequence=");
2505     try writeOptionalU64Text(writer, integrity.stop_sequence);
2506     try writer.writeByte('\n');
2507     const message = integrity.message orelse return;
2508     try writer.print(
2509         "memtrace capture caveat={s} action={s} message=",
2510         .{ integrity.status, integrity.action },
2511     );
2512     var stream = pretty_json.Writer.init(writer, .minified);
2513     try stream.write(message);
2514     try writer.writeByte('\n');
2515 }
2516 
2517 fn writeCaptureIntegrityJson(
2518     object: pretty_json.Object,
2519     integrity: CaptureIntegrity,
2520 ) !void {
2521     const capture = try object.object("capture_integrity");
2522     try capture.field("method", capture_integrity_method);
2523     try capture.field("status", integrity.status);
2524     try capture.field("action", integrity.action);
2525     try capture.field("message", integrity.message);
2526     try capture.field("event_count", integrity.event_count);
2527     try capture.field("sequenced_event_count", integrity.sequenced_event_count);
2528     try capture.field("unsequenced_event_count", integrity.unsequenced_event_count);
2529     try capture.field("first_sequence", integrity.first_sequence);
2530     try capture.field("last_sequence", integrity.last_sequence);
2531     try capture.field("sequence_gap_count", integrity.sequence_gap_count);
2532     try capture.field("missing_sequence_event_count", integrity.missing_sequence_event_count);
2533     try capture.field("sequence_regression_count", integrity.sequence_regression_count);
2534     try capture.field("start_event_count", integrity.start_event_count);
2535     try capture.field("stop_event_count", integrity.stop_event_count);
2536     try capture.field("unbalanced_event_count", integrity.unbalanced_event_count);
2537     try capture.field("start_sequence", integrity.start_sequence);
2538     try capture.field("stop_sequence", integrity.stop_sequence);
2539     const limits = try capture.array("limits");
2540     try limits.element(
2541         "sequence gaps are lower-bound missing-row evidence",
2542     );
2543     try limits.element(
2544         "complete bounds do not prove whole-process coverage, low perturbation, or " ++
2545             "representative workload coverage",
2546     );
2547     try limits.end();
2548     try capture.end();
2549 }
2550 
2551 fn writeOptionalU64Text(writer: *std.Io.Writer, value: ?u64) !void {
2552     if (value) |actual| try writer.print("{d}", .{actual}) else try writer.writeAll("none");
2553 }
2554 
2555 pub fn writeSummaryFromJsonlPath(allocator: Allocator, path: []const u8, writer: *std.Io.Writer, options: SummaryOptions) !void {
2556     var analyzer = Analyzer.initForLayer(allocator, options.layer);
2557     defer analyzer.deinit();
2558     analyzer.track_sites = options.include_sites;
2559     analyzer.site_detail_return_address = options.site_detail_return_address;
2560     try ingestPath(&analyzer, path);
2561     try analyzer.writeSummary(writer, options);
2562 }
2563 
2564 pub fn writeSummaryJsonlFromJsonlPath(
2565     allocator: Allocator,
2566     path: []const u8,
2567     writer: *std.Io.Writer,
2568     options: SummaryOptions,
2569 ) !CaptureIntegrity {
2570     var analyzer = Analyzer.initForLayer(allocator, options.layer);
2571     defer analyzer.deinit();
2572     analyzer.track_sites = options.include_sites;
2573     analyzer.site_detail_return_address = options.site_detail_return_address;
2574     try ingestPath(&analyzer, path);
2575     try analyzer.writeSummaryJsonl(writer, options);
2576     return analyzer.captureIntegrity();
2577 }
2578 
2579 fn ingestPath(analyzer: *Analyzer, path: []const u8) !void {
2580     var file = try sys.fs.cwd().openFile(sys.fs.debugIo(), path, .{});
2581     defer file.close(sys.fs.debugIo());
2582 
2583     var buffer: [64 * 1024]u8 = undefined;
2584     var reader = file.reader(sys.fs.debugIo(), &buffer);
2585     while (true) {
2586         const line = reader.interface.takeDelimiter('\n') catch |err| switch (err) {
2587             error.ReadFailed => return reader.err.?,
2588             else => return err,
2589         };
2590         const actual = line orelse break;
2591         try analyzer.ingestJsonLine(actual);
2592     }
2593 }
2594 
2595 fn allocationKey(allocation_id: u64, address: u64) u64 {
2596     if (allocation_id != 0) return allocation_id;
2597     return address;
2598 }
2599 
2600 fn completedLifetimeEvents(allocation_event: u64, free_event: u64) u64 {
2601     if (free_event <= allocation_event) return 0;
2602     return free_event - allocation_event;
2603 }
2604 
2605 fn completedLifetimeByteEvents(
2606     len: usize,
2607     allocation_event: u64,
2608     free_event: u64,
2609 ) u64 {
2610     const events = completedLifetimeEvents(allocation_event, free_event);
2611     return @as(u64, @intCast(len)) *| events;
2612 }
2613 
2614 fn accrueLifetimeByteEvents(record: *AllocationRecord, event: u64) void {
2615     record.byte_events +|= completedLifetimeByteEvents(
2616         record.len,
2617         record.segment_event,
2618         event,
2619     );
2620     record.segment_event = event;
2621 }
2622 
2623 fn applyCompletedLifetime(
2624     counters: anytype,
2625     lifetime_events: u64,
2626     lifetime_byte_events: u64,
2627 ) void {
2628     counters.completed_lifetimes +|= 1;
2629     counters.lifetime_total_events +|= lifetime_events;
2630     counters.lifetime_max_events = @max(counters.lifetime_max_events, lifetime_events);
2631     counters.lifetime_total_byte_events +|= lifetime_byte_events;
2632     counters.lifetime_max_byte_events = @max(
2633         counters.lifetime_max_byte_events,
2634         lifetime_byte_events,
2635     );
2636 }
2637 
2638 fn applyCountersLostTracking(counters: anytype, len: usize) void {
2639     if (counters.live_allocations > 0) counters.live_allocations -= 1;
2640     if (counters.live_bytes >= len) {
2641         counters.live_bytes -= len;
2642     } else {
2643         counters.live_bytes = 0;
2644     }
2645     counters.untracked_requests +|= 1;
2646     counters.untracked_request_bytes +|= len;
2647 }
2648 
2649 fn meanLifetimeEvents(counters: anytype) u64 {
2650     if (counters.completed_lifetimes == 0) return 0;
2651     return counters.lifetime_total_events / counters.completed_lifetimes;
2652 }
2653 
2654 fn meanLifetimeByteEvents(counters: anytype) u64 {
2655     if (counters.completed_lifetimes == 0) return 0;
2656     return counters.lifetime_total_byte_events / counters.completed_lifetimes;
2657 }
2658 
2659 test "escaped canonical scope uses generic replay" {
2660     var bytes = std.Io.Writer.Allocating.init(std.testing.allocator);
2661     defer bytes.deinit();
2662     try (event_mod.Event{
2663         .seq = 1,
2664         .kind = .alloc,
2665         .allocation_id = 1,
2666         .address = 4096,
2667         .len = 32,
2668     }).writeJsonLine(&bytes.writer, null, "root/quoted\"scope");
2669 
2670     var analyzer = Analyzer.init(std.testing.allocator);
2671     defer analyzer.deinit();
2672     try analyzer.ingestJsonlBytes(bytes.written());
2673 
2674     var summary = std.Io.Writer.Allocating.init(std.testing.allocator);
2675     defer summary.deinit();
2676     try analyzer.writeSummary(&summary.writer, .{});
2677     try std.testing.expect(std.mem.indexOf(u8, summary.written(), "root/quoted\"scope retained_bytes=32") != null);
2678 }
2679 
2680 test "event analysis resolves scope ids from scope entry events" {
2681     const events =
2682         "{\"v\":3,\"seq\":1,\"kind\":\"trace.start\"}\n" ++
2683         "{\"v\":3,\"seq\":2,\"kind\":\"scope.enter\",\"scope_id\":1," ++
2684         "\"scope\":\"root/phase\"}\n" ++
2685         "{\"v\":3,\"seq\":3,\"kind\":\"alloc\",\"allocation_id\":1," ++
2686         "\"scope_id\":1,\"address\":4096,\"len\":32}\n" ++
2687         "{\"v\":3,\"seq\":4,\"kind\":\"free\",\"allocation_id\":1," ++
2688         "\"scope_id\":1,\"address\":4096,\"old_len\":32}\n" ++
2689         "{\"v\":3,\"seq\":5,\"kind\":\"scope.exit\",\"scope_id\":1}\n" ++
2690         "{\"v\":3,\"seq\":6,\"kind\":\"trace.stop\"}\n";
2691     var analyzer = Analyzer.init(std.testing.allocator);
2692     defer analyzer.deinit();
2693     try analyzer.ingestJsonlBytes(events);
2694 
2695     var summary = std.Io.Writer.Allocating.init(std.testing.allocator);
2696     defer summary.deinit();
2697     try analyzer.writeSummary(
2698         &summary.writer,
2699         .{ .include_zero_live = true },
2700     );
2701     try std.testing.expect(
2702         std.mem.indexOf(
2703             u8,
2704             summary.written(),
2705             "root/phase retained_bytes=0 high_water_retained_bytes=32",
2706         ) != null,
2707     );
2708 }
2709 
2710 fn scopeSummaryGreaterThan(
2711     sort: Sort,
2712     left: ScopeSummary,
2713     right: ScopeSummary,
2714 ) bool {
2715     if (counterOrder(sort, left.counters, right.counters)) |order| return order;
2716     return std.mem.lessThan(u8, left.path, right.path);
2717 }
2718 
2719 fn siteSummaryGreaterThan(
2720     sort: Sort,
2721     left: SiteSummary,
2722     right: SiteSummary,
2723 ) bool {
2724     if (counterOrder(sort, left.counters, right.counters)) |order| return order;
2725     return left.return_address < right.return_address;
2726 }
2727 
2728 fn siteSizeSummaryGreaterThan(
2729     sort: Sort,
2730     left: SiteSizeSummary,
2731     right: SiteSizeSummary,
2732 ) bool {
2733     if (counterOrder(sort, left.counters, right.counters)) |order| return order;
2734     return left.len < right.len;
2735 }
2736 
2737 fn siteScopeSummaryGreaterThan(
2738     sort: Sort,
2739     left: SiteScopeSummary,
2740     right: SiteScopeSummary,
2741 ) bool {
2742     if (counterOrder(sort, left.counters, right.counters)) |order| return order;
2743     return std.mem.lessThan(u8, left.scope, right.scope);
2744 }
2745 
2746 fn counterOrder(sort: Sort, left: anytype, right: @TypeOf(left)) ?bool {
2747     switch (sort) {
2748         .retained => {
2749             if (left.retained_bytes != right.retained_bytes) {
2750                 return left.retained_bytes > right.retained_bytes;
2751             }
2752             if (left.high_water_retained_bytes !=
2753                 right.high_water_retained_bytes)
2754             {
2755                 return left.high_water_retained_bytes >
2756                     right.high_water_retained_bytes;
2757             }
2758         },
2759         .traffic => {
2760             if (left.allocated_bytes != right.allocated_bytes) {
2761                 return left.allocated_bytes > right.allocated_bytes;
2762             }
2763             if (left.allocations != right.allocations) {
2764                 return left.allocations > right.allocations;
2765             }
2766         },
2767         .lifetime => {
2768             if (left.lifetime_total_byte_events !=
2769                 right.lifetime_total_byte_events)
2770             {
2771                 return left.lifetime_total_byte_events >
2772                     right.lifetime_total_byte_events;
2773             }
2774             if (left.lifetime_total_events != right.lifetime_total_events) {
2775                 return left.lifetime_total_events >
2776                     right.lifetime_total_events;
2777             }
2778             if (left.lifetime_max_events != right.lifetime_max_events) {
2779                 return left.lifetime_max_events > right.lifetime_max_events;
2780             }
2781             if (left.completed_lifetimes != right.completed_lifetimes) {
2782                 return left.completed_lifetimes > right.completed_lifetimes;
2783             }
2784         },
2785     }
2786     if (left.retained_bytes != right.retained_bytes) {
2787         return left.retained_bytes > right.retained_bytes;
2788     }
2789     if (left.high_water_retained_bytes != right.high_water_retained_bytes) {
2790         return left.high_water_retained_bytes >
2791             right.high_water_retained_bytes;
2792     }
2793     if (left.allocated_bytes != right.allocated_bytes) {
2794         return left.allocated_bytes > right.allocated_bytes;
2795     }
2796     if (left.allocations != right.allocations) {
2797         return left.allocations > right.allocations;
2798     }
2799     if (left.lifetime_total_byte_events !=
2800         right.lifetime_total_byte_events)
2801     {
2802         return left.lifetime_total_byte_events >
2803             right.lifetime_total_byte_events;
2804     }
2805     if (left.lifetime_total_events != right.lifetime_total_events) {
2806         return left.lifetime_total_events > right.lifetime_total_events;
2807     }
2808     if (left.lifetime_max_events != right.lifetime_max_events) {
2809         return left.lifetime_max_events > right.lifetime_max_events;
2810     }
2811     if (left.completed_lifetimes != right.completed_lifetimes) {
2812         return left.completed_lifetimes > right.completed_lifetimes;
2813     }
2814     if (left.live_bytes != right.live_bytes) {
2815         return left.live_bytes > right.live_bytes;
2816     }
2817     if (left.high_water_live_bytes != right.high_water_live_bytes) {
2818         return left.high_water_live_bytes > right.high_water_live_bytes;
2819     }
2820     return null;
2821 }
2822 
2823 test "allocation summary sort separates retention traffic and lifetime" {
2824     const retained = ScopeSummary{
2825         .path = "retained",
2826         .counters = .{
2827             .retained_bytes = 128,
2828             .allocated_bytes = 16,
2829             .lifetime_total_events = 8,
2830         },
2831     };
2832     const traffic = ScopeSummary{
2833         .path = "traffic",
2834         .counters = .{
2835             .retained_bytes = 32,
2836             .allocated_bytes = 256,
2837             .lifetime_total_events = 4,
2838         },
2839     };
2840     const lifetime = ScopeSummary{
2841         .path = "lifetime",
2842         .counters = .{
2843             .retained_bytes = 16,
2844             .allocated_bytes = 32,
2845             .lifetime_total_events = 512,
2846         },
2847     };
2848     try std.testing.expect(scopeSummaryGreaterThan(.retained, retained, traffic));
2849     try std.testing.expect(scopeSummaryGreaterThan(.traffic, traffic, retained));
2850     try std.testing.expect(scopeSummaryGreaterThan(.lifetime, lifetime, retained));
2851     inline for (std.meta.tags(Sort)) |sort| {
2852         try std.testing.expect(!scopeSummaryGreaterThan(sort, retained, retained));
2853     }
2854     try std.testing.expectEqual(Sort.retained, Sort.parse("retained").?);
2855     try std.testing.expectEqual(Sort.traffic, Sort.parse("traffic").?);
2856     try std.testing.expectEqual(Sort.lifetime, Sort.parse("lifetime").?);
2857     try std.testing.expect(Sort.parse("bytes") == null);
2858 }
2859 
2860 fn resolveSiteSummarySymbolsAlloc(
2861     allocator: Allocator,
2862     binary_path: []const u8,
2863     summaries: []const SiteSummary,
2864 ) !?stack_mod.symbolize.Symbols {
2865     if (summaries.len == 0) return null;
2866     const addresses = try allocator.alloc(u64, summaries.len);
2867     defer allocator.free(addresses);
2868     for (summaries, 0..) |summary, index| {
2869         addresses[index] = summary.return_address;
2870     }
2871     return try stack_mod.symbolize.resolveAlloc(
2872         allocator,
2873         binary_path,
2874         addresses,
2875     );
2876 }
2877 
2878 fn resolveSiteAddressesAlloc(
2879     allocator: Allocator,
2880     binary_path: []const u8,
2881     addresses: []const u64,
2882 ) !?stack_mod.symbolize.Symbols {
2883     if (addresses.len == 0) return null;
2884     return try stack_mod.symbolize.resolveAlloc(
2885         allocator,
2886         binary_path,
2887         addresses,
2888     );
2889 }
2890 
2891 test "event analysis preserves retained allocator pressure" {
2892     var tracer = try tracer_mod.Tracer.init(std.testing.allocator, .{ .record_events = true });
2893     defer tracer.deinit();
2894     var traced = try tracer.tracedAllocatorWithOptions(std.testing.allocator, .{
2895         .name = "arena",
2896         .retention = .retains_freed_memory,
2897     });
2898     const allocator = traced.allocator();
2899 
2900     var scope = try tracer.enter("phase");
2901     const bytes = try allocator.alloc(u8, 64);
2902     allocator.free(bytes);
2903     scope.exit();
2904 
2905     var events = std.Io.Writer.Allocating.init(std.testing.allocator);
2906     defer events.deinit();
2907     try tracer.writeEventsJsonl(&events.writer);
2908 
2909     var analyzer = Analyzer.init(std.testing.allocator);
2910     defer analyzer.deinit();
2911     try analyzer.ingestJsonlBytes(events.written());
2912     const integrity = analyzer.captureIntegrity();
2913     try std.testing.expectEqualStrings("complete", integrity.status);
2914     try std.testing.expectEqual(@as(?u64, 1), integrity.first_sequence);
2915     try std.testing.expectEqual(integrity.first_sequence, integrity.start_sequence);
2916     try std.testing.expectEqual(integrity.last_sequence, integrity.stop_sequence);
2917 
2918     var out = std.Io.Writer.Allocating.init(std.testing.allocator);
2919     defer out.deinit();
2920     try analyzer.writeSummary(&out.writer, .{ .top = 8, .include_zero_live = true });
2921     const text = out.written();
2922     try std.testing.expect(std.mem.indexOf(u8, text, "retained_bytes=64") != null);
2923     try std.testing.expect(std.mem.indexOf(u8, text, "live_bytes=0") != null);
2924     try std.testing.expect(std.mem.indexOf(u8, text, "root/phase retained_bytes=64") != null);
2925 }
2926 
2927 test "event analysis separates resize and remap event counts" {
2928     var analyzer = Analyzer.init(std.testing.allocator);
2929     defer analyzer.deinit();
2930     var events = std.Io.Writer.Allocating.init(std.testing.allocator);
2931     defer events.deinit();
2932     const rows = [_]event_mod.Event{
2933         .{ .seq = 1, .kind = .trace_start },
2934         .{ .seq = 2, .kind = .allocator, .allocator_id = 0 },
2935         .{
2936             .seq = 3,
2937             .kind = .alloc,
2938             .allocation_id = 1,
2939             .address = 100,
2940             .len = 16,
2941         },
2942         .{
2943             .seq = 4,
2944             .kind = .resize,
2945             .allocation_id = 1,
2946             .address = 100,
2947             .old_len = 16,
2948             .len = 24,
2949         },
2950         .{
2951             .seq = 5,
2952             .kind = .remap,
2953             .allocation_id = 1,
2954             .old_address = 100,
2955             .address = 200,
2956             .old_len = 24,
2957             .len = 32,
2958         },
2959         .{ .seq = 6, .kind = .trace_stop },
2960     };
2961     for (rows) |row| try row.writeJsonLine(&events.writer, null, null);
2962     try analyzer.ingestJsonlBytes(events.written());
2963     var output: [8 * 1024]u8 = undefined;
2964     var writer = std.Io.Writer.fixed(&output);
2965     try analyzer.writeSummaryJsonl(&writer, .{ .top = 0 });
2966     try std.testing.expect(
2967         std.mem.indexOf(u8, writer.buffered(), "\"resizes\":1,\"remaps\":1") != null,
2968     );
2969 }
2970 
2971 test "event analysis classifies release-explicit lifecycle as not required" {
2972     var events = std.Io.Writer.Allocating.init(std.testing.allocator);
2973     defer events.deinit();
2974     const rows = [_]event_mod.Event{
2975         .{ .seq = 1, .kind = .trace_start },
2976         .{
2977             .seq = 2,
2978             .kind = .allocator,
2979             .allocator_id = 1,
2980             .retains_freed_memory = true,
2981             .layer = .logical_allocator,
2982             .lifecycle_instrumented = true,
2983         },
2984         .{
2985             .seq = 3,
2986             .kind = .allocator,
2987             .allocator_id = 2,
2988             .retains_freed_memory = false,
2989             .layer = .logical_allocator,
2990         },
2991         .{ .seq = 4, .kind = .trace_stop },
2992     };
2993     for (rows) |row| try row.writeJsonLine(&events.writer, null, null);
2994     var analyzer = Analyzer.initForLayer(std.testing.allocator, .logical);
2995     defer analyzer.deinit();
2996     try analyzer.ingestJsonlBytes(events.written());
2997 
2998     var summary = std.Io.Writer.Allocating.init(std.testing.allocator);
2999     defer summary.deinit();
3000     try analyzer.writeSummaryJsonl(&summary.writer, .{
3001         .layer = .logical,
3002     });
3003     try std.testing.expect(std.mem.indexOf(
3004         u8,
3005         summary.written(),
3006         "\"lifecycle_coverage\":\"complete\"",
3007     ) != null);
3008     try std.testing.expect(std.mem.indexOf(
3009         u8,
3010         summary.written(),
3011         "\"lifecycle_instrumented_producers\":1",
3012     ) != null);
3013     try std.testing.expect(std.mem.indexOf(
3014         u8,
3015         summary.written(),
3016         "\"lifecycle_uninstrumented_producers\":0",
3017     ) != null);
3018     try std.testing.expect(std.mem.indexOf(
3019         u8,
3020         summary.written(),
3021         "\"lifecycle_not_required_producers\":1",
3022     ) != null);
3023     try std.testing.expect(std.mem.indexOf(
3024         u8,
3025         summary.written(),
3026         "\"prefix_complete_producers\":2",
3027     ) != null);
3028 }
3029 
3030 test "event replay matches failed known and lost-tracking remaps" {
3031     if (!observe.enabled or !sys.memory.observe.enabled) {
3032         return error.SkipZigTest;
3033     }
3034     var tracer = try tracer_mod.Tracer.init(std.testing.allocator, .{
3035         .record_events = true,
3036     });
3037     defer tracer.deinit();
3038     var observation = try tracer.observeOwnedAllocators();
3039     defer observation.stop();
3040     const identity = observe.Identity{
3041         .producer_id = observe.producerId(),
3042         .producer = .arena,
3043         .generation = 0,
3044         .owner_cookie = 0xcafe,
3045     };
3046 
3047     var first = observe.beginOwned(
3048         identity,
3049         .alloc,
3050         0,
3051         0,
3052         64,
3053         8,
3054         @returnAddress(),
3055     );
3056     first.finish(.{
3057         .address = 0x1000,
3058         .succeeded = true,
3059     });
3060     var second = observe.beginOwned(
3061         identity,
3062         .alloc,
3063         0,
3064         0,
3065         32,
3066         8,
3067         @returnAddress(),
3068     );
3069     second.finish(.{
3070         .address = 0x2000,
3071         .succeeded = true,
3072     });
3073     var failed_known = observe.beginOwned(
3074         identity,
3075         .remap,
3076         0x1000,
3077         64,
3078         80,
3079         8,
3080         @returnAddress(),
3081     );
3082     failed_known.finish(.{
3083         .address = 0,
3084         .succeeded = false,
3085     });
3086     var collision = observe.beginOwned(
3087         identity,
3088         .remap,
3089         0x1000,
3090         64,
3091         96,
3092         8,
3093         @returnAddress(),
3094     );
3095     collision.finish(.{
3096         .address = 0x2000,
3097         .succeeded = true,
3098     });
3099     observation.stop();
3100 
3101     const live = tracer.snapshotLayer(.logical_allocator);
3102     var live_summary = std.Io.Writer.Allocating.init(
3103         std.testing.allocator,
3104     );
3105     defer live_summary.deinit();
3106     try tracer.writeSummaryJsonl(&live_summary.writer, .{
3107         .layer = .logical_allocator,
3108         .include_zero_live = true,
3109     });
3110     try std.testing.expect(std.mem.indexOf(
3111         u8,
3112         live_summary.written(),
3113         "\"resizes\":1,\"remaps\":0",
3114     ) != null);
3115     try std.testing.expect(std.mem.indexOf(
3116         u8,
3117         live_summary.written(),
3118         "\"untracked_requests\":1,\"untracked_request_bytes\":96",
3119     ) != null);
3120     try std.testing.expect(std.mem.indexOf(
3121         u8,
3122         live_summary.written(),
3123         "\"failed_remaps\":1,\"unmatched_frees\":0," ++
3124             "\"unmatched_resizes\":0,\"unmatched_remaps\":0",
3125     ) != null);
3126 
3127     var emitted = std.Io.Writer.Allocating.init(std.testing.allocator);
3128     defer emitted.deinit();
3129     try tracer.writeEventsJsonl(&emitted.writer);
3130     var replay = Analyzer.initForLayer(std.testing.allocator, .logical);
3131     defer replay.deinit();
3132     try replay.ingestJsonlBytes(emitted.written());
3133 
3134     try std.testing.expectEqual(live.allocations, replay.counters.allocations);
3135     try std.testing.expectEqual(live.frees, replay.counters.frees);
3136     try std.testing.expectEqual(
3137         live.live_allocations,
3138         replay.counters.live_allocations,
3139     );
3140     try std.testing.expectEqual(
3141         live.allocated_bytes,
3142         replay.counters.allocated_bytes,
3143     );
3144     try std.testing.expectEqual(
3145         live.freed_bytes,
3146         replay.counters.freed_bytes,
3147     );
3148     try std.testing.expectEqual(live.live_bytes, replay.counters.live_bytes);
3149     try std.testing.expectEqual(
3150         live.high_water_live_bytes,
3151         replay.counters.high_water_live_bytes,
3152     );
3153     try std.testing.expectEqual(@as(u64, 1), replay.counters.resizes);
3154     try std.testing.expectEqual(@as(u64, 0), replay.counters.remaps);
3155     try std.testing.expectEqual(@as(u64, 1), replay.counters.failed_remaps);
3156     try std.testing.expectEqual(@as(u64, 0), replay.counters.unmatched_remaps);
3157     try std.testing.expectEqual(
3158         @as(u64, 1),
3159         replay.counters.untracked_requests,
3160     );
3161     try std.testing.expectEqual(
3162         @as(usize, 96),
3163         replay.counters.untracked_request_bytes,
3164     );
3165     try std.testing.expectEqual(@as(usize, 1), replay.allocations.count());
3166 }
3167 
3168 test "event analysis classifies sequence loss and tail truncation" {
3169     var gapped = Analyzer.init(std.testing.allocator);
3170     defer gapped.deinit();
3171     try gapped.ingestJsonlBytes(
3172         "{\"v\":3,\"seq\":1,\"kind\":\"trace.start\"}\n" ++
3173             "{\"v\":3,\"seq\":3,\"kind\":\"allocator\",\"allocator_id\":0}\n" ++
3174             "{\"v\":3,\"seq\":4,\"kind\":\"trace.stop\"}\n",
3175     );
3176     const gap_integrity = gapped.captureIntegrity();
3177     try std.testing.expectEqualStrings("sequence_gaps", gap_integrity.status);
3178     try std.testing.expectEqual(@as(u64, 1), gap_integrity.sequence_gap_count);
3179     try std.testing.expectEqual(@as(u64, 1), gap_integrity.missing_sequence_event_count);
3180 
3181     var truncated = Analyzer.init(std.testing.allocator);
3182     defer truncated.deinit();
3183     try truncated.ingestJsonlBytes(
3184         "{\"v\":3,\"seq\":1,\"kind\":\"trace.start\"}\n" ++
3185             "{\"v\":3,\"seq\":2,\"kind\":\"allocator\",\"allocator_id\":0}\n",
3186     );
3187     const tail_integrity = truncated.captureIntegrity();
3188     try std.testing.expectEqualStrings("missing_stop_event", tail_integrity.status);
3189     try std.testing.expectEqual(@as(u64, 0), tail_integrity.sequence_gap_count);
3190 }
3191 
3192 test "event analysis reports allocation sites" {
3193     var analyzer = Analyzer.init(std.testing.allocator);
3194     defer analyzer.deinit();
3195 
3196     try analyzer.ingestJsonLine(
3197         "{\"v\":3,\"kind\":\"allocator\",\"allocator_id\":0,\"retains_freed_memory\":true}",
3198     );
3199     try analyzer.ingestJsonLine(
3200         "{\"v\":3,\"kind\":\"alloc\",\"allocator_id\":0,\"allocation_id\":1,\"address\":4096,\"len\":64,\"return_address\":2748,\"scope\":\"root/phase\"}",
3201     );
3202     try analyzer.ingestJsonLine(
3203         "{\"v\":3,\"kind\":\"free\",\"allocator_id\":0,\"allocation_id\":1,\"address\":4096,\"len\":64,\"return_address\":8192,\"scope\":\"root/phase\"}",
3204     );
3205 
3206     var out = std.Io.Writer.Allocating.init(std.testing.allocator);
3207     defer out.deinit();
3208     try analyzer.writeSummary(&out.writer, .{ .top = 8, .include_sites = true });
3209     const text = out.written();
3210     try std.testing.expect(std.mem.indexOf(u8, text, "site return_address=0xabc retained_bytes=64") != null);
3211     try std.testing.expect(std.mem.indexOf(u8, text, "freed_bytes=64") != null);
3212 }
3213 
3214 test "event analysis reports site detail by size and scope" {
3215     var analyzer = Analyzer.init(std.testing.allocator);
3216     defer analyzer.deinit();
3217     analyzer.site_detail_return_address = 0xabc;
3218 
3219     try analyzer.ingestJsonLine(
3220         "{\"v\":3,\"kind\":\"allocator\",\"allocator_id\":0,\"retains_freed_memory\":true}",
3221     );
3222     try analyzer.ingestJsonLine(
3223         "{\"v\":3,\"kind\":\"alloc\",\"allocator_id\":0,\"allocation_id\":1,\"address\":4096,\"len\":64,\"return_address\":2748,\"scope\":\"root/left\"}",
3224     );
3225     try analyzer.ingestJsonLine(
3226         "{\"v\":3,\"kind\":\"alloc\",\"allocator_id\":0,\"allocation_id\":2,\"address\":8192,\"len\":128,\"return_address\":2748,\"scope\":\"root/right\"}",
3227     );
3228     try analyzer.ingestJsonLine(
3229         "{\"v\":3,\"kind\":\"free\",\"allocator_id\":0,\"allocation_id\":1,\"address\":4096,\"len\":64,\"return_address\":8192,\"scope\":\"root/left\"}",
3230     );
3231 
3232     var out = std.Io.Writer.Allocating.init(std.testing.allocator);
3233     defer out.deinit();
3234     try analyzer.writeSummary(&out.writer, .{ .top = 8, .site_detail_return_address = 0xabc });
3235     const text = out.written();
3236     try std.testing.expect(std.mem.indexOf(u8, text, "site_detail return_address=0xabc") != null);
3237     try std.testing.expect(std.mem.indexOf(u8, text, "site_size len=128 retained_bytes=128") != null);
3238     try std.testing.expect(std.mem.indexOf(u8, text, "site_size len=64 retained_bytes=64") != null);
3239     try std.testing.expect(std.mem.indexOf(u8, text, "site_scope scope=\"root/right\" retained_bytes=128") != null);
3240     try std.testing.expect(std.mem.indexOf(u8, text, "site_scope scope=\"root/left\" retained_bytes=64") != null);
3241 }
3242 
3243 test "event analysis reports completed allocation lifetimes" {
3244     var analyzer = Analyzer.init(std.testing.allocator);
3245     defer analyzer.deinit();
3246 
3247     try analyzer.ingestJsonLine(
3248         "{\"v\":3,\"kind\":\"allocator\",\"allocator_id\":0,\"retains_freed_memory\":false}",
3249     );
3250     try analyzer.ingestJsonLine(
3251         "{\"v\":3,\"kind\":\"alloc\",\"allocator_id\":0,\"allocation_id\":1,\"address\":4096,\"len\":64,\"return_address\":2748,\"scope\":\"root/phase\"}",
3252     );
3253     try analyzer.ingestJsonLine(
3254         "{\"v\":3,\"kind\":\"resize\",\"allocator_id\":0,\"allocation_id\":1,\"address\":4096,\"old_len\":64,\"len\":80,\"return_address\":2748,\"scope\":\"root/phase\"}",
3255     );
3256     try analyzer.ingestJsonLine(
3257         "{\"v\":3,\"kind\":\"alloc\",\"allocator_id\":0,\"allocation_id\":2,\"address\":8192,\"len\":32,\"return_address\":3567,\"scope\":\"root/temp\"}",
3258     );
3259     try analyzer.ingestJsonLine(
3260         "{\"v\":3,\"kind\":\"free\",\"allocator_id\":0,\"allocation_id\":2,\"address\":8192,\"len\":32,\"return_address\":3567,\"scope\":\"root/temp\"}",
3261     );
3262     try analyzer.ingestJsonLine(
3263         "{\"v\":3,\"kind\":\"free\",\"allocator_id\":0,\"allocation_id\":1,\"address\":4096,\"len\":80,\"return_address\":2748,\"scope\":\"root/phase\"}",
3264     );
3265 
3266     var out = std.Io.Writer.Allocating.init(std.testing.allocator);
3267     defer out.deinit();
3268     try analyzer.writeSummary(&out.writer, .{ .top = 8, .include_sites = true, .include_zero_live = true });
3269     const text = out.written();
3270     try std.testing.expect(std.mem.indexOf(u8, text, "memtrace lifetimes completed=2 total_events=5 mean_events=2 max_events=4") != null);
3271     try std.testing.expect(std.mem.indexOf(u8, text, "completed_lifetimes=1 lifetime_total_events=4 lifetime_mean_events=4 lifetime_max_events=4") != null);
3272     try std.testing.expect(std.mem.indexOf(u8, text, "completed_lifetimes=1 lifetime_total_events=1 lifetime_mean_events=1 lifetime_max_events=1") != null);
3273     try std.testing.expect(std.mem.indexOf(
3274         u8,
3275         text,
3276         "total_byte_events=336 mean_byte_events=168 max_byte_events=304",
3277     ) != null);
3278 
3279     var jsonl = std.Io.Writer.Allocating.init(std.testing.allocator);
3280     defer jsonl.deinit();
3281     try analyzer.writeSummaryJsonl(&jsonl.writer, .{ .top = 8, .include_sites = true, .include_zero_live = true });
3282     const jsonl_text = jsonl.written();
3283     try std.testing.expect(std.mem.indexOf(u8, jsonl_text, "\"completed_lifetimes\":2") != null);
3284     try std.testing.expect(std.mem.indexOf(u8, jsonl_text, "\"lifetime_total_events\":5") != null);
3285     try std.testing.expect(std.mem.indexOf(u8, jsonl_text, "\"lifetime_max_events\":4") != null);
3286     try std.testing.expect(std.mem.indexOf(
3287         u8,
3288         jsonl_text,
3289         "\"lifetime_total_byte_events\":336",
3290     ) != null);
3291     try std.testing.expect(std.mem.indexOf(
3292         u8,
3293         jsonl_text,
3294         "\"kind\":\"scope\",\"sort\":\"retained\"",
3295     ) != null);
3296     try std.testing.expect(std.mem.indexOf(
3297         u8,
3298         jsonl_text,
3299         "\"kind\":\"site\",\"sort\":\"retained\"",
3300     ) != null);
3301 
3302     var snapshot_value = try analyzer.snapshot(std.testing.allocator, .{
3303         .top = 8,
3304         .include_zero_live = true,
3305     });
3306     defer snapshot_value.deinit();
3307     try std.testing.expectEqual(
3308         @as(u64, 336),
3309         snapshot_value.summary.lifetime_total_byte_events,
3310     );
3311     try std.testing.expectEqual(
3312         @as(u64, 304),
3313         snapshot_value.summary.lifetime_max_byte_events,
3314     );
3315     inline for (std.meta.tags(Sort)) |sort| {
3316         const ranking = snapshot_value.rankings.get(sort);
3317         try std.testing.expectEqual(@as(usize, 2), ranking.scopes.len);
3318         try std.testing.expectEqual(@as(usize, 2), ranking.sources.len);
3319     }
3320     try std.testing.expectEqualStrings(
3321         "root/phase",
3322         snapshot_value.rankings.lifetime.scopes[0].scope,
3323     );
3324     try std.testing.expectEqual(
3325         @as(u64, 304),
3326         snapshot_value.rankings.lifetime.scopes[0]
3327             .lifetime_total_byte_events,
3328     );
3329 }
3330 
3331 test "event analysis separates backing logical and failed allocations" {
3332     const events =
3333         "{\"v\":3,\"kind\":\"allocator\",\"allocator_id\":0," ++
3334         "\"layer\":\"backing_boundary\"}\n" ++
3335         "{\"v\":3,\"kind\":\"allocator\",\"allocator_id\":1," ++
3336         "\"layer\":\"logical_allocator\",\"producer\":\"arena\"}\n" ++
3337         "{\"v\":3,\"kind\":\"alloc\",\"allocator_id\":0," ++
3338         "\"allocation_id\":1,\"address\":4096,\"len\":128," ++
3339         "\"scope\":\"root\",\"layer\":\"backing_boundary\"}\n" ++
3340         "{\"v\":3,\"kind\":\"alloc\",\"allocator_id\":1," ++
3341         "\"allocation_id\":2,\"address\":8192,\"len\":16," ++
3342         "\"scope\":\"root\",\"layer\":\"logical_allocator\"," ++
3343         "\"producer\":\"arena\"}\n" ++
3344         "{\"v\":3,\"kind\":\"alloc\",\"allocator_id\":1,\"len\":64," ++
3345         "\"succeeded\":false,\"scope\":\"root\"," ++
3346         "\"layer\":\"logical_allocator\",\"producer\":\"arena\"}\n";
3347 
3348     var backing = Analyzer.initForLayer(std.testing.allocator, .backing);
3349     defer backing.deinit();
3350     try backing.ingestJsonlBytes(events);
3351     var backing_summary = std.Io.Writer.Allocating.init(
3352         std.testing.allocator,
3353     );
3354     defer backing_summary.deinit();
3355     try backing.writeSummary(&backing_summary.writer, .{});
3356     try std.testing.expect(std.mem.indexOf(
3357         u8,
3358         backing_summary.written(),
3359         "layer=backing sort=retained events=5 allocations=1",
3360     ) != null);
3361     try std.testing.expect(std.mem.indexOf(
3362         u8,
3363         backing_summary.written(),
3364         "allocated_bytes=128",
3365     ) != null);
3366 
3367     var logical = Analyzer.initForLayer(std.testing.allocator, .logical);
3368     defer logical.deinit();
3369     try logical.ingestJsonlBytes(events);
3370     var logical_summary = std.Io.Writer.Allocating.init(
3371         std.testing.allocator,
3372     );
3373     defer logical_summary.deinit();
3374     try logical.writeSummary(
3375         &logical_summary.writer,
3376         .{ .layer = .logical },
3377     );
3378     try std.testing.expect(std.mem.indexOf(
3379         u8,
3380         logical_summary.written(),
3381         "layer=logical sort=retained events=5 allocations=1",
3382     ) != null);
3383     try std.testing.expect(std.mem.indexOf(
3384         u8,
3385         logical_summary.written(),
3386         "requested_bytes=16",
3387     ) != null);
3388     try std.testing.expect(std.mem.indexOf(
3389         u8,
3390         logical_summary.written(),
3391         "failed_allocations=1",
3392     ) != null);
3393 }
3394 
3395 test "physical analysis preserves partial mapping lifecycles" {
3396     const events =
3397         "{\"v\":3,\"kind\":\"map\",\"allocator_id\":2," ++
3398         "\"address\":4096,\"len\":16384,\"scope\":\"root\"," ++
3399         "\"layer\":\"physical_page\",\"producer\":\"sys_memory\"}\n" ++
3400         "{\"v\":3,\"kind\":\"unmap\",\"allocator_id\":2," ++
3401         "\"address\":4096,\"len\":4096,\"scope\":\"root\"," ++
3402         "\"layer\":\"physical_page\",\"producer\":\"sys_memory\"}\n" ++
3403         "{\"v\":3,\"kind\":\"unmap\",\"allocator_id\":2," ++
3404         "\"address\":16384,\"len\":4096,\"scope\":\"root\"," ++
3405         "\"layer\":\"physical_page\",\"producer\":\"sys_memory\"}\n" ++
3406         "{\"v\":3,\"kind\":\"discard\",\"allocator_id\":2," ++
3407         "\"address\":8192,\"len\":4096,\"scope\":\"root\"," ++
3408         "\"layer\":\"physical_page\",\"producer\":\"sys_memory\"}\n";
3409 
3410     var analyzer = Analyzer.initForLayer(std.testing.allocator, .physical);
3411     defer analyzer.deinit();
3412     try analyzer.ingestJsonlBytes(events);
3413     try std.testing.expectEqual(@as(u64, 1), analyzer.physical.maps);
3414     try std.testing.expectEqual(@as(u64, 2), analyzer.physical.unmaps);
3415     try std.testing.expectEqual(@as(u64, 1), analyzer.physical.discards);
3416     try std.testing.expectEqual(
3417         @as(usize, 8192),
3418         analyzer.physical.live_mapped_bytes,
3419     );
3420     try std.testing.expectEqual(@as(usize, 1), analyzer.mappings.count());
3421     try std.testing.expectEqual(
3422         @as(usize, 0),
3423         analyzer.physical.untracked_unmap_bytes,
3424     );
3425 
3426     var output = std.Io.Writer.Allocating.init(std.testing.allocator);
3427     defer output.deinit();
3428     try analyzer.writeSummaryJsonl(
3429         &output.writer,
3430         .{ .layer = .physical },
3431     );
3432     try std.testing.expect(std.mem.indexOf(
3433         u8,
3434         output.written(),
3435         "\"live_mapped_bytes\":8192",
3436     ) != null);
3437 }
3438 
3439 test "physical advice records success and failure without changing mapped bytes" {
3440     const events =
3441         "{\"v\":3,\"kind\":\"map\",\"allocator_id\":2," ++
3442         "\"address\":4096,\"len\":4096,\"layer\":\"physical_page\"}\n" ++
3443         "{\"v\":3,\"kind\":\"advise\",\"allocator_id\":2," ++
3444         "\"address\":4096,\"len\":4096,\"layer\":\"physical_page\"}\n" ++
3445         "{\"v\":3,\"kind\":\"advise\",\"allocator_id\":2," ++
3446         "\"address\":4096,\"len\":4096,\"layer\":\"physical_page\",\"succeeded\":false}\n";
3447     var analyzer = Analyzer.initForLayer(std.testing.allocator, .physical);
3448     defer analyzer.deinit();
3449     try analyzer.ingestJsonlBytes(events);
3450     try std.testing.expectEqual(@as(u64, 1), analyzer.physical.advises);
3451     try std.testing.expectEqual(@as(u64, 1), analyzer.physical.failed_advises);
3452     try std.testing.expectEqual(@as(u64, 1), analyzer.physical.maps);
3453     try std.testing.expectEqual(@as(usize, 4096), analyzer.physical.live_mapped_bytes);
3454     try std.testing.expectEqual(@as(usize, 4096), analyzer.physical.high_water_mapped_bytes);
3455     try std.testing.expectEqual(@as(usize, 1), analyzer.mappings.count());
3456 }