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 }