lib/memtrace/src/stack/analyze.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const std = @import("std");
  2 const memtrace = @import("../root.zig");
  3 const capture_mod = @import("capture.zig");
  4 const identity_mod = @import("identity.zig");
  5 
  6 const coverage_mod = memtrace.coverage;
  7 const event_mod = memtrace.event;
  8 
  9 const Allocator = std.mem.Allocator;
 10 
 11 pub const Counters = struct {
 12     calls: u64 = 0,
 13     requested_bytes: u128 = 0,
 14 };
 15 
 16 pub const Totals = struct {
 17     calls: u64 = 0,
 18     successful: u64 = 0,
 19     failed: u64 = 0,
 20     requested_bytes: u128 = 0,
 21 };
 22 
 23 pub const Window = struct {
 24     scope: ?[]const u8 = null,
 25     first_sequence: ?u64 = null,
 26     last_sequence: ?u64 = null,
 27 
 28     pub fn validate(self: Window) !void {
 29         if (self.scope) |scope| {
 30             if (scope.len == 0 or
 31                 (!std.mem.eql(u8, scope, "root") and
 32                     !std.mem.startsWith(u8, scope, "root/")) or
 33                 scope[scope.len - 1] == '/')
 34             {
 35                 return error.InvalidScopeWindow;
 36             }
 37         }
 38         if (self.first_sequence) |first| {
 39             if (first == 0) return error.InvalidSequenceWindow;
 40         }
 41         if (self.last_sequence) |last| {
 42             if (last == 0) return error.InvalidSequenceWindow;
 43         }
 44         if (self.first_sequence != null and
 45             self.last_sequence != null and
 46             self.first_sequence.? > self.last_sequence.?)
 47         {
 48             return error.InvalidSequenceWindow;
 49         }
 50     }
 51 
 52     fn includesSequence(self: Window, sequence: ?u64) bool {
 53         const actual = sequence orelse return false;
 54         if (self.first_sequence) |first| {
 55             if (actual < first) return false;
 56         }
 57         if (self.last_sequence) |last| {
 58             if (actual > last) return false;
 59         }
 60         return true;
 61     }
 62 };
 63 
 64 pub const Selection = enum {
 65     allocations,
 66     all,
 67 
 68     pub fn includes(self: Selection, kind: event_mod.Kind) bool {
 69         return switch (self) {
 70             .allocations => kind == .alloc or kind == .map,
 71             .all => kind.isMemoryOperation(),
 72         };
 73     }
 74 };
 75 
 76 pub const OperationKey = struct {
 77     stack_id: u32,
 78     kind: event_mod.Kind,
 79     succeeded: bool,
 80     layer: event_mod.Layer,
 81     producer: @import("alloc_observe").Producer,
 82 };
 83 
 84 pub const Definition = struct {
 85     call_addresses: []u64,
 86     truncated: bool,
 87     unwind_failed: bool,
 88     missing_return_address: bool,
 89 
 90     pub fn complete(self: Definition) bool {
 91         return !self.truncated and
 92             !self.unwind_failed and
 93             !self.missing_return_address and
 94             self.call_addresses.len > 1;
 95     }
 96 };
 97 
 98 pub const Summary = struct {
 99     key: OperationKey,
100     counters: Counters,
101     definition: Definition,
102 };
103 
104 const Sequence = struct {
105     events: u64 = 0,
106     unsequenced: u64 = 0,
107     gaps: u64 = 0,
108     regressions: u64 = 0,
109     starts: u64 = 0,
110     stops: u64 = 0,
111     recording_failures: u64 = 0,
112     first: ?u64 = null,
113     last: ?u64 = null,
114     stop: ?u64 = null,
115 
116     fn record(self: *Sequence, event: event_mod.ReplayEvent) void {
117         self.events +|= 1;
118         const current = event.seq orelse {
119             self.unsequenced +|= 1;
120             return;
121         };
122         if (current == 0) {
123             self.unsequenced +|= 1;
124             return;
125         }
126         if (self.last) |previous| {
127             if (current <= previous) {
128                 self.regressions +|= 1;
129             } else if (current != previous + 1) {
130                 self.gaps +|= current - previous - 1;
131             }
132         } else {
133             self.first = current;
134             if (current != 1) self.gaps +|= current - 1;
135         }
136         self.last = current;
137         switch (event.kind) {
138             .trace_start => self.starts +|= 1,
139             .trace_stop => {
140                 self.stops +|= 1;
141                 self.stop = current;
142                 self.recording_failures +|= event.recording_failures;
143             },
144             else => {},
145         }
146     }
147 
148     fn validate(self: Sequence) !void {
149         if (self.events == 0 or self.starts != 1 or self.stops != 1) {
150             return error.IncompleteEventSequence;
151         }
152         if (self.unsequenced != 0 or self.gaps != 0 or self.regressions != 0) {
153             return error.IncompleteEventSequence;
154         }
155         if (self.first != 1 or self.stop != self.last) {
156             return error.IncompleteEventSequence;
157         }
158         if (self.recording_failures != 0) return error.IncompleteEventSequence;
159     }
160 };
161 
162 const ScopeIndex = struct {
163     paths: std.AutoHashMapUnmanaged(u32, []const u8) = .{},
164 
165     fn deinit(self: *ScopeIndex, allocator: Allocator) void {
166         var paths = self.paths.valueIterator();
167         while (paths.next()) |scope_path| allocator.free(scope_path.*);
168         self.paths.deinit(allocator);
169         self.* = undefined;
170     }
171 
172     fn record(
173         self: *ScopeIndex,
174         allocator: Allocator,
175         event: event_mod.ReplayEvent,
176     ) !void {
177         if (event.kind != .scope_enter) return;
178         if (event.scope_id == 0 or event.scope.len == 0) {
179             return error.InvalidScopeDefinition;
180         }
181         if (self.paths.get(event.scope_id)) |scope_path| {
182             if (!std.mem.eql(u8, scope_path, event.scope)) {
183                 return error.ScopeIdentityConflict;
184             }
185             return;
186         }
187         const owned = try allocator.dupe(u8, event.scope);
188         errdefer allocator.free(owned);
189         try self.paths.putNoClobber(allocator, event.scope_id, owned);
190     }
191 
192     fn includes(
193         self: *const ScopeIndex,
194         window: Window,
195         event: event_mod.ReplayEvent,
196     ) !bool {
197         if (!window.includesSequence(event.seq)) return false;
198         const requested = window.scope orelse return true;
199         const actual = self.path(event.scope_id) orelse
200             return error.MissingScopeDefinition;
201         return scopeContains(requested, actual);
202     }
203 
204     fn validateWindow(self: *const ScopeIndex, window: Window) !void {
205         try window.validate();
206         const requested = window.scope orelse return;
207         if (std.mem.eql(u8, requested, "root")) return;
208         var paths = self.paths.valueIterator();
209         while (paths.next()) |scope_path| {
210             if (scopeContains(requested, scope_path.*)) return;
211         }
212         return error.UnknownScopeWindow;
213     }
214 
215     fn path(self: *const ScopeIndex, scope_id: u32) ?[]const u8 {
216         if (scope_id == 0) return "root";
217         return self.paths.get(scope_id);
218     }
219 };
220 
221 pub const Analyzer = struct {
222     allocator: Allocator,
223     window: Window,
224     scopes: ScopeIndex = .{},
225     definitions: std.AutoHashMapUnmanaged(u32, Definition) = .{},
226     referenced_stacks: std.AutoHashMapUnmanaged(u32, void) = .{},
227     counters: std.AutoHashMapUnmanaged(OperationKey, Counters) = .{},
228     executable_digest: ?identity_mod.Digest = null,
229     executable_records: u32 = 0,
230     coverage: ?coverage_mod.Manifest = null,
231     coverage_records: u32 = 0,
232     unattributed_operations: u64 = 0,
233     sequence: Sequence = .{},
234 
235     pub fn init(allocator: Allocator, window: Window) Analyzer {
236         return .{ .allocator = allocator, .window = window };
237     }
238 
239     pub fn deinit(self: *Analyzer) void {
240         var definitions = self.definitions.valueIterator();
241         while (definitions.next()) |definition| {
242             self.allocator.free(definition.call_addresses);
243         }
244         self.scopes.deinit(self.allocator);
245         self.definitions.deinit(self.allocator);
246         self.referenced_stacks.deinit(self.allocator);
247         self.counters.deinit(self.allocator);
248         self.* = undefined;
249     }
250 
251     pub fn ingestJsonLine(self: *Analyzer, line: []const u8) !void {
252         const text = std.mem.trim(u8, line, " \t\r\n");
253         if (text.len == 0) return;
254         if (identity_mod.isMetadataLine(text)) {
255             return try self.ingestExecutable(text);
256         }
257         if (coverage_mod.isMetadataLine(text)) {
258             return try self.ingestCoverage(text);
259         }
260         if (capture_mod.isMetadataLine(text)) {
261             return try self.ingestStack(text);
262         }
263         const event = try event_mod.parseReplayFast(text);
264         self.sequence.record(event);
265         try self.scopes.record(self.allocator, event);
266         if (!event.kind.isMemoryOperation()) return;
267         if (event.stack_id == 0) {
268             self.unattributed_operations = try std.math.add(
269                 u64,
270                 self.unattributed_operations,
271                 1,
272             );
273             return;
274         }
275         try self.referenced_stacks.put(self.allocator, event.stack_id, {});
276         if (!try self.includes(event)) return;
277         const entry = try self.counters.getOrPut(
278             self.allocator,
279             .{
280                 .stack_id = event.stack_id,
281                 .kind = event.kind,
282                 .succeeded = event.succeeded,
283                 .layer = event.layer,
284                 .producer = event.producer,
285             },
286         );
287         if (!entry.found_existing) entry.value_ptr.* = .{};
288         entry.value_ptr.calls = try std.math.add(
289             u64,
290             entry.value_ptr.calls,
291             1,
292         );
293         entry.value_ptr.requested_bytes = try std.math.add(
294             u128,
295             entry.value_ptr.requested_bytes,
296             requestBytes(event),
297         );
298     }
299 
300     pub fn validate(self: *const Analyzer) !void {
301         try self.sequence.validate();
302         try self.scopes.validateWindow(self.window);
303         if (self.executable_records != 1 or self.executable_digest == null) {
304             return error.MissingExecutableIdentity;
305         }
306         if (self.coverage_records != 1 or self.coverage == null) {
307             return error.MissingCoverageManifest;
308         }
309         if (self.unattributed_operations != 0) {
310             return error.MissingStackAttribution;
311         }
312         var definitions = self.definitions.valueIterator();
313         while (definitions.next()) |definition| {
314             if (!definition.complete()) return error.IncompleteStackCapture;
315         }
316         var referenced = self.referenced_stacks.keyIterator();
317         while (referenced.next()) |stack_id| {
318             _ = self.definitions.get(stack_id.*) orelse
319                 return error.MissingStackDefinition;
320         }
321     }
322 
323     pub fn collect(
324         self: *const Analyzer,
325         selection: Selection,
326         layer: event_mod.LayerFilter,
327     ) !std.ArrayListUnmanaged(Summary) {
328         var summaries = std.ArrayListUnmanaged(Summary).empty;
329         errdefer summaries.deinit(self.allocator);
330         try summaries.ensureTotalCapacity(self.allocator, self.counters.count());
331         var counters = self.counters.iterator();
332         while (counters.next()) |entry| {
333             if (!selection.includes(entry.key_ptr.kind)) continue;
334             if (!layer.includes(entry.key_ptr.layer)) continue;
335             summaries.appendAssumeCapacity(.{
336                 .key = entry.key_ptr.*,
337                 .counters = entry.value_ptr.*,
338                 .definition = self.definitions.get(entry.key_ptr.stack_id).?,
339             });
340         }
341         std.mem.sort(Summary, summaries.items, {}, summaryGreaterThan);
342         return summaries;
343     }
344 
345     pub fn totals(
346         self: *const Analyzer,
347         selection: Selection,
348         layer: event_mod.LayerFilter,
349     ) Totals {
350         var result: Totals = .{};
351         var counters = self.counters.iterator();
352         while (counters.next()) |entry| {
353             if (!selection.includes(entry.key_ptr.kind)) continue;
354             if (!layer.includes(entry.key_ptr.layer)) continue;
355             result.calls +|= entry.value_ptr.calls;
356             result.requested_bytes +|= entry.value_ptr.requested_bytes;
357             if (entry.key_ptr.succeeded) {
358                 result.successful +|= entry.value_ptr.calls;
359             } else {
360                 result.failed +|= entry.value_ptr.calls;
361             }
362         }
363         return result;
364     }
365 
366     pub fn stackDefinition(
367         self: *const Analyzer,
368         stack_id: u32,
369     ) ?Definition {
370         return self.definitions.get(stack_id);
371     }
372 
373     pub fn includes(
374         self: *const Analyzer,
375         event: event_mod.ReplayEvent,
376     ) !bool {
377         return try self.scopes.includes(self.window, event);
378     }
379 
380     pub fn scopePath(self: *const Analyzer, scope_id: u32) ?[]const u8 {
381         return self.scopes.path(scope_id);
382     }
383 
384     fn ingestExecutable(self: *Analyzer, text: []const u8) !void {
385         var parsed = try parseObject(self.allocator, text);
386         defer parsed.deinit();
387         const digest_text = try jsonString(
388             parsed.value.object.get("sha256") orelse
389                 return error.InvalidExecutableIdentity,
390         );
391         const digest = try identity_mod.parseDigest(digest_text);
392         if (self.executable_digest) |prior| {
393             if (!std.mem.eql(u8, &prior, &digest)) {
394                 return error.InvalidExecutableIdentity;
395             }
396         }
397         self.executable_digest = digest;
398         self.executable_records = try std.math.add(
399             u32,
400             self.executable_records,
401             1,
402         );
403     }
404 
405     fn ingestCoverage(self: *Analyzer, text: []const u8) !void {
406         if (self.coverage != null) return error.InvalidCoverageMetadata;
407         self.coverage = try coverage_mod.parseMetadata(self.allocator, text);
408         self.coverage_records = try std.math.add(
409             u32,
410             self.coverage_records,
411             1,
412         );
413     }
414 
415     fn ingestStack(self: *Analyzer, text: []const u8) !void {
416         var parsed = try parseObject(self.allocator, text);
417         defer parsed.deinit();
418         const object = parsed.value.object;
419         const stack_id = std.math.cast(
420             u32,
421             try jsonU64(object.get("stack_id") orelse
422                 return error.InvalidStackDefinition),
423         ) orelse return error.InvalidStackDefinition;
424         if (stack_id == 0 or self.definitions.contains(stack_id)) {
425             return error.InvalidStackDefinition;
426         }
427         const values = try jsonArray(object.get("call_addresses") orelse
428             return error.InvalidStackDefinition);
429         if (values.items.len == 0 or values.items.len > capture_mod.max_frames_limit) {
430             return error.InvalidStackDefinition;
431         }
432         const addresses = try self.allocator.alloc(u64, values.items.len);
433         errdefer self.allocator.free(addresses);
434         for (values.items, addresses) |value, *address| {
435             address.* = try jsonU64(value);
436             if (address.* == 0) return error.InvalidStackDefinition;
437         }
438         try self.definitions.putNoClobber(self.allocator, stack_id, .{
439             .call_addresses = addresses,
440             .truncated = try jsonBool(object.get("truncated") orelse
441                 return error.InvalidStackDefinition),
442             .unwind_failed = try jsonBool(object.get("unwind_failed") orelse
443                 return error.InvalidStackDefinition),
444             .missing_return_address = try jsonBool(
445                 object.get("missing_return_address") orelse
446                     return error.InvalidStackDefinition,
447             ),
448         });
449     }
450 };
451 
452 fn scopeContains(requested: []const u8, actual: []const u8) bool {
453     if (std.mem.eql(u8, requested, actual)) return true;
454     return actual.len > requested.len and
455         std.mem.startsWith(u8, actual, requested) and
456         actual[requested.len] == '/';
457 }
458 
459 fn summaryGreaterThan(_: void, left: Summary, right: Summary) bool {
460     if (left.counters.calls != right.counters.calls) {
461         return left.counters.calls > right.counters.calls;
462     }
463     if (left.counters.requested_bytes != right.counters.requested_bytes) {
464         return left.counters.requested_bytes > right.counters.requested_bytes;
465     }
466     if (left.key.kind != right.key.kind) {
467         return @backingInt(left.key.kind) < @backingInt(right.key.kind);
468     }
469     if (left.key.layer != right.key.layer) {
470         return @backingInt(left.key.layer) < @backingInt(right.key.layer);
471     }
472     if (left.key.producer != right.key.producer) {
473         return @backingInt(left.key.producer) < @backingInt(right.key.producer);
474     }
475     if (left.key.succeeded != right.key.succeeded) return left.key.succeeded;
476     return left.key.stack_id < right.key.stack_id;
477 }
478 
479 fn requestBytes(event: event_mod.ReplayEvent) usize {
480     return switch (event.kind) {
481         .free, .release, .unmap => event.old_len,
482         .alloc, .resize, .remap, .map, .protect, .discard, .decommit, .advise => event.len,
483         else => unreachable,
484     };
485 }
486 
487 fn parseObject(
488     allocator: Allocator,
489     text: []const u8,
490 ) !std.json.Parsed(std.json.Value) {
491     const parsed = std.json.parseFromSlice(
492         std.json.Value,
493         allocator,
494         text,
495         .{},
496     ) catch |err| switch (err) {
497         error.OutOfMemory => return err,
498         else => return error.InvalidStackMetadata,
499     };
500     if (parsed.value != .object) return error.InvalidStackMetadata;
501     return parsed;
502 }
503 
504 fn jsonString(value: std.json.Value) ![]const u8 {
505     return switch (value) {
506         .string => |text| text,
507         else => error.InvalidStackMetadata,
508     };
509 }
510 
511 fn jsonU64(value: std.json.Value) !u64 {
512     return switch (value) {
513         .integer => |number| if (number >= 0)
514             @intCast(number)
515         else
516             error.InvalidStackMetadata,
517         else => error.InvalidStackMetadata,
518     };
519 }
520 
521 fn jsonBool(value: std.json.Value) !bool {
522     return switch (value) {
523         .bool => |flag| flag,
524         else => error.InvalidStackMetadata,
525     };
526 }
527 
528 fn jsonArray(value: std.json.Value) !std.json.Array {
529     return switch (value) {
530         .array => |array| array,
531         else => error.InvalidStackMetadata,
532     };
533 }
534 
535 test "exact analyzer rejects an allocation without a stack" {
536     var analyzer = Analyzer.init(std.testing.allocator, .{});
537     defer analyzer.deinit();
538     const digest: identity_mod.Digest = @splat(0xaa);
539     var metadata = std.Io.Writer.Allocating.init(std.testing.allocator);
540     defer metadata.deinit();
541     try identity_mod.writeMetadata(&metadata.writer, digest);
542     try coverage_mod.writeMetadata(
543         &metadata.writer,
544         coverage_mod.boundaryManifest(),
545     );
546     var lines = std.mem.splitScalar(u8, metadata.written(), '\n');
547     while (lines.next()) |line| try analyzer.ingestJsonLine(line);
548     try analyzer.ingestJsonLine(
549         "{\"v\":3,\"seq\":1,\"kind\":\"trace.start\"}",
550     );
551     try analyzer.ingestJsonLine(
552         "{\"v\":3,\"seq\":2,\"kind\":\"alloc\",\"len\":8,\"stack_id\":0}",
553     );
554     try analyzer.ingestJsonLine(
555         "{\"v\":3,\"seq\":3,\"kind\":\"trace.stop\"}",
556     );
557     try std.testing.expectError(
558         error.MissingStackAttribution,
559         analyzer.validate(),
560     );
561 }
562 
563 test "exact analyzer rejects a truncated physical stack" {
564     var analyzer = Analyzer.init(std.testing.allocator, .{});
565     defer analyzer.deinit();
566     const digest: identity_mod.Digest = @splat(0xaa);
567     var metadata = std.Io.Writer.Allocating.init(std.testing.allocator);
568     defer metadata.deinit();
569     try identity_mod.writeMetadata(&metadata.writer, digest);
570     try coverage_mod.writeMetadata(
571         &metadata.writer,
572         coverage_mod.boundaryManifest(),
573     );
574     var addresses = [_]usize{ 1, 2 };
575     try capture_mod.writeDefinition(&metadata.writer, 1, .{
576         .addresses = &addresses,
577         .truncated = true,
578         .unwind_failed = false,
579         .missing_return_address = false,
580         .collision_next = 0,
581     });
582     var lines = std.mem.splitScalar(u8, metadata.written(), '\n');
583     while (lines.next()) |line| try analyzer.ingestJsonLine(line);
584     try analyzer.ingestJsonLine(
585         "{\"v\":3,\"seq\":1,\"kind\":\"trace.start\"}",
586     );
587     try analyzer.ingestJsonLine(
588         "{\"v\":3,\"seq\":2,\"kind\":\"alloc\",\"len\":8,\"stack_id\":1}",
589     );
590     try analyzer.ingestJsonLine(
591         "{\"v\":3,\"seq\":3,\"kind\":\"trace.stop\"}",
592     );
593     try std.testing.expectError(
594         error.IncompleteStackCapture,
595         analyzer.validate(),
596     );
597 }
598 
599 test "exact analyzer preserves allocator layers and producers" {
600     var analyzer = Analyzer.init(std.testing.allocator, .{});
601     defer analyzer.deinit();
602     const digest: identity_mod.Digest = @splat(0xaa);
603     var input = std.Io.Writer.Allocating.init(std.testing.allocator);
604     defer input.deinit();
605     try identity_mod.writeMetadata(&input.writer, digest);
606     try coverage_mod.writeMetadata(
607         &input.writer,
608         coverage_mod.ownedProducerManifest(),
609     );
610     var addresses = [_]usize{ 1, 2 };
611     try capture_mod.writeDefinition(&input.writer, 1, .{
612         .addresses = &addresses,
613         .truncated = false,
614         .unwind_failed = false,
615         .missing_return_address = false,
616         .collision_next = 0,
617     });
618     try (event_mod.Event{
619         .seq = 1,
620         .kind = .trace_start,
621     }).writeJsonLine(&input.writer, null, null);
622     try (event_mod.Event{
623         .seq = 2,
624         .kind = .alloc,
625         .len = 128,
626         .stack_id = 1,
627     }).writeJsonLine(&input.writer, null, null);
628     try (event_mod.Event{
629         .seq = 3,
630         .kind = .alloc,
631         .len = 16,
632         .stack_id = 1,
633         .layer = .logical_allocator,
634         .producer = .arena,
635     }).writeJsonLine(&input.writer, null, null);
636     try (event_mod.Event{
637         .seq = 4,
638         .kind = .trace_stop,
639     }).writeJsonLine(&input.writer, null, null);
640     var lines = std.mem.splitScalar(u8, input.written(), '\n');
641     while (lines.next()) |line| try analyzer.ingestJsonLine(line);
642     try analyzer.validate();
643 
644     const backing = analyzer.totals(.allocations, .backing);
645     const logical = analyzer.totals(.allocations, .logical);
646     const all = analyzer.totals(.allocations, .all);
647     try std.testing.expectEqual(@as(u64, 1), backing.calls);
648     try std.testing.expectEqual(@as(u128, 128), backing.requested_bytes);
649     try std.testing.expectEqual(@as(u64, 1), logical.calls);
650     try std.testing.expectEqual(@as(u128, 16), logical.requested_bytes);
651     try std.testing.expectEqual(@as(u64, 2), all.calls);
652 
653     var logical_summaries = try analyzer.collect(.allocations, .logical);
654     defer logical_summaries.deinit(std.testing.allocator);
655     try std.testing.expectEqual(@as(usize, 1), logical_summaries.items.len);
656     try std.testing.expectEqual(
657         event_mod.Layer.logical_allocator,
658         logical_summaries.items[0].key.layer,
659     );
660     try std.testing.expectEqual(
661         @import("alloc_observe").Producer.arena,
662         logical_summaries.items[0].key.producer,
663     );
664 }
665 
666 test "exact analyzer filters a scope subtree and inclusive sequence window" {
667     var analyzer = Analyzer.init(std.testing.allocator, .{
668         .scope = "root/phase",
669         .first_sequence = 3,
670         .last_sequence = 5,
671     });
672     defer analyzer.deinit();
673     try ingestWindowFixture(&analyzer);
674     try analyzer.validate();
675 
676     const totals = analyzer.totals(.allocations, .backing);
677     try std.testing.expectEqual(@as(u64, 2), totals.calls);
678     try std.testing.expectEqual(@as(u128, 24), totals.requested_bytes);
679 }
680 
681 test "exact analyzer rejects an unknown scope window" {
682     var analyzer = Analyzer.init(std.testing.allocator, .{
683         .scope = "root/absent",
684     });
685     defer analyzer.deinit();
686     try ingestWindowFixture(&analyzer);
687     try std.testing.expectError(error.UnknownScopeWindow, analyzer.validate());
688 }
689 
690 fn ingestWindowFixture(analyzer: *Analyzer) !void {
691     const digest: identity_mod.Digest = @splat(0xaa);
692     var input = std.Io.Writer.Allocating.init(std.testing.allocator);
693     defer input.deinit();
694     try identity_mod.writeMetadata(&input.writer, digest);
695     try coverage_mod.writeMetadata(
696         &input.writer,
697         coverage_mod.boundaryManifest(),
698     );
699     var addresses = [_]usize{ 1, 2 };
700     try capture_mod.writeDefinition(&input.writer, 1, .{
701         .addresses = &addresses,
702         .truncated = false,
703         .unwind_failed = false,
704         .missing_return_address = false,
705         .collision_next = 0,
706     });
707     const lines = [_][]const u8{
708         "{\"v\":3,\"seq\":1,\"kind\":\"trace.start\"}",
709         "{\"v\":3,\"seq\":2,\"kind\":\"scope.enter\",\"scope_id\":1," ++
710             "\"scope\":\"root/phase\"}",
711         "{\"v\":3,\"seq\":3,\"kind\":\"alloc\",\"scope_id\":1,\"len\":8," ++
712             "\"stack_id\":1}",
713         "{\"v\":3,\"seq\":4,\"kind\":\"scope.enter\",\"scope_id\":2," ++
714             "\"scope\":\"root/phase/child\"}",
715         "{\"v\":3,\"seq\":5,\"kind\":\"alloc\",\"scope_id\":2,\"len\":16," ++
716             "\"stack_id\":1}",
717         "{\"v\":3,\"seq\":6,\"kind\":\"scope.exit\",\"scope_id\":2}",
718         "{\"v\":3,\"seq\":7,\"kind\":\"scope.exit\",\"scope_id\":1}",
719         "{\"v\":3,\"seq\":8,\"kind\":\"scope.enter\",\"scope_id\":3," ++
720             "\"scope\":\"root/other\"}",
721         "{\"v\":3,\"seq\":9,\"kind\":\"alloc\",\"scope_id\":3,\"len\":32," ++
722             "\"stack_id\":1}",
723         "{\"v\":3,\"seq\":10,\"kind\":\"scope.exit\",\"scope_id\":3}",
724         "{\"v\":3,\"seq\":11,\"kind\":\"trace.stop\"}",
725     };
726     var metadata = std.mem.splitScalar(u8, input.written(), '\n');
727     while (metadata.next()) |line| try analyzer.ingestJsonLine(line);
728     for (lines) |line| try analyzer.ingestJsonLine(line);
729 }