lib/tracy/src/memory.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const pretty_json = @import("pretty").json;
3 const capture_mod = @import("capture.zig");
4 const report = @import("report.zig");
5 const event = @import("event.zig");
6 const record_mod = @import("record.zig");
7 const transport = @import("transport.zig");
8
9 pub const schema = "tracy.memory/v1";
10 pub const CaptureIntegrity = capture_mod.Integrity;
11
12 pub const Sort = enum {
13 live,
14 high_water,
15 allocated,
16 lifetime_tail,
17 allocs,
18 frees,
19 name,
20
21 pub fn fromName(text: []const u8) ?Sort {
22 if (std.mem.eql(u8, text, "live")) return .live;
23 if (std.mem.eql(u8, text, "high-water")) return .high_water;
24 if (std.mem.eql(u8, text, "allocated")) return .allocated;
25 if (std.mem.eql(u8, text, "lifetime-tail")) return .lifetime_tail;
26 if (std.mem.eql(u8, text, "allocs")) return .allocs;
27 if (std.mem.eql(u8, text, "frees")) return .frees;
28 if (std.mem.eql(u8, text, "name")) return .name;
29 return null;
30 }
31
32 fn tag(self: Sort) []const u8 {
33 return switch (self) {
34 .live => "live",
35 .high_water => "high-water",
36 .allocated => "allocated",
37 .lifetime_tail => "lifetime-tail",
38 .allocs => "allocs",
39 .frees => "frees",
40 .name => "name",
41 };
42 }
43 };
44
45 pub const Options = struct {
46 top: usize = 20,
47 occurrences: usize = 40,
48 sort: Sort = .live,
49 min_live_bytes: u64 = 0,
50 name: ?[]const u8 = null,
51 };
52
53 pub const Counters = struct {
54 events: u64 = 0,
55 allocations: u64 = 0,
56 frees: u64 = 0,
57 unmatched_frees: u64 = 0,
58 duplicate_allocations: u64 = 0,
59 completed_lifetimes: u64 = 0,
60 lifetime_samples: u64 = 0,
61 timestamp_regressions: u64 = 0,
62 untracked_allocations: u64 = 0,
63 size_mismatches: u64 = 0,
64 allocated_bytes: u64 = 0,
65 freed_bytes: u64 = 0,
66 live_bytes: u64 = 0,
67 high_water_live_bytes: u64 = 0,
68 high_water_time_ns: u64 = 0,
69 };
70
71 pub const Summary = struct {
72 name: []const u8,
73 allocations: u64 = 0,
74 frees: u64 = 0,
75 unmatched_frees: u64 = 0,
76 duplicate_allocations: u64 = 0,
77 active_allocations: u64 = 0,
78 completed_lifetimes: u64 = 0,
79 lifetime_samples: u64 = 0,
80 timestamp_regressions: u64 = 0,
81 untracked_allocations: u64 = 0,
82 size_mismatches: u64 = 0,
83 allocated_bytes: u64 = 0,
84 freed_bytes: u64 = 0,
85 live_bytes: u64 = 0,
86 high_water_live_bytes: u64 = 0,
87 high_water_time_ns: u64 = 0,
88 max_alloc_bytes: u64 = 0,
89 total_lifetime_ns: u64 = 0,
90 min_lifetime_ns: u64 = 0,
91 p50_lifetime_ns: u64 = 0,
92 p90_lifetime_ns: u64 = 0,
93 p99_lifetime_ns: u64 = 0,
94 max_lifetime_ns: u64 = 0,
95 last_event_ns: u64 = 0,
96
97 pub fn meanAllocBytes(self: Summary) u64 {
98 if (self.allocations == 0) return 0;
99 return self.allocated_bytes / self.allocations;
100 }
101
102 pub fn meanLifetimeNs(self: Summary) u64 {
103 if (self.lifetime_samples == 0) return 0;
104 return self.total_lifetime_ns / self.lifetime_samples;
105 }
106 };
107
108 const Record = struct {
109 name: []const u8,
110 size: u64,
111 allocated_ns: u64,
112 thread: u64,
113 };
114
115 const LiveView = struct {
116 name: []const u8,
117 address: u64,
118 size: u64,
119 allocated_ns: u64,
120 age_ns: u64,
121 thread: u64,
122 };
123
124 pub const Lifetime = struct {
125 name: []const u8,
126 address: u64,
127 size: u64,
128 reported_free_size: u64,
129 allocation_thread: u64,
130 free_thread: u64,
131 allocated_ns: u64,
132 freed_ns: u64,
133 lifetime_ns: u64,
134 size_match: bool,
135 valid_time: bool,
136 };
137
138 const LifetimeSample = struct {
139 row: usize,
140 duration_ns: u64,
141 };
142
143 pub const Analyzer = struct {
144 allocator: std.mem.Allocator,
145 capture: capture_mod.Tracker = .{},
146 summaries: std.StringHashMapUnmanaged(Summary) = .{},
147 active: std.AutoHashMapUnmanaged(u64, Record) = .{},
148 lifetimes: std.ArrayListUnmanaged(Lifetime) = .empty,
149 counters: Counters = .{},
150 start_ns: ?u64 = null,
151 end_ns: ?u64 = null,
152
153 pub fn init(allocator: std.mem.Allocator) Analyzer {
154 return .{ .allocator = allocator };
155 }
156
157 pub fn deinit(self: *Analyzer) void {
158 var iter = self.summaries.iterator();
159 while (iter.next()) |entry| self.allocator.free(entry.key_ptr.*);
160 self.summaries.deinit(self.allocator);
161 self.active.deinit(self.allocator);
162 self.lifetimes.deinit(self.allocator);
163 self.* = undefined;
164 }
165
166 pub fn ingestJsonlBytes(self: *Analyzer, bytes: []const u8) !void {
167 var lines = std.mem.splitScalar(u8, bytes, '\n');
168 while (lines.next()) |line| try self.ingestJsonLine(line);
169 }
170
171 pub fn ingestJsonLine(self: *Analyzer, line: []const u8) !void {
172 const text = std.mem.trim(u8, line, " \t\r\n");
173 if (text.len == 0) return;
174 var parsed = try record_mod.parseLine(self.allocator, text);
175 defer parsed.deinit();
176 switch (parsed) {
177 .event => |value| try self.ingest(value),
178 .flight => |report_value| self.recordFlightReport(report_value),
179 }
180 }
181
182 pub fn ingest(self: *Analyzer, parsed: event.Parsed) !void {
183 self.capture.record(parsed);
184 self.counters.events += 1;
185 if (self.start_ns == null and parsed.time_ns != 0) self.start_ns = parsed.time_ns;
186 if (parsed.time_ns != 0) self.end_ns = parsed.time_ns;
187 switch (parsed.kind) {
188 .start => {
189 if (parsed.time_ns != 0) self.start_ns = parsed.time_ns;
190 },
191 .stop => {
192 if (parsed.time_ns != 0) self.end_ns = parsed.time_ns;
193 },
194 .alloc => try self.recordAlloc(parsed),
195 .free => try self.recordFree(parsed),
196 else => {},
197 }
198 }
199
200 pub fn collectSummaries(
201 self: *Analyzer,
202 allocator: std.mem.Allocator,
203 options: Options,
204 ) !std.ArrayListUnmanaged(Summary) {
205 var rows: std.ArrayListUnmanaged(Summary) = .empty;
206 errdefer rows.deinit(allocator);
207 var iter = self.summaries.valueIterator();
208 while (iter.next()) |summary| {
209 if (options.name) |wanted| {
210 if (!std.mem.eql(u8, wanted, summary.name)) continue;
211 }
212 if (summary.live_bytes < options.min_live_bytes) continue;
213 try rows.append(allocator, summary.*);
214 }
215 try applyLifetimeDistributions(allocator, rows.items, self.lifetimes.items);
216 sortSummaries(rows.items, options.sort);
217 return rows;
218 }
219
220 pub fn collectLive(
221 self: *Analyzer,
222 allocator: std.mem.Allocator,
223 options: Options,
224 ) !std.ArrayListUnmanaged(LiveView) {
225 var rows: std.ArrayListUnmanaged(LiveView) = .empty;
226 errdefer rows.deinit(allocator);
227 const end_ns = self.end_ns orelse 0;
228 var iter = self.active.iterator();
229 while (iter.next()) |entry| {
230 const record = entry.value_ptr.*;
231 if (options.name) |wanted| {
232 if (!std.mem.eql(u8, wanted, record.name)) continue;
233 }
234 if (record.size < options.min_live_bytes) continue;
235 try rows.append(allocator, .{
236 .name = record.name,
237 .address = entry.key_ptr.*,
238 .size = record.size,
239 .allocated_ns = record.allocated_ns,
240 .age_ns = if (end_ns >= record.allocated_ns) end_ns - record.allocated_ns else 0,
241 .thread = record.thread,
242 });
243 }
244 std.mem.sort(LiveView, rows.items, {}, liveGreaterThan);
245 return rows;
246 }
247
248 pub fn collectLifetimes(
249 self: *Analyzer,
250 allocator: std.mem.Allocator,
251 options: Options,
252 ) !std.ArrayListUnmanaged(Lifetime) {
253 var rows: std.ArrayListUnmanaged(Lifetime) = .empty;
254 errdefer rows.deinit(allocator);
255 for (self.lifetimes.items) |lifetime| {
256 if (options.name) |wanted| {
257 if (!std.mem.eql(u8, wanted, lifetime.name)) continue;
258 }
259 try rows.append(allocator, lifetime);
260 }
261 std.mem.sort(Lifetime, rows.items, {}, lifetimeGreaterThan);
262 if (rows.items.len > options.occurrences) {
263 rows.shrinkRetainingCapacity(options.occurrences);
264 }
265 return rows;
266 }
267
268 pub fn durationNs(self: Analyzer) u64 {
269 const start_ns = self.start_ns orelse return 0;
270 const end_ns = self.end_ns orelse return 0;
271 if (end_ns <= start_ns) return 0;
272 return end_ns - start_ns;
273 }
274
275 pub fn captureIntegrity(self: Analyzer) CaptureIntegrity {
276 return self.capture.integrity(0);
277 }
278
279 pub fn lifetimeEvidence(self: Analyzer) []const u8 {
280 if (!std.mem.eql(u8, self.captureIntegrity().status, "complete")) return "partial";
281 if (self.counters.timestamp_regressions != 0) return "partial";
282 if (self.counters.duplicate_allocations != 0) return "partial";
283 if (self.counters.unmatched_frees != 0) return "partial";
284 if (self.counters.untracked_allocations != 0) return "partial";
285 if (self.counters.size_mismatches != 0) return "partial";
286 if (self.active.count() != 0) return "partial";
287 return "complete";
288 }
289
290 pub fn recordFlightReport(self: *Analyzer, report_value: transport.Report) void {
291 self.capture.recordFlightReport(report_value);
292 }
293
294 fn recordAlloc(self: *Analyzer, parsed: event.Parsed) !void {
295 const name = parsed.name orelse "default";
296 const summary = try self.summaryFor(name);
297 var tracked = false;
298 if (parsed.address != 0) {
299 const entry = try self.active.getOrPut(self.allocator, parsed.address);
300 if (entry.found_existing) {
301 self.recordDuplicate(entry.value_ptr.*);
302 } else {
303 entry.value_ptr.* = .{
304 .name = summary.name,
305 .size = parsed.size,
306 .allocated_ns = parsed.time_ns,
307 .thread = parsed.thread,
308 };
309 tracked = true;
310 }
311 } else {
312 self.counters.untracked_allocations +|= 1;
313 summary.untracked_allocations +|= 1;
314 }
315
316 self.counters.allocations +|= 1;
317 self.counters.allocated_bytes +|= parsed.size;
318 summary.allocations +|= 1;
319 summary.allocated_bytes +|= parsed.size;
320 summary.max_alloc_bytes = @max(summary.max_alloc_bytes, parsed.size);
321 summary.last_event_ns = parsed.time_ns;
322 if (tracked) self.addLive(summary, parsed.size, parsed.time_ns);
323 }
324
325 fn recordFree(self: *Analyzer, parsed: event.Parsed) !void {
326 if (parsed.address != 0) {
327 if (self.active.get(parsed.address)) |record| {
328 try self.recordMatchedFree(parsed, record);
329 return;
330 }
331 }
332 try self.recordUnmatchedFree(parsed);
333 }
334
335 fn recordDuplicate(self: *Analyzer, record: Record) void {
336 const summary = self.summaries.getPtr(record.name).?;
337 self.counters.duplicate_allocations +|= 1;
338 summary.duplicate_allocations +|= 1;
339 }
340
341 fn addLive(self: *Analyzer, summary: *Summary, size: u64, time_ns: u64) void {
342 self.counters.live_bytes +|= size;
343 summary.live_bytes +|= size;
344 summary.active_allocations +|= 1;
345 if (self.counters.live_bytes > self.counters.high_water_live_bytes) {
346 self.counters.high_water_live_bytes = self.counters.live_bytes;
347 self.counters.high_water_time_ns = time_ns;
348 }
349 if (summary.live_bytes > summary.high_water_live_bytes) {
350 summary.high_water_live_bytes = summary.live_bytes;
351 summary.high_water_time_ns = time_ns;
352 }
353 }
354
355 fn removeLive(self: *Analyzer, summary: *Summary, size: u64) void {
356 subtractCounterLive(&self.counters, size);
357 _ = subtractSummaryLive(summary, size);
358 if (summary.active_allocations != 0) summary.active_allocations -= 1;
359 }
360
361 fn recordMatchedFree(self: *Analyzer, parsed: event.Parsed, record: Record) !void {
362 const valid_time = parsed.time_ns >= record.allocated_ns;
363 try self.lifetimes.append(self.allocator, .{
364 .name = record.name,
365 .address = parsed.address,
366 .size = record.size,
367 .reported_free_size = parsed.size,
368 .allocation_thread = record.thread,
369 .free_thread = parsed.thread,
370 .allocated_ns = record.allocated_ns,
371 .freed_ns = parsed.time_ns,
372 .lifetime_ns = duration(record.allocated_ns, parsed.time_ns),
373 .size_match = parsed.size == 0 or parsed.size == record.size,
374 .valid_time = valid_time,
375 });
376 _ = self.active.remove(parsed.address);
377 const summary = self.summaries.getPtr(record.name).?;
378 self.recordLifetime(summary, record, parsed, valid_time);
379 self.removeLive(summary, record.size);
380 }
381
382 fn recordLifetime(
383 self: *Analyzer,
384 summary: *Summary,
385 record: Record,
386 parsed: event.Parsed,
387 valid_time: bool,
388 ) void {
389 self.counters.frees +|= 1;
390 self.counters.completed_lifetimes +|= 1;
391 self.counters.freed_bytes +|= record.size;
392 summary.frees +|= 1;
393 summary.completed_lifetimes +|= 1;
394 summary.freed_bytes +|= record.size;
395 summary.last_event_ns = parsed.time_ns;
396 if (parsed.size != 0 and parsed.size != record.size) self.recordSizeMismatch(summary);
397 if (valid_time) {
398 const lifetime_ns = duration(record.allocated_ns, parsed.time_ns);
399 self.counters.lifetime_samples +|= 1;
400 summary.lifetime_samples +|= 1;
401 summary.total_lifetime_ns +|= lifetime_ns;
402 summary.max_lifetime_ns = @max(summary.max_lifetime_ns, lifetime_ns);
403 } else {
404 self.counters.timestamp_regressions +|= 1;
405 summary.timestamp_regressions +|= 1;
406 }
407 }
408
409 fn recordSizeMismatch(self: *Analyzer, summary: *Summary) void {
410 self.counters.size_mismatches +|= 1;
411 summary.size_mismatches +|= 1;
412 }
413
414 fn recordUnmatchedFree(self: *Analyzer, parsed: event.Parsed) !void {
415 const summary = try self.summaryFor(parsed.name orelse "default");
416 self.counters.frees +|= 1;
417 self.counters.freed_bytes +|= parsed.size;
418 summary.frees +|= 1;
419 summary.freed_bytes +|= parsed.size;
420 summary.last_event_ns = parsed.time_ns;
421 if (parsed.address == 0 and parsed.size == 0) return;
422 self.counters.unmatched_frees +|= 1;
423 summary.unmatched_frees +|= 1;
424 }
425
426 fn summaryFor(self: *Analyzer, name: []const u8) !*Summary {
427 if (self.summaries.getPtr(name)) |summary| return summary;
428 const owned_name = try self.allocator.dupe(u8, name);
429 errdefer self.allocator.free(owned_name);
430 const entry = try self.summaries.getOrPut(self.allocator, owned_name);
431 std.debug.assert(!entry.found_existing);
432 entry.key_ptr.* = owned_name;
433 entry.value_ptr.* = .{ .name = owned_name };
434 return entry.value_ptr;
435 }
436 };
437
438 pub fn writeTextFromJsonlPath(
439 allocator: std.mem.Allocator,
440 path: []const u8,
441 writer: *std.Io.Writer,
442 options: Options,
443 ) !void {
444 return report.writeFromJsonlPath(Analyzer, writeText, allocator, path, writer, options);
445 }
446
447 pub fn writeJsonlFromJsonlPath(
448 allocator: std.mem.Allocator,
449 path: []const u8,
450 writer: *std.Io.Writer,
451 options: Options,
452 ) !void {
453 return report.writeFromJsonlPath(Analyzer, writeJsonl, allocator, path, writer, options);
454 }
455
456 pub fn ingestPath(analyzer: *Analyzer, path: []const u8) !void {
457 return report.ingestJsonlPath(analyzer, path);
458 }
459
460 fn writeText(
461 allocator: std.mem.Allocator,
462 analyzer: *Analyzer,
463 writer: *std.Io.Writer,
464 options: Options,
465 ) !void {
466 var summaries = try analyzer.collectSummaries(allocator, options);
467 defer summaries.deinit(allocator);
468 var lifetimes = try analyzer.collectLifetimes(allocator, options);
469 defer lifetimes.deinit(allocator);
470 var live = try analyzer.collectLive(allocator, options);
471 defer live.deinit(allocator);
472 try writeTextHeader(writer, analyzer, options, summaries.items.len);
473 try capture_mod.writeText(writer, analyzer.captureIntegrity());
474 const summary_limit = @min(options.top, summaries.items.len);
475 for (summaries.items[0..summary_limit]) |summary| try writeTextMemory(writer, summary);
476 for (lifetimes.items) |lifetime| try writeTextLifetime(writer, lifetime);
477 const live_limit = @min(options.top, live.items.len);
478 for (live.items[0..live_limit]) |item| try writeTextLive(writer, item);
479 }
480
481 fn writeTextHeader(
482 writer: *std.Io.Writer,
483 analyzer: *Analyzer,
484 options: Options,
485 names: usize,
486 ) !void {
487 try writer.print(
488 "tracy memory names={d} allocations={d} frees={d} completed_lifetimes={d} " ++
489 "lifetime_samples={d} right_censored_allocations={d} live_bytes={d} " ++
490 "high_water_live_bytes={d} high_water_time_ns={d} allocated_bytes={d} " ++
491 "freed_bytes={d} unmatched_frees={d} duplicate_allocations={d} " ++
492 "timestamp_regressions={d} untracked_allocations={d} size_mismatches={d} " ++
493 "lifetime_population=completed lifetime_evidence={s} duration_ns={d} sort={s}\n",
494 .{
495 names,
496 analyzer.counters.allocations,
497 analyzer.counters.frees,
498 analyzer.counters.completed_lifetimes,
499 analyzer.counters.lifetime_samples,
500 analyzer.active.count(),
501 analyzer.counters.live_bytes,
502 analyzer.counters.high_water_live_bytes,
503 analyzer.counters.high_water_time_ns,
504 analyzer.counters.allocated_bytes,
505 analyzer.counters.freed_bytes,
506 analyzer.counters.unmatched_frees,
507 analyzer.counters.duplicate_allocations,
508 analyzer.counters.timestamp_regressions,
509 analyzer.counters.untracked_allocations,
510 analyzer.counters.size_mismatches,
511 analyzer.lifetimeEvidence(),
512 analyzer.durationNs(),
513 options.sort.tag(),
514 },
515 );
516 }
517
518 fn writeTextMemory(writer: *std.Io.Writer, summary: Summary) !void {
519 try writer.writeAll("memory name=");
520 try pretty_json.writeString(writer, summary.name);
521 try writer.print(
522 " allocations={d} frees={d} right_censored_allocations={d} live_bytes={d} " ++
523 "high_water_live_bytes={d} high_water_time_ns={d} allocated_bytes={d} " ++
524 "freed_bytes={d} mean_alloc_bytes={d} max_alloc_bytes={d}",
525 .{
526 summary.allocations, summary.frees,
527 summary.active_allocations, summary.live_bytes,
528 summary.high_water_live_bytes, summary.high_water_time_ns,
529 summary.allocated_bytes, summary.freed_bytes,
530 summary.meanAllocBytes(), summary.max_alloc_bytes,
531 },
532 );
533 try writer.print(
534 " completed_lifetimes={d} lifetime_samples={d} lifetime_total_ns={d} " ++
535 "lifetime_mean_ns={d} lifetime_min_ns={d} lifetime_p50_ns={d} " ++
536 "lifetime_p90_ns={d} lifetime_p99_ns={d} lifetime_max_ns={d} " ++
537 "unmatched_frees={d} duplicate_allocations={d} timestamp_regressions={d} " ++
538 "untracked_allocations={d} size_mismatches={d} last_event_ns={d}\n",
539 .{
540 summary.completed_lifetimes, summary.lifetime_samples,
541 summary.total_lifetime_ns, summary.meanLifetimeNs(),
542 summary.min_lifetime_ns, summary.p50_lifetime_ns,
543 summary.p90_lifetime_ns, summary.p99_lifetime_ns,
544 summary.max_lifetime_ns, summary.unmatched_frees,
545 summary.duplicate_allocations, summary.timestamp_regressions,
546 summary.untracked_allocations, summary.size_mismatches,
547 summary.last_event_ns,
548 },
549 );
550 }
551
552 fn writeTextLifetime(writer: *std.Io.Writer, lifetime: Lifetime) !void {
553 try writer.writeAll("lifetime name=");
554 try pretty_json.writeString(writer, lifetime.name);
555 try writer.print(
556 " address={d} size={d} reported_free_size={d} size_match={} " ++
557 "allocation_thread={d} free_thread={d} allocated_ns={d} freed_ns={d} " ++
558 "lifetime_ns={d}",
559 .{
560 lifetime.address, lifetime.size,
561 lifetime.reported_free_size, lifetime.size_match,
562 lifetime.allocation_thread, lifetime.free_thread,
563 lifetime.allocated_ns, lifetime.freed_ns,
564 lifetime.lifetime_ns,
565 },
566 );
567 if (!lifetime.valid_time) try writer.writeAll(" valid_time=false");
568 try writer.writeByte('\n');
569 }
570
571 fn writeTextLive(writer: *std.Io.Writer, item: LiveView) !void {
572 try writer.writeAll("live name=");
573 try pretty_json.writeString(writer, item.name);
574 try writer.print(
575 " address={d} size={d} allocated_ns={d} age_ns={d} thread={d}\n",
576 .{ item.address, item.size, item.allocated_ns, item.age_ns, item.thread },
577 );
578 }
579
580 fn writeJsonl(
581 allocator: std.mem.Allocator,
582 analyzer: *Analyzer,
583 writer: *std.Io.Writer,
584 options: Options,
585 ) !void {
586 var summaries = try analyzer.collectSummaries(allocator, options);
587 defer summaries.deinit(allocator);
588 var lifetimes = try analyzer.collectLifetimes(allocator, options);
589 defer lifetimes.deinit(allocator);
590 var live = try analyzer.collectLive(allocator, options);
591 defer live.deinit(allocator);
592 try writeJsonSummary(writer, analyzer, options, summaries.items.len);
593 const summary_limit = @min(options.top, summaries.items.len);
594 for (summaries.items[0..summary_limit]) |summary| try writeJsonMemory(writer, summary);
595 for (lifetimes.items) |lifetime| try writeJsonLifetime(writer, lifetime);
596 const live_limit = @min(options.top, live.items.len);
597 for (live.items[0..live_limit]) |item| try writeJsonLive(writer, item);
598 }
599
600 fn writeJsonSummary(
601 writer: *std.Io.Writer,
602 analyzer: *Analyzer,
603 options: Options,
604 names: usize,
605 ) !void {
606 var stream = pretty_json.Writer.init(writer, .minified);
607 const object = try stream.object();
608 try object.field("schema", schema);
609 try object.field("kind", "summary");
610 try object.field("names", names);
611 try object.field("allocations", analyzer.counters.allocations);
612 try object.field("frees", analyzer.counters.frees);
613 try object.field("completed_lifetimes", analyzer.counters.completed_lifetimes);
614 try object.field("lifetime_samples", analyzer.counters.lifetime_samples);
615 try object.field("right_censored_allocations", analyzer.active.count());
616 try object.field("live_bytes", analyzer.counters.live_bytes);
617 try object.field("high_water_live_bytes", analyzer.counters.high_water_live_bytes);
618 try object.field("high_water_time_ns", analyzer.counters.high_water_time_ns);
619 try object.field("allocated_bytes", analyzer.counters.allocated_bytes);
620 try object.field("freed_bytes", analyzer.counters.freed_bytes);
621 try object.field("unmatched_frees", analyzer.counters.unmatched_frees);
622 try object.field("duplicate_allocations", analyzer.counters.duplicate_allocations);
623 try object.field("timestamp_regressions", analyzer.counters.timestamp_regressions);
624 try object.field("untracked_allocations", analyzer.counters.untracked_allocations);
625 try object.field("size_mismatches", analyzer.counters.size_mismatches);
626 try object.field("duration_ns", analyzer.durationNs());
627 try object.field("lifetime_population", "completed");
628 try object.field("lifetime_evidence", analyzer.lifetimeEvidence());
629 try object.field("sort", options.sort.tag());
630 try capture_mod.writeFields(object, analyzer.captureIntegrity());
631 try object.endLine();
632 }
633
634 fn writeJsonMemory(writer: *std.Io.Writer, summary: Summary) !void {
635 var stream = pretty_json.Writer.init(writer, .minified);
636 const object = try stream.object();
637 try object.field("schema", schema);
638 try object.field("kind", "memory");
639 try object.field("name", summary.name);
640 try object.field("allocations", summary.allocations);
641 try object.field("frees", summary.frees);
642 try object.field("right_censored_allocations", summary.active_allocations);
643 try object.field("live_bytes", summary.live_bytes);
644 try object.field("high_water_live_bytes", summary.high_water_live_bytes);
645 try object.field("high_water_time_ns", summary.high_water_time_ns);
646 try object.field("allocated_bytes", summary.allocated_bytes);
647 try object.field("freed_bytes", summary.freed_bytes);
648 try object.field("mean_alloc_bytes", summary.meanAllocBytes());
649 try object.field("max_alloc_bytes", summary.max_alloc_bytes);
650 try object.field("completed_lifetimes", summary.completed_lifetimes);
651 try object.field("lifetime_samples", summary.lifetime_samples);
652 try object.field("lifetime_total_ns", summary.total_lifetime_ns);
653 try object.field("lifetime_mean_ns", summary.meanLifetimeNs());
654 try object.field("lifetime_min_ns", summary.min_lifetime_ns);
655 try object.field("lifetime_p50_ns", summary.p50_lifetime_ns);
656 try object.field("lifetime_p90_ns", summary.p90_lifetime_ns);
657 try object.field("lifetime_p99_ns", summary.p99_lifetime_ns);
658 try object.field("lifetime_max_ns", summary.max_lifetime_ns);
659 try object.field("unmatched_frees", summary.unmatched_frees);
660 try object.field("duplicate_allocations", summary.duplicate_allocations);
661 try object.field("timestamp_regressions", summary.timestamp_regressions);
662 try object.field("untracked_allocations", summary.untracked_allocations);
663 try object.field("size_mismatches", summary.size_mismatches);
664 try object.field("last_event_ns", summary.last_event_ns);
665 try object.endLine();
666 }
667
668 fn writeJsonLifetime(writer: *std.Io.Writer, lifetime: Lifetime) !void {
669 var stream = pretty_json.Writer.init(writer, .minified);
670 const object = try stream.object();
671 try object.field("schema", schema);
672 try object.field("kind", "lifetime");
673 try object.field("name", lifetime.name);
674 try object.field("address", lifetime.address);
675 try object.field("size", lifetime.size);
676 try object.field("reported_free_size", lifetime.reported_free_size);
677 try object.field("size_match", lifetime.size_match);
678 try object.field("allocation_thread", lifetime.allocation_thread);
679 try object.field("free_thread", lifetime.free_thread);
680 try object.field("allocated_ns", lifetime.allocated_ns);
681 try object.field("freed_ns", lifetime.freed_ns);
682 try object.field("lifetime_ns", lifetime.lifetime_ns);
683 try object.field("valid_time", lifetime.valid_time);
684 try object.endLine();
685 }
686
687 fn writeJsonLive(writer: *std.Io.Writer, item: LiveView) !void {
688 var stream = pretty_json.Writer.init(writer, .minified);
689 const object = try stream.object();
690 try object.field("schema", schema);
691 try object.field("kind", "live");
692 try object.field("name", item.name);
693 try object.field("address", item.address);
694 try object.field("size", item.size);
695 try object.field("allocated_ns", item.allocated_ns);
696 try object.field("age_ns", item.age_ns);
697 try object.field("thread", item.thread);
698 try object.endLine();
699 }
700
701 fn subtractCounterLive(counters: *Counters, size: u64) void {
702 if (counters.live_bytes >= size) {
703 counters.live_bytes -= size;
704 } else {
705 counters.live_bytes = 0;
706 }
707 }
708
709 fn subtractSummaryLive(summary: *Summary, size: u64) u64 {
710 const removed = @min(summary.live_bytes, size);
711 if (summary.live_bytes >= size) {
712 summary.live_bytes -= size;
713 } else {
714 summary.live_bytes = 0;
715 }
716 return removed;
717 }
718
719 fn duration(start_ns: u64, end_ns: u64) u64 {
720 if (end_ns <= start_ns) return 0;
721 return end_ns - start_ns;
722 }
723
724 fn applyLifetimeDistributions(
725 allocator: std.mem.Allocator,
726 rows: []Summary,
727 lifetimes: []const Lifetime,
728 ) !void {
729 var row_index: std.StringHashMapUnmanaged(usize) = .{};
730 defer row_index.deinit(allocator);
731 for (rows, 0..) |row, index| try row_index.put(allocator, row.name, index);
732 var samples: std.ArrayListUnmanaged(LifetimeSample) = .empty;
733 defer samples.deinit(allocator);
734 for (lifetimes) |lifetime| {
735 if (!lifetime.valid_time) continue;
736 const row = row_index.get(lifetime.name) orelse continue;
737 try samples.append(allocator, .{ .row = row, .duration_ns = lifetime.lifetime_ns });
738 }
739 std.mem.sort(LifetimeSample, samples.items, {}, lifetimeSampleLessThan);
740 var start: usize = 0;
741 while (start < samples.items.len) {
742 var end = start + 1;
743 while (end < samples.items.len and samples.items[end].row == samples.items[start].row) {
744 end += 1;
745 }
746 applyLifetimeDistribution(&rows[samples.items[start].row], samples.items[start..end]);
747 start = end;
748 }
749 }
750
751 fn applyLifetimeDistribution(summary: *Summary, samples: []const LifetimeSample) void {
752 std.debug.assert(samples.len > 0);
753 std.debug.assert(summary.lifetime_samples == samples.len);
754 summary.min_lifetime_ns = lifetimePercentile(samples, 0);
755 summary.p50_lifetime_ns = lifetimePercentile(samples, 50);
756 summary.p90_lifetime_ns = lifetimePercentile(samples, 90);
757 summary.p99_lifetime_ns = lifetimePercentile(samples, 99);
758 summary.max_lifetime_ns = lifetimePercentile(samples, 100);
759 }
760
761 fn lifetimePercentile(samples: []const LifetimeSample, percent: u64) u64 {
762 std.debug.assert(samples.len > 0);
763 const rank: usize = @intCast((@as(u128, @min(percent, 100)) * samples.len + 99) / 100);
764 const index = @min(@max(rank, 1) - 1, samples.len - 1);
765 return samples[index].duration_ns;
766 }
767
768 fn lifetimeSampleLessThan(_: void, left: LifetimeSample, right: LifetimeSample) bool {
769 if (left.row != right.row) return left.row < right.row;
770 return left.duration_ns < right.duration_ns;
771 }
772
773 fn sortSummaries(items: []Summary, sort: Sort) void {
774 std.mem.sort(Summary, items, sort, summaryLessThan);
775 }
776
777 fn summaryLessThan(sort: Sort, left: Summary, right: Summary) bool {
778 return switch (sort) {
779 .live => summaryLiveGreaterThan({}, left, right),
780 .high_water => summaryHighGreaterThan({}, left, right),
781 .allocated => summaryAllocatedGreaterThan({}, left, right),
782 .lifetime_tail => summaryLifetimeGreaterThan({}, left, right),
783 .allocs => summaryAllocsGreaterThan({}, left, right),
784 .frees => summaryFreesGreaterThan({}, left, right),
785 .name => summaryNameLessThan({}, left, right),
786 };
787 }
788
789 fn summaryLiveGreaterThan(_: void, left: Summary, right: Summary) bool {
790 if (left.live_bytes != right.live_bytes) return left.live_bytes > right.live_bytes;
791 return summaryHighGreaterThan({}, left, right);
792 }
793
794 fn summaryHighGreaterThan(_: void, left: Summary, right: Summary) bool {
795 if (left.high_water_live_bytes != right.high_water_live_bytes) return left.high_water_live_bytes > right.high_water_live_bytes;
796 return summaryNameLessThan({}, left, right);
797 }
798
799 fn summaryAllocatedGreaterThan(_: void, left: Summary, right: Summary) bool {
800 if (left.allocated_bytes != right.allocated_bytes) return left.allocated_bytes > right.allocated_bytes;
801 return summaryNameLessThan({}, left, right);
802 }
803
804 fn summaryLifetimeGreaterThan(_: void, left: Summary, right: Summary) bool {
805 if (left.p99_lifetime_ns != right.p99_lifetime_ns) {
806 return left.p99_lifetime_ns > right.p99_lifetime_ns;
807 }
808 if (left.max_lifetime_ns != right.max_lifetime_ns) {
809 return left.max_lifetime_ns > right.max_lifetime_ns;
810 }
811 return summaryNameLessThan({}, left, right);
812 }
813
814 fn summaryAllocsGreaterThan(_: void, left: Summary, right: Summary) bool {
815 if (left.allocations != right.allocations) return left.allocations > right.allocations;
816 return summaryAllocatedGreaterThan({}, left, right);
817 }
818
819 fn summaryFreesGreaterThan(_: void, left: Summary, right: Summary) bool {
820 if (left.frees != right.frees) return left.frees > right.frees;
821 return summaryAllocatedGreaterThan({}, left, right);
822 }
823
824 fn summaryNameLessThan(_: void, left: Summary, right: Summary) bool {
825 return std.mem.lessThan(u8, left.name, right.name);
826 }
827
828 fn liveGreaterThan(_: void, left: LiveView, right: LiveView) bool {
829 if (left.size != right.size) return left.size > right.size;
830 const name_cmp = std.mem.order(u8, left.name, right.name);
831 if (name_cmp != .eq) return name_cmp == .lt;
832 return left.address < right.address;
833 }
834
835 fn lifetimeGreaterThan(_: void, left: Lifetime, right: Lifetime) bool {
836 if (left.lifetime_ns != right.lifetime_ns) return left.lifetime_ns > right.lifetime_ns;
837 if (left.valid_time != right.valid_time) return left.valid_time;
838 const name_cmp = std.mem.order(u8, left.name, right.name);
839 if (name_cmp != .eq) return name_cmp == .lt;
840 return left.address < right.address;
841 }
842
843 test "memory tracks live allocations and high water by name" {
844 var trace = std.Io.Writer.Allocating.init(std.testing.allocator);
845 defer trace.deinit();
846 try (event.TraceEvent{ .seq = 1, .kind = .start, .time_ns = 90, .thread = 1, .name = "test" }).writeJsonLine(&trace.writer);
847 try (event.TraceEvent{ .seq = 2, .kind = .alloc, .time_ns = 100, .thread = 1, .name = "arena", .address = 4096, .size = 64 }).writeJsonLine(&trace.writer);
848 try (event.TraceEvent{ .seq = 3, .kind = .alloc, .time_ns = 110, .thread = 2, .name = "arena", .address = 8192, .size = 32 }).writeJsonLine(&trace.writer);
849 try (event.TraceEvent{ .seq = 4, .kind = .free, .time_ns = 120, .thread = 1, .name = "arena", .address = 4096 }).writeJsonLine(&trace.writer);
850 try (event.TraceEvent{ .seq = 5, .kind = .free, .time_ns = 130, .thread = 1, .name = "external", .address = 12288, .size = 16 }).writeJsonLine(&trace.writer);
851 try (event.TraceEvent{ .seq = 6, .kind = .free, .time_ns = 140, .thread = 1, .name = "arena", .address = 16384 }).writeJsonLine(&trace.writer);
852 try (event.TraceEvent{ .seq = 7, .kind = .stop, .time_ns = 160, .thread = 1 }).writeJsonLine(&trace.writer);
853
854 var analyzer = Analyzer.init(std.testing.allocator);
855 defer analyzer.deinit();
856 try analyzer.ingestJsonlBytes(trace.written());
857 try std.testing.expectEqual(@as(u64, 2), analyzer.counters.allocations);
858 try std.testing.expectEqual(@as(u64, 3), analyzer.counters.frees);
859 try std.testing.expectEqual(@as(u64, 2), analyzer.counters.unmatched_frees);
860 try std.testing.expectEqual(@as(u64, 96), analyzer.counters.allocated_bytes);
861 try std.testing.expectEqual(@as(u64, 80), analyzer.counters.freed_bytes);
862 try std.testing.expectEqual(@as(u64, 32), analyzer.counters.live_bytes);
863 try std.testing.expectEqual(@as(u64, 96), analyzer.counters.high_water_live_bytes);
864 try std.testing.expectEqual(@as(u64, 110), analyzer.counters.high_water_time_ns);
865 try std.testing.expectEqual(@as(usize, 1), analyzer.active.count());
866
867 const arena = analyzer.summaries.get("arena").?;
868 try std.testing.expectEqual(@as(u64, 32), arena.live_bytes);
869 try std.testing.expectEqual(@as(u64, 96), arena.high_water_live_bytes);
870 try std.testing.expectEqual(@as(u64, 1), arena.unmatched_frees);
871 try std.testing.expectEqual(@as(u64, 48), arena.meanAllocBytes());
872
873 var out = std.Io.Writer.Allocating.init(std.testing.allocator);
874 defer out.deinit();
875 try writeText(std.testing.allocator, &analyzer, &out.writer, .{ .top = 4 });
876 const text = out.written();
877 try std.testing.expect(std.mem.indexOf(
878 u8,
879 text,
880 "completed_lifetimes=1 lifetime_samples=1",
881 ) != null);
882 try std.testing.expect(std.mem.indexOf(
883 u8,
884 text,
885 "right_censored_allocations=1 live_bytes=32",
886 ) != null);
887 try std.testing.expect(std.mem.indexOf(u8, text, "live name=\"arena\" address=8192 size=32 allocated_ns=110 age_ns=50 thread=2") != null);
888 }
889
890 test "memory jsonl filters names and live rows" {
891 var trace = std.Io.Writer.Allocating.init(std.testing.allocator);
892 defer trace.deinit();
893 try (event.TraceEvent{ .seq = 1, .kind = .alloc, .time_ns = 100, .thread = 1, .name = "arena", .address = 4096, .size = 64 }).writeJsonLine(&trace.writer);
894 try (event.TraceEvent{ .seq = 2, .kind = .alloc, .time_ns = 110, .thread = 1, .name = "scratch", .address = 8192, .size = 8 }).writeJsonLine(&trace.writer);
895 try (event.TraceEvent{ .seq = 3, .kind = .stop, .time_ns = 150, .thread = 1 }).writeJsonLine(&trace.writer);
896
897 var analyzer = Analyzer.init(std.testing.allocator);
898 defer analyzer.deinit();
899 try analyzer.ingestJsonlBytes(trace.written());
900
901 var out = std.Io.Writer.Allocating.init(std.testing.allocator);
902 defer out.deinit();
903 try writeJsonl(std.testing.allocator, &analyzer, &out.writer, .{ .top = 4, .name = "arena", .min_live_bytes = 32 });
904 const text = out.written();
905 try std.testing.expect(std.mem.indexOf(u8, text, "\"schema\":\"tracy.memory/v1\"") != null);
906 try std.testing.expect(std.mem.indexOf(u8, text, "\"kind\":\"memory\"") != null);
907 try std.testing.expect(std.mem.indexOf(u8, text, "\"name\":\"arena\"") != null);
908 try std.testing.expect(std.mem.indexOf(u8, text, "\"kind\":\"live\"") != null);
909 try std.testing.expect(std.mem.indexOf(u8, text, "\"address\":4096") != null);
910 try std.testing.expect(std.mem.indexOf(u8, text, "\"name\":\"scratch\"") == null);
911 }
912
913 test "memory handles duplicate allocation addresses as anomalies" {
914 var trace = std.Io.Writer.Allocating.init(std.testing.allocator);
915 defer trace.deinit();
916 try (event.TraceEvent{ .seq = 1, .kind = .alloc, .time_ns = 100, .thread = 1, .name = "arena", .address = 4096, .size = 64 }).writeJsonLine(&trace.writer);
917 try (event.TraceEvent{ .seq = 2, .kind = .alloc, .time_ns = 110, .thread = 1, .name = "arena", .address = 4096, .size = 16 }).writeJsonLine(&trace.writer);
918
919 var analyzer = Analyzer.init(std.testing.allocator);
920 defer analyzer.deinit();
921 try analyzer.ingestJsonlBytes(trace.written());
922 try std.testing.expectEqual(@as(u64, 1), analyzer.counters.duplicate_allocations);
923 try std.testing.expectEqual(@as(u64, 64), analyzer.counters.live_bytes);
924 try std.testing.expectEqual(@as(usize, 1), analyzer.active.count());
925 }
926
927 test "memory preserves completed lifetime distributions and worst occurrences" {
928 var trace = std.Io.Writer.Allocating.init(std.testing.allocator);
929 defer trace.deinit();
930 var seq: u64 = 1;
931 try (event.TraceEvent{ .seq = seq, .kind = .start, .time_ns = 1 })
932 .writeJsonLine(&trace.writer);
933 seq += 1;
934 var time_ns: u64 = 100;
935 for ([_]u64{ 10, 20, 30, 40, 100 }, 0..) |lifetime_ns, index| {
936 try appendLifetimePair(
937 &trace.writer,
938 &seq,
939 "arena",
940 @intCast(index + 1),
941 time_ns,
942 lifetime_ns,
943 16,
944 );
945 time_ns += lifetime_ns + 10;
946 }
947 try appendLifetimePair(&trace.writer, &seq, "cache", 20, time_ns, 200, 64);
948 try (event.TraceEvent{ .seq = seq, .kind = .stop, .time_ns = time_ns + 210 })
949 .writeJsonLine(&trace.writer);
950
951 var analyzer = Analyzer.init(std.testing.allocator);
952 defer analyzer.deinit();
953 try analyzer.ingestJsonlBytes(trace.written());
954 try std.testing.expectEqual(@as(u64, 6), analyzer.counters.completed_lifetimes);
955 try std.testing.expectEqual(@as(u64, 6), analyzer.counters.lifetime_samples);
956 try std.testing.expectEqualStrings("complete", analyzer.lifetimeEvidence());
957
958 var rows = try analyzer.collectSummaries(
959 std.testing.allocator,
960 .{ .sort = .lifetime_tail },
961 );
962 defer rows.deinit(std.testing.allocator);
963 try std.testing.expectEqualStrings("cache", rows.items[0].name);
964 const arena = rows.items[1];
965 try std.testing.expectEqual(@as(u64, 5), arena.lifetime_samples);
966 try std.testing.expectEqual(@as(u64, 200), arena.total_lifetime_ns);
967 try std.testing.expectEqual(@as(u64, 40), arena.meanLifetimeNs());
968 try std.testing.expectEqual(@as(u64, 10), arena.min_lifetime_ns);
969 try std.testing.expectEqual(@as(u64, 30), arena.p50_lifetime_ns);
970 try std.testing.expectEqual(@as(u64, 100), arena.p90_lifetime_ns);
971 try std.testing.expectEqual(@as(u64, 100), arena.p99_lifetime_ns);
972 try std.testing.expectEqual(@as(u64, 100), arena.max_lifetime_ns);
973
974 var lifetimes = try analyzer.collectLifetimes(
975 std.testing.allocator,
976 .{ .occurrences = 2 },
977 );
978 defer lifetimes.deinit(std.testing.allocator);
979 try std.testing.expectEqual(@as(usize, 2), lifetimes.items.len);
980 try std.testing.expectEqual(@as(u64, 200), lifetimes.items[0].lifetime_ns);
981 try std.testing.expectEqual(@as(u64, 100), lifetimes.items[1].lifetime_ns);
982 }
983
984 test "memory separates censored allocations and invalid lifetime evidence" {
985 var trace = std.Io.Writer.Allocating.init(std.testing.allocator);
986 defer trace.deinit();
987 try (event.TraceEvent{ .seq = 1, .kind = .start, .time_ns = 1 })
988 .writeJsonLine(&trace.writer);
989 try appendMemoryEvent(&trace.writer, 2, .alloc, 100, 1, 64);
990 try appendMemoryEvent(&trace.writer, 3, .alloc, 110, 1, 16);
991 try appendMemoryEvent(&trace.writer, 4, .free, 90, 1, 32);
992 try appendMemoryEvent(&trace.writer, 5, .alloc, 120, 2, 32);
993 try appendMemoryEvent(&trace.writer, 6, .alloc, 130, 0, 8);
994 try appendMemoryEvent(&trace.writer, 7, .free, 140, 3, 4);
995 try (event.TraceEvent{ .seq = 8, .kind = .stop, .time_ns = 150 })
996 .writeJsonLine(&trace.writer);
997
998 var analyzer = Analyzer.init(std.testing.allocator);
999 defer analyzer.deinit();
1000 try analyzer.ingestJsonlBytes(trace.written());
1001 try std.testing.expectEqual(@as(u64, 1), analyzer.counters.completed_lifetimes);
1002 try std.testing.expectEqual(@as(u64, 0), analyzer.counters.lifetime_samples);
1003 try std.testing.expectEqual(@as(u64, 1), analyzer.counters.duplicate_allocations);
1004 try std.testing.expectEqual(@as(u64, 1), analyzer.counters.timestamp_regressions);
1005 try std.testing.expectEqual(@as(u64, 1), analyzer.counters.untracked_allocations);
1006 try std.testing.expectEqual(@as(u64, 1), analyzer.counters.size_mismatches);
1007 try std.testing.expectEqual(@as(u64, 1), analyzer.counters.unmatched_frees);
1008 try std.testing.expectEqual(@as(u64, 32), analyzer.counters.live_bytes);
1009 try std.testing.expectEqual(@as(usize, 1), analyzer.active.count());
1010 try std.testing.expectEqualStrings("partial", analyzer.lifetimeEvidence());
1011
1012 var out = std.Io.Writer.Allocating.init(std.testing.allocator);
1013 defer out.deinit();
1014 try writeJsonl(std.testing.allocator, &analyzer, &out.writer, .{});
1015 try expectMemoryJsonl(out.written());
1016 try expectMemoryContains(out.written(), "\"right_censored_allocations\":1");
1017 try expectMemoryContains(out.written(), "\"lifetime_evidence\":\"partial\"");
1018 try expectMemoryContains(out.written(), "\"size_match\":false");
1019 try expectMemoryContains(out.written(), "\"valid_time\":false");
1020 }
1021
1022 test "memory retains flight reports and sequence gaps" {
1023 var trace = std.Io.Writer.Allocating.init(std.testing.allocator);
1024 defer trace.deinit();
1025 try (event.TraceEvent{ .seq = 1, .kind = .start, .time_ns = 1 })
1026 .writeJsonLine(&trace.writer);
1027 try (event.TraceEvent{ .seq = 3, .kind = .alloc, .time_ns = 10, .address = 1, .size = 8 })
1028 .writeJsonLine(&trace.writer);
1029 try (event.TraceEvent{ .seq = 4, .kind = .free, .time_ns = 20, .address = 1 })
1030 .writeJsonLine(&trace.writer);
1031 try (event.TraceEvent{ .seq = 5, .kind = .stop, .time_ns = 30 })
1032 .writeJsonLine(&trace.writer);
1033 const flight_report = memoryTestFlightReport();
1034 try flight_report.writeJsonl(&trace.writer);
1035
1036 var analyzer = Analyzer.init(std.testing.allocator);
1037 defer analyzer.deinit();
1038 try analyzer.ingestJsonlBytes(trace.written());
1039 const integrity = analyzer.captureIntegrity();
1040 try std.testing.expectEqualStrings("sequence_gaps", integrity.status);
1041 try std.testing.expectEqualDeep(flight_report, integrity.flight_report.?);
1042 try std.testing.expectEqualStrings("partial", analyzer.lifetimeEvidence());
1043 }
1044
1045 test "memory releases lifetime evidence on allocation failure" {
1046 try std.testing.checkAllAllocationFailures(
1047 std.testing.allocator,
1048 analyzeMemoryLifetimes,
1049 .{},
1050 );
1051 }
1052
1053 fn analyzeMemoryLifetimes(allocator: std.mem.Allocator) !void {
1054 var trace = std.Io.Writer.Allocating.init(std.testing.allocator);
1055 defer trace.deinit();
1056 var seq: u64 = 1;
1057 try (event.TraceEvent{ .seq = seq, .kind = .start, .time_ns = 1 })
1058 .writeJsonLine(&trace.writer);
1059 seq += 1;
1060 try appendLifetimePair(&trace.writer, &seq, "arena", 1, 10, 20, 64);
1061 try appendLifetimePair(&trace.writer, &seq, "arena", 2, 40, 30, 32);
1062 try (event.TraceEvent{ .seq = seq, .kind = .stop, .time_ns = 80 })
1063 .writeJsonLine(&trace.writer);
1064 try memoryTestFlightReport().writeJsonl(&trace.writer);
1065
1066 var analyzer = Analyzer.init(allocator);
1067 defer analyzer.deinit();
1068 try analyzer.ingestJsonlBytes(trace.written());
1069 var out = std.Io.Writer.Allocating.init(std.testing.allocator);
1070 defer out.deinit();
1071 try writeJsonl(allocator, &analyzer, &out.writer, .{ .sort = .lifetime_tail });
1072 }
1073
1074 fn appendLifetimePair(
1075 writer: *std.Io.Writer,
1076 seq: *u64,
1077 name: []const u8,
1078 address: u64,
1079 allocated_ns: u64,
1080 lifetime_ns: u64,
1081 size: u64,
1082 ) !void {
1083 try (event.TraceEvent{
1084 .seq = seq.*,
1085 .kind = .alloc,
1086 .time_ns = allocated_ns,
1087 .thread = 1,
1088 .name = name,
1089 .address = address,
1090 .size = size,
1091 }).writeJsonLine(writer);
1092 seq.* += 1;
1093 try (event.TraceEvent{
1094 .seq = seq.*,
1095 .kind = .free,
1096 .time_ns = allocated_ns + lifetime_ns,
1097 .thread = 2,
1098 .name = name,
1099 .address = address,
1100 .size = size,
1101 }).writeJsonLine(writer);
1102 seq.* += 1;
1103 }
1104
1105 fn appendMemoryEvent(
1106 writer: *std.Io.Writer,
1107 seq: u64,
1108 kind: event.Kind,
1109 time_ns: u64,
1110 address: u64,
1111 size: u64,
1112 ) !void {
1113 try (event.TraceEvent{
1114 .seq = seq,
1115 .kind = kind,
1116 .time_ns = time_ns,
1117 .thread = if (kind == .alloc) 1 else 2,
1118 .name = "arena",
1119 .address = address,
1120 .size = size,
1121 }).writeJsonLine(writer);
1122 }
1123
1124 fn memoryTestFlightReport() transport.Report {
1125 return .{
1126 .policy = .overwrite_oldest,
1127 .state = .accepting,
1128 .capacity_bytes = 64,
1129 .retained_bytes = 32,
1130 .event_capacity_bytes = 16,
1131 .writer_capacity_bytes = 8,
1132 .observed_events = 5,
1133 .stored_events = 5,
1134 .retained_events = 4,
1135 .overwritten_events = 1,
1136 .dropped_events = 0,
1137 .oversized_events = 0,
1138 .partial_event_bytes = 0,
1139 .discarding_oversized_event = false,
1140 };
1141 }
1142
1143 fn expectMemoryContains(haystack: []const u8, needle: []const u8) !void {
1144 try std.testing.expect(std.mem.indexOf(u8, haystack, needle) != null);
1145 }
1146
1147 fn expectMemoryJsonl(bytes: []const u8) !void {
1148 var lines = std.mem.splitScalar(u8, bytes, '\n');
1149 while (lines.next()) |line| {
1150 if (line.len == 0) continue;
1151 var parsed = try std.json.parseFromSlice(
1152 std.json.Value,
1153 std.testing.allocator,
1154 line,
1155 .{},
1156 );
1157 parsed.deinit();
1158 }
1159 }