lib/tracy/src/summary.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const pretty_json = @import("pretty").json;
3 const sys = @import("sys");
4 const capture_mod = @import("capture.zig");
5 const context_mod = @import("context.zig");
6 const event = @import("event.zig");
7 const record_mod = @import("record.zig");
8 const fiber_mod = @import("fiber.zig");
9 const gpu_mod = @import("gpu.zig");
10 const lock_mod = @import("lock.zig");
11 const memory_mod = @import("memory.zig");
12 const plot_mod = @import("plot.zig");
13 const sample_mod = @import("sample.zig");
14 const system_mod = @import("system.zig");
15 const transport = @import("transport.zig");
16
17 pub const Options = struct {
18 top: usize = 20,
19 };
20
21 pub const Counters = struct {
22 started_zones: u64 = 0,
23 completed_zones: u64 = 0,
24 zone_duration_samples: u64 = 0,
25 zone_timestamp_regressions: u64 = 0,
26 unmatched_zone_ends: u64 = 0,
27 messages: u64 = 0,
28 frames: u64 = 0,
29 plots: u64 = 0,
30 plot_configurations: u64 = 0,
31 plot_configuration_conflicts: u64 = 0,
32 app_infos: u64 = 0,
33 thread_names: u64 = 0,
34 samples: u64 = 0,
35 sample_frames: u64 = 0,
36 fiber_enters: u64 = 0,
37 fiber_leaves: u64 = 0,
38 fiber_names: u64 = 0,
39 fiber_unmatched_leaves: u64 = 0,
40 gpu_contexts: u64 = 0,
41 gpu_zones: u64 = 0,
42 gpu_completed_zones: u64 = 0,
43 gpu_times: u64 = 0,
44 gpu_annotations: u64 = 0,
45 gpu_unmatched_ends: u64 = 0,
46 gpu_unmatched_times: u64 = 0,
47 gpu_duplicate_queries: u64 = 0,
48 gpu_duplicate_times: u64 = 0,
49 gpu_duration_samples: u64 = 0,
50 gpu_cpu_duration_samples: u64 = 0,
51 gpu_timestamp_regressions: u64 = 0,
52 gpu_cpu_timestamp_regressions: u64 = 0,
53 system_thread_contexts: u64 = 0,
54 system_thread_pid_maps: u64 = 0,
55 system_cpu_topology: u64 = 0,
56 system_sys_time: u64 = 0,
57 system_sys_power: u64 = 0,
58 system_computed_power: u64 = 0,
59 system_hw_samples: u64 = 0,
60 context_switches: u64 = 0,
61 thread_wakeups: u64 = 0,
62 lock_events: u64 = 0,
63 };
64
65 pub const CaptureIntegrity = capture_mod.Integrity;
66
67 const ActiveZone = struct {
68 name: []u8,
69 file: ?[]u8 = null,
70 function: ?[]u8 = null,
71 line: u32 = 0,
72 thread: u64 = 0,
73 start_ns: u64 = 0,
74 value: ?u64 = null,
75 color: ?u32 = null,
76
77 fn deinit(self: *ActiveZone, allocator: std.mem.Allocator) void {
78 allocator.free(self.name);
79 if (self.file) |file| allocator.free(file);
80 if (self.function) |function| allocator.free(function);
81 self.* = undefined;
82 }
83 };
84
85 pub const ZoneSummary = struct {
86 name: []const u8,
87 file: ?[]const u8 = null,
88 function: ?[]const u8 = null,
89 line: u32 = 0,
90 count: u64 = 0,
91 duration_samples: u64 = 0,
92 total_ns: u64 = 0,
93 min_ns: u64 = std.math.maxInt(u64),
94 max_ns: u64 = 0,
95 durations_ns: std.ArrayListUnmanaged(u64) = .empty,
96
97 pub fn meanNs(self: ZoneSummary) u64 {
98 if (self.duration_samples == 0) return 0;
99 return self.total_ns / self.duration_samples;
100 }
101
102 pub fn percentileNs(self: ZoneSummary, percentile: u8) u64 {
103 if (self.durations_ns.items.len == 0) return 0;
104 const capped = @min(percentile, 100);
105 const top_index = self.durations_ns.items.len - 1;
106 const index = (top_index * capped) / 100;
107 return self.durations_ns.items[index];
108 }
109 };
110
111 pub const ZonePrefixSummary = struct {
112 name: []const u8,
113 count: u64 = 0,
114 duration_samples: u64 = 0,
115 total_ns: u64 = 0,
116 min_ns: u64 = std.math.maxInt(u64),
117 max_ns: u64 = 0,
118
119 pub fn meanNs(self: ZonePrefixSummary) u64 {
120 if (self.duration_samples == 0) return 0;
121 return self.total_ns / self.duration_samples;
122 }
123 };
124
125 pub const ZoneThreadSummary = struct {
126 thread: u64,
127 name: ?[]const u8 = null,
128 count: u64 = 0,
129 duration_samples: u64 = 0,
130 total_ns: u64 = 0,
131 min_ns: u64 = std.math.maxInt(u64),
132 max_ns: u64 = 0,
133
134 pub fn meanNs(self: ZoneThreadSummary) u64 {
135 if (self.duration_samples == 0) return 0;
136 return self.total_ns / self.duration_samples;
137 }
138 };
139
140 pub const PlotSummary = plot_mod.Summary;
141 pub const MemorySummary = memory_mod.Summary;
142 pub const SampleSummary = sample_mod.Summary;
143 pub const LockSummary = lock_mod.Summary;
144 pub const ContextSummary = context_mod.Summary;
145 pub const FiberSummary = fiber_mod.Summary;
146 pub const GpuSummary = gpu_mod.Summary;
147 pub const SystemSummary = system_mod.Summary;
148
149 pub const Analyzer = struct {
150 allocator: std.mem.Allocator,
151 capture: capture_mod.Tracker = .{},
152 active_zones: std.AutoHashMapUnmanaged(u64, ActiveZone) = .{},
153 zone_summaries: std.StringHashMapUnmanaged(ZoneSummary) = .{},
154 zone_prefix_summaries: std.StringHashMapUnmanaged(ZonePrefixSummary) = .{},
155 zone_thread_summaries: std.AutoHashMapUnmanaged(u64, ZoneThreadSummary) = .{},
156 plots: plot_mod.Analyzer,
157 memory: memory_mod.Analyzer,
158 samples: sample_mod.Analyzer,
159 fibers: fiber_mod.Analyzer,
160 gpu: gpu_mod.Analyzer,
161 system: system_mod.Analyzer,
162 context: context_mod.Analyzer,
163 locks: lock_mod.Analyzer,
164 counters: Counters = .{},
165 start_ns: ?u64 = null,
166 end_ns: ?u64 = null,
167
168 pub fn init(allocator: std.mem.Allocator) Analyzer {
169 return .{
170 .allocator = allocator,
171 .plots = plot_mod.Analyzer.init(allocator),
172 .memory = memory_mod.Analyzer.init(allocator),
173 .samples = sample_mod.Analyzer.init(allocator),
174 .fibers = fiber_mod.Analyzer.init(allocator),
175 .gpu = gpu_mod.Analyzer.init(allocator),
176 .system = system_mod.Analyzer.init(allocator),
177 .context = context_mod.Analyzer.init(allocator),
178 .locks = lock_mod.Analyzer.init(allocator),
179 };
180 }
181
182 pub fn deinit(self: *Analyzer) void {
183 var active_iter = self.active_zones.valueIterator();
184 while (active_iter.next()) |zone| zone.deinit(self.allocator);
185 self.active_zones.deinit(self.allocator);
186
187 var zone_iter = self.zone_summaries.iterator();
188 while (zone_iter.next()) |entry| {
189 self.allocator.free(entry.key_ptr.*);
190 if (entry.value_ptr.file) |file| self.allocator.free(file);
191 if (entry.value_ptr.function) |function| self.allocator.free(function);
192 entry.value_ptr.durations_ns.deinit(self.allocator);
193 }
194 self.zone_summaries.deinit(self.allocator);
195
196 var prefix_iter = self.zone_prefix_summaries.iterator();
197 while (prefix_iter.next()) |entry| self.allocator.free(entry.key_ptr.*);
198 self.zone_prefix_summaries.deinit(self.allocator);
199
200 var thread_iter = self.zone_thread_summaries.valueIterator();
201 while (thread_iter.next()) |entry| {
202 if (entry.name) |name| self.allocator.free(name);
203 }
204 self.zone_thread_summaries.deinit(self.allocator);
205
206 self.plots.deinit();
207 self.memory.deinit();
208 self.samples.deinit();
209 self.fibers.deinit();
210 self.gpu.deinit();
211 self.system.deinit();
212 self.context.deinit();
213 self.locks.deinit();
214 self.* = undefined;
215 }
216
217 pub fn ingestJsonlBytes(self: *Analyzer, bytes: []const u8) !void {
218 var lines = std.mem.splitScalar(u8, bytes, '\n');
219 while (lines.next()) |line| try self.ingestJsonLine(line);
220 }
221
222 pub fn ingestJsonLine(self: *Analyzer, line: []const u8) !void {
223 const text = std.mem.trim(u8, line, " \t\r\n");
224 if (text.len == 0) return;
225 var parsed = try record_mod.parseLine(self.allocator, text);
226 defer parsed.deinit();
227 switch (parsed) {
228 .event => |value| try self.ingest(value),
229 .flight => |report_value| {
230 self.capture.recordFlightReport(report_value);
231 self.plots.recordFlightReport(report_value);
232 self.memory.recordFlightReport(report_value);
233 self.context.recordFlightReport(report_value);
234 self.locks.recordFlightReport(report_value);
235 },
236 }
237 }
238
239 pub fn ingest(self: *Analyzer, parsed: event.Parsed) !void {
240 self.capture.record(parsed);
241 try self.context.ingest(parsed);
242 try self.plots.ingest(parsed);
243 try self.memory.ingest(parsed);
244 try self.locks.ingest(parsed);
245 self.counters.plots = self.plots.counters.samples;
246 self.counters.plot_configurations = self.plots.counters.configurations;
247 self.counters.plot_configuration_conflicts =
248 self.plots.counters.configuration_conflicts;
249 if (self.start_ns == null and parsed.time_ns != 0) self.start_ns = parsed.time_ns;
250 if (parsed.time_ns != 0) self.end_ns = parsed.time_ns;
251 switch (parsed.kind) {
252 .start => {
253 if (parsed.time_ns != 0) self.start_ns = parsed.time_ns;
254 },
255 .stop => {
256 if (parsed.time_ns != 0) self.end_ns = parsed.time_ns;
257 },
258 .zone_begin => try self.recordZoneBegin(parsed),
259 .zone_end => try self.recordZoneEnd(parsed),
260 .zone_name => try self.recordZoneName(parsed),
261 .zone_color => self.recordZoneColor(parsed),
262 .zone_value => self.recordZoneValue(parsed),
263 .message => self.counters.messages += 1,
264 .frame => self.counters.frames += 1,
265 .plot, .plot_config => {},
266 .app_info => self.counters.app_infos += 1,
267 .thread_name => {
268 self.counters.thread_names += 1;
269 try self.recordThreadName(parsed);
270 try self.fibers.ingest(parsed);
271 },
272 .fiber_name, .fiber_enter, .fiber_leave => {
273 try self.fibers.ingest(parsed);
274 self.counters.fiber_enters = self.fibers.counters.enters;
275 self.counters.fiber_leaves = self.fibers.counters.leaves;
276 self.counters.fiber_names = self.fibers.counters.names;
277 self.counters.fiber_unmatched_leaves = self.fibers.counters.unmatched_leaves;
278 },
279 .gpu_context,
280 .gpu_context_name,
281 .gpu_zone_begin,
282 .gpu_zone_end,
283 .gpu_time,
284 .gpu_calibration,
285 .gpu_time_sync,
286 .gpu_annotation,
287 .gpu_annotation_name,
288 => {
289 try self.gpu.ingest(parsed);
290 self.counters.gpu_contexts = @intCast(self.gpu.contexts.count());
291 self.counters.gpu_zones = self.gpu.counters.zone_begins;
292 self.counters.gpu_completed_zones = self.gpu.counters.completed_zones;
293 self.counters.gpu_times = self.gpu.counters.gpu_times;
294 self.counters.gpu_annotations = self.gpu.counters.annotations;
295 self.counters.gpu_unmatched_ends = self.gpu.counters.unmatched_ends;
296 self.counters.gpu_unmatched_times = self.gpu.counters.unmatched_times;
297 self.counters.gpu_duplicate_queries = self.gpu.counters.duplicate_queries;
298 self.counters.gpu_duplicate_times = self.gpu.counters.duplicate_times;
299 self.counters.gpu_duration_samples = self.gpu.counters.gpu_duration_samples;
300 self.counters.gpu_cpu_duration_samples = self.gpu.counters.cpu_duration_samples;
301 self.counters.gpu_timestamp_regressions =
302 self.gpu.counters.gpu_timestamp_regressions;
303 self.counters.gpu_cpu_timestamp_regressions =
304 self.gpu.counters.cpu_timestamp_regressions;
305 },
306 .thread_context,
307 .thread_pid,
308 .cpu_topology,
309 .sys_time,
310 .sys_power,
311 .hw_sample,
312 => {
313 try self.system.ingest(parsed);
314 self.counters.system_thread_contexts = self.system.counters.thread_contexts;
315 self.counters.system_thread_pid_maps = self.system.counters.thread_pid_maps;
316 self.counters.system_cpu_topology = self.system.counters.cpu_topology;
317 self.counters.system_sys_time = self.system.counters.sys_time;
318 self.counters.system_sys_power = self.system.counters.sys_power;
319 self.counters.system_computed_power = self.system.counters.computed_power;
320 self.counters.system_hw_samples = self.system.counters.hw_samples;
321 },
322 .sample, .sample_frame => {
323 try self.samples.ingest(parsed);
324 self.counters.samples = self.samples.counters.samples;
325 self.counters.sample_frames = self.samples.counters.frames;
326 },
327 .context_switch, .thread_wakeup => {
328 self.counters.context_switches = self.context.counters.switches;
329 self.counters.thread_wakeups = self.context.counters.wakeups;
330 },
331 .alloc, .free => {},
332 .lock_announce,
333 .lock_terminate,
334 .lock_wait,
335 .lock_obtain,
336 .lock_release,
337 .lock_shared_wait,
338 .lock_shared_obtain,
339 .lock_shared_release,
340 .lock_mark,
341 .lock_name,
342 => {
343 self.counters.lock_events +|= 1;
344 },
345 .zone_text => {},
346 }
347 }
348
349 pub fn captureIntegrity(self: *const Analyzer) CaptureIntegrity {
350 return self.capture.integrity(self.unbalancedEventCount());
351 }
352
353 pub fn zoneDurationEvidence(self: *const Analyzer) []const u8 {
354 if (!std.mem.eql(u8, self.captureIntegrity().status, "complete")) return "partial";
355 if (self.counters.zone_timestamp_regressions != 0) return "partial";
356 return "complete";
357 }
358
359 pub fn gpuDurationEvidence(self: *const Analyzer) []const u8 {
360 if (!std.mem.eql(u8, self.captureIntegrity().status, "complete")) return "partial";
361 return if (self.gpu.durationPairsComplete()) "complete" else "partial";
362 }
363
364 fn unbalancedEventCount(self: *const Analyzer) u64 {
365 var count: u64 = @intCast(self.active_zones.count());
366 count +|= self.counters.unmatched_zone_ends;
367 count +|= self.fibers.counters.unmatched_leaves;
368 count +|= self.fibers.activeCount();
369 count +|= self.gpu.counters.unmatched_ends;
370 count +|= self.gpu.counters.unmatched_times;
371 count +|= self.gpu.incompleteZoneCount();
372 count +|= self.memory.counters.unmatched_frees;
373 count +|= self.locks.counters.unmatched_releases;
374 count +|= @intCast(self.locks.pending_waits.count());
375 count +|= @intCast(self.locks.pending_holds.count());
376 return count;
377 }
378
379 pub fn writeSummary(self: *Analyzer, writer: *std.Io.Writer, options: Options) !void {
380 var zones = try self.collectZones();
381 defer zones.deinit(self.allocator);
382 var zone_prefixes = try self.collectZonePrefixes();
383 defer zone_prefixes.deinit(self.allocator);
384 var zone_threads = try self.collectZoneThreads();
385 defer zone_threads.deinit(self.allocator);
386 var plots = try self.collectPlots();
387 defer plots.deinit(self.allocator);
388 var memory = try self.collectMemory();
389 defer memory.deinit(self.allocator);
390 var samples = try self.collectSamples();
391 defer sample_mod.deinitSummaries(self.allocator, &samples);
392 var lock_rows = try self.collectLocks();
393 defer lock_rows.deinit(self.allocator);
394 var fiber_rows = try self.collectFibers();
395 defer fiber_mod.deinitSummaries(self.allocator, &fiber_rows);
396 var gpu_rows = try self.collectGpu();
397 defer gpu_mod.deinitSummaries(self.allocator, &gpu_rows);
398 var system_rows = try self.collectSystem();
399 defer system_mod.deinitSummaries(self.allocator, &system_rows);
400 var context_rows = try self.collectContext();
401 defer context_mod.deinitSummaries(self.allocator, &context_rows);
402
403 try writer.print(
404 "tracy events={d} zones={d} zone_duration_samples={d} " ++
405 "zone_timestamp_regressions={d} zone_duration_evidence={s} " ++
406 "zone_prefixes={d} zone_threads={d} active_zones={d} " ++
407 "messages={d} frames={d} plots={d} " ++
408 "plot_configurations={d} plot_configuration_conflicts={d} " ++
409 "samples={d} sample_frames={d} fiber_enters={d} fiber_leaves={d} " ++
410 "fiber_names={d} context_switches={d} thread_wakeups={d} " ++
411 "context_ready_latency_samples={d} context_redundant_wakeups={d} " ++
412 "context_ready_time_regressions={d} context_pending_ready_intervals={d} " ++
413 "context_ready_latency_evidence={s} duration_ns={d}\n",
414 .{
415 self.capture.counters.events,
416 self.counters.completed_zones,
417 self.counters.zone_duration_samples,
418 self.counters.zone_timestamp_regressions,
419 self.zoneDurationEvidence(),
420 zone_prefixes.items.len,
421 zone_threads.items.len,
422 self.active_zones.count(),
423 self.counters.messages,
424 self.counters.frames,
425 self.counters.plots,
426 self.counters.plot_configurations,
427 self.counters.plot_configuration_conflicts,
428 self.counters.samples,
429 self.counters.sample_frames,
430 self.counters.fiber_enters,
431 self.counters.fiber_leaves,
432 self.counters.fiber_names,
433 self.counters.context_switches,
434 self.counters.thread_wakeups,
435 self.context.counters.ready_latency_samples,
436 self.context.counters.redundant_wakeups,
437 self.context.counters.ready_time_regressions,
438 self.context.pendingReadyIntervals(),
439 self.context.readyLatencyEvidence(),
440 self.durationNs(),
441 },
442 );
443 try capture_mod.writeText(writer, self.captureIntegrity());
444 const memory_counters = self.memory.counters;
445 try writer.print(
446 "tracy allocations={d} frees={d} completed_lifetimes={d} " ++
447 "lifetime_samples={d} right_censored_allocations={d} live_bytes={d} " ++
448 "high_water_live_bytes={d} allocated_bytes={d} freed_bytes={d} " ++
449 "unmatched_frees={d} duplicate_allocations={d} " ++
450 "timestamp_regressions={d} untracked_allocations={d} " ++
451 "size_mismatches={d} lifetime_population=completed " ++
452 "lifetime_evidence={s}\n",
453 .{
454 memory_counters.allocations,
455 memory_counters.frees,
456 memory_counters.completed_lifetimes,
457 memory_counters.lifetime_samples,
458 self.memory.active.count(),
459 memory_counters.live_bytes,
460 memory_counters.high_water_live_bytes,
461 memory_counters.allocated_bytes,
462 memory_counters.freed_bytes,
463 memory_counters.unmatched_frees,
464 memory_counters.duplicate_allocations,
465 memory_counters.timestamp_regressions,
466 memory_counters.untracked_allocations,
467 memory_counters.size_mismatches,
468 self.memory.lifetimeEvidence(),
469 },
470 );
471 if (self.counters.unmatched_zone_ends != 0) {
472 try writer.print("tracy anomalies unmatched_zone_ends={d}\n", .{self.counters.unmatched_zone_ends});
473 }
474 if (self.counters.fiber_unmatched_leaves != 0) {
475 try writer.print("tracy fiber_anomalies unmatched_leaves={d}\n", .{self.counters.fiber_unmatched_leaves});
476 }
477 try writer.print(
478 "tracy locks={d} lock_waits={d} lock_holds={d} lock_wait_samples={d} " ++
479 "lock_hold_samples={d} pending_waits={d} pending_holds={d} " ++
480 "duplicate_waits={d} duplicate_obtains={d} timestamp_regressions={d} " ++
481 "unmatched_releases={d} lock_duration_evidence={s}\n",
482 .{
483 self.locks.locks.count(),
484 self.locks.counters.completed_waits,
485 self.locks.counters.completed_holds,
486 self.locks.counters.wait_samples,
487 self.locks.counters.hold_samples,
488 self.locks.pending_waits.count(),
489 self.locks.pending_holds.count(),
490 self.locks.counters.duplicate_waits,
491 self.locks.counters.duplicate_obtains,
492 self.locks.counters.timestamp_regressions,
493 self.locks.counters.unmatched_releases,
494 self.locks.durationEvidence(),
495 },
496 );
497 const lock_limit = @min(options.top, lock_rows.items.len);
498 for (lock_rows.items[0..lock_limit]) |row| {
499 try writer.print("lock id={d} name=", .{row.id});
500 try pretty_json.writeString(writer, row.name);
501 try lock_mod.writeSummaryDurationsText(writer, row);
502 try writer.print(" threads={d} marks={d} last_event_ns={d}\n", .{
503 row.thread_count,
504 row.mark_count,
505 row.last_event_ns,
506 });
507 }
508 try writeGpuCaptureText(writer, self);
509 try writer.print(
510 "tracy system_thread_contexts={d} system_thread_pid_maps={d} system_cpu_topology={d} system_sys_time={d} system_sys_power={d} system_computed_power={d} system_hw_samples={d}\n",
511 .{
512 self.counters.system_thread_contexts,
513 self.counters.system_thread_pid_maps,
514 self.counters.system_cpu_topology,
515 self.counters.system_sys_time,
516 self.counters.system_sys_power,
517 self.counters.system_computed_power,
518 self.counters.system_hw_samples,
519 },
520 );
521 const zone_limit = @min(options.top, zones.items.len);
522 for (zones.items[0..zone_limit]) |zone| {
523 try writer.print(
524 "zone name=",
525 .{},
526 );
527 try pretty_json.writeString(writer, zone.name);
528 try writer.print(
529 " count={d} duration_samples={d} total_ns={d} mean_ns={d} " ++
530 "min_ns={d} p50_ns={d} p90_ns={d} p99_ns={d} max_ns={d}",
531 .{
532 zone.count,
533 zone.duration_samples,
534 zone.total_ns,
535 zone.meanNs(),
536 if (zone.min_ns == std.math.maxInt(u64)) 0 else zone.min_ns,
537 zone.percentileNs(50),
538 zone.percentileNs(90),
539 zone.percentileNs(99),
540 zone.max_ns,
541 },
542 );
543 if (zone.file) |file| try writer.print(" file={s}:{d}", .{ file, zone.line });
544 try writer.writeByte('\n');
545 }
546 const prefix_limit = @min(options.top, zone_prefixes.items.len);
547 for (zone_prefixes.items[0..prefix_limit]) |prefix| {
548 try writer.print("zone_prefix name=", .{});
549 try pretty_json.writeString(writer, prefix.name);
550 try writer.print(
551 " count={d} duration_samples={d} total_ns={d} " ++
552 "mean_ns={d} min_ns={d} max_ns={d}\n",
553 .{
554 prefix.count,
555 prefix.duration_samples,
556 prefix.total_ns,
557 prefix.meanNs(),
558 if (prefix.min_ns == std.math.maxInt(u64)) 0 else prefix.min_ns,
559 prefix.max_ns,
560 },
561 );
562 }
563 const thread_limit = @min(options.top, zone_threads.items.len);
564 for (zone_threads.items[0..thread_limit]) |thread| {
565 try writer.print("zone_thread thread={d}", .{thread.thread});
566 if (thread.name) |name| {
567 try writer.writeAll(" name=");
568 try pretty_json.writeString(writer, name);
569 }
570 try writer.print(
571 " count={d} duration_samples={d} total_ns={d} " ++
572 "mean_ns={d} min_ns={d} max_ns={d}\n",
573 .{
574 thread.count,
575 thread.duration_samples,
576 thread.total_ns,
577 thread.meanNs(),
578 if (thread.min_ns == std.math.maxInt(u64)) 0 else thread.min_ns,
579 thread.max_ns,
580 },
581 );
582 }
583 const plot_limit = @min(options.top, plots.items.len);
584 for (plots.items[0..plot_limit]) |plot| try writePlotSummaryText(writer, plot);
585 const memory_limit = @min(options.top, memory.items.len);
586 for (memory.items[0..memory_limit]) |item| try writeMemorySummaryText(writer, item);
587 const sample_limit = @min(options.top, samples.items.len);
588 for (samples.items[0..sample_limit]) |sample| {
589 try writer.print("sample label=", .{});
590 try pretty_json.writeString(writer, sample.label);
591 try writer.print(" count={d} weight={d} first_ns={d} last_ns={d}", .{ sample.count, sample.weight, sample.first_ns, sample.last_ns });
592 if (sample.kind) |kind| {
593 try writer.writeAll(" kind=");
594 try pretty_json.writeString(writer, kind);
595 }
596 if (sample.file) |file| try writer.print(" file={s}:{d}", .{ file, sample.line });
597 try writer.writeByte('\n');
598 }
599 const fiber_limit = @min(options.top, fiber_rows.items.len);
600 for (fiber_rows.items[0..fiber_limit]) |row| {
601 try writer.print("fiber label=", .{});
602 try pretty_json.writeString(writer, row.label);
603 try writer.print(" count={d} running_ns={d} enters={d} leaves={d}", .{ row.count, row.running_ns, row.enters, row.leaves });
604 if (row.fiber) |fiber| try writer.print(" fiber={d}", .{fiber});
605 if (row.thread) |thread| try writer.print(" thread={d}", .{thread});
606 if (row.group_hint) |group_hint| try writer.print(" group_hint={d}", .{group_hint});
607 if (row.migrations != 0) try writer.print(" migrations={d}", .{row.migrations});
608 if (row.unmatched_leaves != 0) try writer.print(" unmatched_leaves={d}", .{row.unmatched_leaves});
609 if (row.active) try writer.writeAll(" active=true");
610 try writer.writeByte('\n');
611 }
612 const gpu_limit = @min(options.top, gpu_rows.items.len);
613 for (gpu_rows.items[0..gpu_limit]) |row| {
614 try writeGpuSummaryText(writer, row);
615 }
616 const system_limit = @min(options.top, system_rows.items.len);
617 for (system_rows.items[0..system_limit]) |row| {
618 try writer.print("system label=", .{});
619 try pretty_json.writeString(writer, row.label);
620 try writer.print(" count={d}", .{row.count});
621 try writeSystemSummaryTextFields(writer, row);
622 try writer.writeByte('\n');
623 }
624 const context_limit = @min(options.top, context_rows.items.len);
625 for (context_rows.items[0..context_limit]) |row| {
626 try writer.print("context label=", .{});
627 try pretty_json.writeString(writer, row.label);
628 try writer.print(
629 " count={d} running_ns={d} blocked_ns={d} ready_ns={d} " ++
630 "unknown_off_cpu_ns={d}",
631 .{
632 row.count,
633 row.running_ns,
634 row.blocked_ns,
635 row.ready_ns,
636 row.unknown_off_cpu_ns,
637 },
638 );
639 try writeContextLatencyText(writer, row);
640 if (row.thread) |thread| try writer.print(" thread={d}", .{thread});
641 if (row.cpu) |cpu| try writer.print(" cpu={d}", .{cpu});
642 if (row.migrations != 0) try writer.print(" migrations={d}", .{row.migrations});
643 try writer.writeByte('\n');
644 }
645 }
646
647 pub fn writeSummaryJsonl(self: *Analyzer, writer: *std.Io.Writer, options: Options) !void {
648 var zones = try self.collectZones();
649 defer zones.deinit(self.allocator);
650 var zone_prefixes = try self.collectZonePrefixes();
651 defer zone_prefixes.deinit(self.allocator);
652 var zone_threads = try self.collectZoneThreads();
653 defer zone_threads.deinit(self.allocator);
654 var plots = try self.collectPlots();
655 defer plots.deinit(self.allocator);
656 var memory = try self.collectMemory();
657 defer memory.deinit(self.allocator);
658 var samples = try self.collectSamples();
659 defer sample_mod.deinitSummaries(self.allocator, &samples);
660 var lock_rows = try self.collectLocks();
661 defer lock_rows.deinit(self.allocator);
662 var fiber_rows = try self.collectFibers();
663 defer fiber_mod.deinitSummaries(self.allocator, &fiber_rows);
664 var gpu_rows = try self.collectGpu();
665 defer gpu_mod.deinitSummaries(self.allocator, &gpu_rows);
666 var system_rows = try self.collectSystem();
667 defer system_mod.deinitSummaries(self.allocator, &system_rows);
668 var context_rows = try self.collectContext();
669 defer context_mod.deinitSummaries(self.allocator, &context_rows);
670
671 var summary_stream = pretty_json.Writer.init(writer, .minified);
672 const summary = try summary_stream.object();
673 try summary.field("kind", "summary");
674 try writeCaptureFields(
675 summary,
676 self,
677 zone_prefixes.items.len,
678 zone_threads.items.len,
679 );
680 try writeGpuCaptureFields(summary, self);
681 try writeSystemCaptureFields(summary, self);
682 try writeContextCaptureFields(summary, self);
683 try summary.field("duration_ns", self.durationNs());
684 try writeMemoryCaptureFields(summary, self);
685 try writeLockCaptureFields(summary, self);
686 try summary.field("memory_lifetime_population", "completed");
687 try summary.field("memory_lifetime_evidence", self.memory.lifetimeEvidence());
688 try capture_mod.writeFields(summary, self.captureIntegrity());
689 try summary.endLine();
690 const lock_limit = @min(options.top, lock_rows.items.len);
691 for (lock_rows.items[0..lock_limit]) |row| {
692 var stream = pretty_json.Writer.init(writer, .minified);
693 const object = try stream.object();
694 try object.field("kind", "lock");
695 try object.field("id", row.id);
696 try object.field("name", row.name);
697 try lock_mod.writeSummaryDurationFields(object, row);
698 try object.field("threads", row.thread_count);
699 try object.field("marks", row.mark_count);
700 try object.field("last_event_ns", row.last_event_ns);
701 try object.endLine();
702 }
703 const zone_limit = @min(options.top, zones.items.len);
704 for (zones.items[0..zone_limit]) |zone| {
705 var stream = pretty_json.Writer.init(writer, .minified);
706 const object = try stream.object();
707 try object.field("kind", "zone");
708 try object.field("name", zone.name);
709 try object.field("count", zone.count);
710 try object.field("duration_samples", zone.duration_samples);
711 try object.field("total_ns", zone.total_ns);
712 try object.field("mean_ns", zone.meanNs());
713 try object.field(
714 "min_ns",
715 if (zone.min_ns == std.math.maxInt(u64)) 0 else zone.min_ns,
716 );
717 try object.field("p50_ns", zone.percentileNs(50));
718 try object.field("p90_ns", zone.percentileNs(90));
719 try object.field("p99_ns", zone.percentileNs(99));
720 try object.field("max_ns", zone.max_ns);
721 if (zone.file) |file| {
722 try object.field("file", file);
723 try object.field("line", zone.line);
724 }
725 try object.endLine();
726 }
727 const prefix_limit = @min(options.top, zone_prefixes.items.len);
728 for (zone_prefixes.items[0..prefix_limit]) |prefix| {
729 var stream = pretty_json.Writer.init(writer, .minified);
730 const object = try stream.object();
731 try object.field("kind", "zone_prefix");
732 try object.field("name", prefix.name);
733 try object.field("count", prefix.count);
734 try object.field("duration_samples", prefix.duration_samples);
735 try object.field("total_ns", prefix.total_ns);
736 try object.field("mean_ns", prefix.meanNs());
737 try object.field(
738 "min_ns",
739 if (prefix.min_ns == std.math.maxInt(u64)) 0 else prefix.min_ns,
740 );
741 try object.field("max_ns", prefix.max_ns);
742 try object.endLine();
743 }
744 const thread_limit = @min(options.top, zone_threads.items.len);
745 for (zone_threads.items[0..thread_limit]) |thread| {
746 var stream = pretty_json.Writer.init(writer, .minified);
747 const object = try stream.object();
748 try object.field("kind", "zone_thread");
749 try object.field("thread", thread.thread);
750 if (thread.name) |name| try object.field("name", name);
751 try object.field("count", thread.count);
752 try object.field("duration_samples", thread.duration_samples);
753 try object.field("total_ns", thread.total_ns);
754 try object.field("mean_ns", thread.meanNs());
755 try object.field(
756 "min_ns",
757 if (thread.min_ns == std.math.maxInt(u64)) 0 else thread.min_ns,
758 );
759 try object.field("max_ns", thread.max_ns);
760 try object.endLine();
761 }
762 const plot_limit = @min(options.top, plots.items.len);
763 for (plots.items[0..plot_limit]) |plot| try writePlotSummaryJson(writer, plot);
764 const memory_limit = @min(options.top, memory.items.len);
765 for (memory.items[0..memory_limit]) |item| try writeMemorySummaryJson(writer, item);
766 const sample_limit = @min(options.top, samples.items.len);
767 for (samples.items[0..sample_limit]) |sample| {
768 var stream = pretty_json.Writer.init(writer, .minified);
769 const object = try stream.object();
770 try object.field("kind", "sample");
771 try object.field("label", sample.label);
772 try object.field("count", sample.count);
773 try object.field("weight", sample.weight);
774 try object.field("first_ns", sample.first_ns);
775 try object.field("last_ns", sample.last_ns);
776 if (sample.kind) |kind| try object.field("sample_kind", kind);
777 if (sample.file) |file| {
778 try object.field("file", file);
779 try object.field("line", sample.line);
780 }
781 try object.endLine();
782 }
783 const fiber_limit = @min(options.top, fiber_rows.items.len);
784 for (fiber_rows.items[0..fiber_limit]) |row| {
785 var stream = pretty_json.Writer.init(writer, .minified);
786 const object = try stream.object();
787 try object.field("kind", "fiber");
788 try object.field("label", row.label);
789 try object.field("count", row.count);
790 try object.field("running_ns", row.running_ns);
791 try object.field("enters", row.enters);
792 try object.field("leaves", row.leaves);
793 if (row.fiber) |fiber| try object.field("fiber", fiber);
794 if (row.thread) |thread| try object.field("thread", thread);
795 if (row.group_hint) |group_hint| try object.field("group_hint", group_hint);
796 if (row.migrations != 0) try object.field("migrations", row.migrations);
797 if (row.unmatched_leaves != 0) {
798 try object.field("unmatched_leaves", row.unmatched_leaves);
799 }
800 if (row.active) try object.field("active", true);
801 try object.endLine();
802 }
803 const gpu_limit = @min(options.top, gpu_rows.items.len);
804 for (gpu_rows.items[0..gpu_limit]) |row| {
805 try writeGpuSummaryJson(writer, row);
806 }
807 const system_limit = @min(options.top, system_rows.items.len);
808 for (system_rows.items[0..system_limit]) |row| {
809 var stream = pretty_json.Writer.init(writer, .minified);
810 const object = try stream.object();
811 try object.field("kind", "system");
812 try object.field("label", row.label);
813 try object.field("group", systemGroupTag(row.group));
814 try object.field("count", row.count);
815 try writeSystemSummaryFields(object, row);
816 try object.endLine();
817 }
818 const context_limit = @min(options.top, context_rows.items.len);
819 for (context_rows.items[0..context_limit]) |row| {
820 var stream = pretty_json.Writer.init(writer, .minified);
821 const object = try stream.object();
822 try object.field("kind", "context");
823 try object.field("label", row.label);
824 try object.field("count", row.count);
825 try object.field("running_ns", row.running_ns);
826 try object.field("blocked_ns", row.blocked_ns);
827 try object.field("ready_ns", row.ready_ns);
828 try object.field("unknown_off_cpu_ns", row.unknown_off_cpu_ns);
829 try writeContextLatencyFields(object, row);
830 if (row.thread) |thread| try object.field("thread", thread);
831 if (row.cpu) |cpu| try object.field("cpu", cpu);
832 if (row.migrations != 0) try object.field("migrations", row.migrations);
833 try object.endLine();
834 }
835 }
836
837 fn recordZoneBegin(self: *Analyzer, parsed: event.Parsed) !void {
838 if (parsed.id == 0) return;
839 if (self.active_zones.fetchRemove(parsed.id)) |removed| {
840 var old = removed.value;
841 old.deinit(self.allocator);
842 }
843 const name = parsed.name orelse "<zone>";
844 try self.active_zones.put(self.allocator, parsed.id, .{
845 .name = try self.allocator.dupe(u8, name),
846 .file = try dupeOptional(self.allocator, parsed.file),
847 .function = try dupeOptional(self.allocator, parsed.function),
848 .line = parsed.line,
849 .thread = parsed.thread,
850 .start_ns = parsed.time_ns,
851 .color = parsed.color,
852 });
853 self.counters.started_zones += 1;
854 }
855
856 fn recordZoneEnd(self: *Analyzer, parsed: event.Parsed) !void {
857 const removed = self.active_zones.fetchRemove(parsed.id) orelse {
858 self.counters.unmatched_zone_ends += 1;
859 return;
860 };
861 var zone = removed.value;
862 defer zone.deinit(self.allocator);
863 const duration = if (parsed.time_ns >= zone.start_ns)
864 parsed.time_ns - zone.start_ns
865 else
866 null;
867 const summary = try self.zoneSummary(zone.name, zone.file, zone.function, zone.line);
868 summary.count += 1;
869 if (duration) |valid| {
870 try summary.durations_ns.append(self.allocator, valid);
871 summary.duration_samples += 1;
872 summary.total_ns +|= valid;
873 summary.min_ns = @min(summary.min_ns, valid);
874 summary.max_ns = @max(summary.max_ns, valid);
875 self.counters.zone_duration_samples += 1;
876 } else {
877 self.counters.zone_timestamp_regressions += 1;
878 }
879 try self.recordZonePrefixes(zone.name, duration);
880 try self.recordZoneThread(zone.thread, duration);
881 self.counters.completed_zones += 1;
882 }
883
884 fn recordZoneName(self: *Analyzer, parsed: event.Parsed) !void {
885 const name = parsed.name orelse return;
886 const zone = self.active_zones.getPtr(parsed.id) orelse return;
887 self.allocator.free(zone.name);
888 zone.name = try self.allocator.dupe(u8, name);
889 }
890
891 fn recordZoneColor(self: *Analyzer, parsed: event.Parsed) void {
892 const color = parsed.color orelse return;
893 const zone = self.active_zones.getPtr(parsed.id) orelse return;
894 zone.color = color;
895 }
896
897 fn recordZoneValue(self: *Analyzer, parsed: event.Parsed) void {
898 const value = parsed.value_u64 orelse return;
899 const zone = self.active_zones.getPtr(parsed.id) orelse return;
900 zone.value = value;
901 }
902
903 fn recordThreadName(self: *Analyzer, parsed: event.Parsed) !void {
904 const name = parsed.name orelse return;
905 const summary = try self.zoneThreadSummary(parsed.thread);
906 if (summary.name) |old| self.allocator.free(old);
907 summary.name = try self.allocator.dupe(u8, name);
908 }
909
910 fn recordZonePrefixes(self: *Analyzer, name: []const u8, duration: ?u64) !void {
911 var start: usize = 0;
912 while (std.mem.indexOfScalarPos(u8, name, start, '.')) |dot| {
913 if (dot != 0) {
914 const summary = try self.zonePrefixSummary(name[0..dot]);
915 summary.count += 1;
916 if (duration) |valid| {
917 summary.duration_samples += 1;
918 summary.total_ns +|= valid;
919 summary.min_ns = @min(summary.min_ns, valid);
920 summary.max_ns = @max(summary.max_ns, valid);
921 }
922 }
923 start = dot + 1;
924 }
925 }
926
927 fn recordZoneThread(self: *Analyzer, thread: u64, duration: ?u64) !void {
928 const summary = try self.zoneThreadSummary(thread);
929 summary.count += 1;
930 if (duration) |valid| {
931 summary.duration_samples += 1;
932 summary.total_ns +|= valid;
933 summary.min_ns = @min(summary.min_ns, valid);
934 summary.max_ns = @max(summary.max_ns, valid);
935 }
936 }
937
938 fn zoneSummary(
939 self: *Analyzer,
940 name: []const u8,
941 file: ?[]const u8,
942 function: ?[]const u8,
943 line: u32,
944 ) !*ZoneSummary {
945 const entry = try self.zone_summaries.getOrPut(self.allocator, name);
946 if (!entry.found_existing) {
947 const owned_name = try self.allocator.dupe(u8, name);
948 entry.key_ptr.* = owned_name;
949 entry.value_ptr.* = .{
950 .name = owned_name,
951 .file = try dupeOptional(self.allocator, file),
952 .function = try dupeOptional(self.allocator, function),
953 .line = line,
954 };
955 }
956 return entry.value_ptr;
957 }
958
959 fn zonePrefixSummary(self: *Analyzer, name: []const u8) !*ZonePrefixSummary {
960 const entry = try self.zone_prefix_summaries.getOrPut(self.allocator, name);
961 if (!entry.found_existing) {
962 const owned_name = try self.allocator.dupe(u8, name);
963 entry.key_ptr.* = owned_name;
964 entry.value_ptr.* = .{ .name = owned_name };
965 }
966 return entry.value_ptr;
967 }
968
969 fn zoneThreadSummary(self: *Analyzer, thread: u64) !*ZoneThreadSummary {
970 const entry = try self.zone_thread_summaries.getOrPut(self.allocator, thread);
971 if (!entry.found_existing) entry.value_ptr.* = .{ .thread = thread };
972 return entry.value_ptr;
973 }
974
975 pub fn collectZones(self: *Analyzer) !std.ArrayListUnmanaged(ZoneSummary) {
976 var zones: std.ArrayListUnmanaged(ZoneSummary) = .empty;
977 var iter = self.zone_summaries.valueIterator();
978 while (iter.next()) |zone| {
979 std.mem.sort(u64, zone.durations_ns.items, {}, durationLessThan);
980 try zones.append(self.allocator, zone.*);
981 }
982 std.mem.sort(ZoneSummary, zones.items, {}, zoneGreaterThan);
983 return zones;
984 }
985
986 pub fn collectZonePrefixes(self: *Analyzer) !std.ArrayListUnmanaged(ZonePrefixSummary) {
987 var prefixes: std.ArrayListUnmanaged(ZonePrefixSummary) = .empty;
988 var iter = self.zone_prefix_summaries.valueIterator();
989 while (iter.next()) |prefix| try prefixes.append(self.allocator, prefix.*);
990 std.mem.sort(ZonePrefixSummary, prefixes.items, {}, zonePrefixGreaterThan);
991 return prefixes;
992 }
993
994 pub fn collectZoneThreads(self: *Analyzer) !std.ArrayListUnmanaged(ZoneThreadSummary) {
995 var threads: std.ArrayListUnmanaged(ZoneThreadSummary) = .empty;
996 var iter = self.zone_thread_summaries.valueIterator();
997 while (iter.next()) |thread| try threads.append(self.allocator, thread.*);
998 std.mem.sort(ZoneThreadSummary, threads.items, {}, zoneThreadGreaterThan);
999 return threads;
1000 }
1001
1002 pub fn collectPlots(self: *Analyzer) !std.ArrayListUnmanaged(PlotSummary) {
1003 return try self.plots.collectSummaries(self.allocator, .{ .sort = .samples });
1004 }
1005
1006 pub fn collectMemory(self: *Analyzer) !std.ArrayListUnmanaged(MemorySummary) {
1007 return try self.memory.collectSummaries(self.allocator, .{ .sort = .high_water });
1008 }
1009
1010 pub fn collectSamples(self: *Analyzer) !std.ArrayListUnmanaged(SampleSummary) {
1011 return try self.samples.collectSummaries(self.allocator, .{ .sort = .samples });
1012 }
1013
1014 pub fn collectLocks(self: *Analyzer) !std.ArrayListUnmanaged(LockSummary) {
1015 return try self.locks.collectRows(self.allocator, .{ .sort = .wait });
1016 }
1017
1018 pub fn collectFibers(self: *Analyzer) !std.ArrayListUnmanaged(FiberSummary) {
1019 return try self.fibers.collectSummaries(self.allocator, .{ .sort = .running });
1020 }
1021
1022 pub fn collectGpu(self: *Analyzer) !std.ArrayListUnmanaged(GpuSummary) {
1023 return try self.gpu.collectSummaries(self.allocator, .{ .sort = .gpu });
1024 }
1025
1026 pub fn collectSystem(self: *Analyzer) !std.ArrayListUnmanaged(SystemSummary) {
1027 return try self.system.collectSummaries(self.allocator, .{ .sort = .count });
1028 }
1029
1030 pub fn collectContext(self: *Analyzer) !std.ArrayListUnmanaged(ContextSummary) {
1031 return try self.context.collectSummaries(self.allocator, .{ .sort = .running });
1032 }
1033
1034 pub fn durationNs(self: Analyzer) u64 {
1035 const start_ns = self.start_ns orelse return 0;
1036 const end_ns = self.end_ns orelse return 0;
1037 if (end_ns <= start_ns) return 0;
1038 return end_ns - start_ns;
1039 }
1040 };
1041
1042 pub fn writeSummaryFromJsonlPath(
1043 allocator: std.mem.Allocator,
1044 path: []const u8,
1045 writer: *std.Io.Writer,
1046 options: Options,
1047 ) !void {
1048 var analyzer = Analyzer.init(allocator);
1049 defer analyzer.deinit();
1050 try ingestPath(&analyzer, path);
1051 try analyzer.writeSummary(writer, options);
1052 }
1053
1054 pub fn writeSummaryJsonlFromJsonlPath(
1055 allocator: std.mem.Allocator,
1056 path: []const u8,
1057 writer: *std.Io.Writer,
1058 options: Options,
1059 ) !void {
1060 var analyzer = Analyzer.init(allocator);
1061 defer analyzer.deinit();
1062 try ingestPath(&analyzer, path);
1063 try analyzer.writeSummaryJsonl(writer, options);
1064 }
1065
1066 pub fn ingestPath(analyzer: *Analyzer, path: []const u8) !void {
1067 var file = try sys.fs.cwd().openFile(sys.fs.debugIo(), path, .{});
1068 defer file.close(sys.fs.debugIo());
1069
1070 var buffer: [64 * 1024]u8 = undefined;
1071 var reader = file.reader(sys.fs.debugIo(), &buffer);
1072 while (true) {
1073 const line = reader.interface.takeDelimiter('\n') catch |err| switch (err) {
1074 error.ReadFailed => return reader.err.?,
1075 else => return err,
1076 };
1077 const actual = line orelse break;
1078 try analyzer.ingestJsonLine(actual);
1079 }
1080 }
1081
1082 fn dupeOptional(allocator: std.mem.Allocator, text: ?[]const u8) !?[]u8 {
1083 const actual = text orelse return null;
1084 return try allocator.dupe(u8, actual);
1085 }
1086
1087 fn writeCaptureFields(
1088 object: pretty_json.Object,
1089 analyzer: *const Analyzer,
1090 zone_prefixes: usize,
1091 zone_threads: usize,
1092 ) !void {
1093 try object.field("events", analyzer.capture.counters.events);
1094 try object.field("zones", analyzer.counters.completed_zones);
1095 try object.field("zone_duration_samples", analyzer.counters.zone_duration_samples);
1096 try object.field(
1097 "zone_timestamp_regressions",
1098 analyzer.counters.zone_timestamp_regressions,
1099 );
1100 try object.field("zone_duration_evidence", analyzer.zoneDurationEvidence());
1101 try object.field("zone_prefixes", zone_prefixes);
1102 try object.field("zone_threads", zone_threads);
1103 try object.field("active_zones", analyzer.active_zones.count());
1104 try object.field("messages", analyzer.counters.messages);
1105 try object.field("frames", analyzer.counters.frames);
1106 try object.field("plots", analyzer.counters.plots);
1107 try object.field("plot_configurations", analyzer.counters.plot_configurations);
1108 try object.field(
1109 "plot_configuration_conflicts",
1110 analyzer.counters.plot_configuration_conflicts,
1111 );
1112 try object.field("samples", analyzer.counters.samples);
1113 try object.field("sample_frames", analyzer.counters.sample_frames);
1114 try object.field("fiber_enters", analyzer.fibers.counters.enters);
1115 try object.field("fiber_leaves", analyzer.fibers.counters.leaves);
1116 try object.field("fiber_names", analyzer.fibers.counters.names);
1117 try object.field("fiber_running_ns", analyzer.fibers.runningNs());
1118 try object.field("fiber_unmatched_leaves", analyzer.fibers.counters.unmatched_leaves);
1119 }
1120
1121 fn writeGpuCaptureFields(object: pretty_json.Object, analyzer: *const Analyzer) !void {
1122 const counters = analyzer.gpu.counters;
1123 try object.field("gpu_contexts", analyzer.gpu.contexts.count());
1124 try object.field("gpu_zones", counters.zone_begins);
1125 try object.field("gpu_completed_zones", counters.completed_zones);
1126 try object.field("gpu_duration_samples", counters.gpu_duration_samples);
1127 try object.field("gpu_cpu_duration_samples", counters.cpu_duration_samples);
1128 try object.field("gpu_times", counters.gpu_times);
1129 try object.field("gpu_annotations", counters.annotations);
1130 try object.field("gpu_ns", analyzer.gpu.gpuNs());
1131 try object.field("gpu_cpu_ns", analyzer.gpu.cpuNs());
1132 try object.field("gpu_unmatched_ends", counters.unmatched_ends);
1133 try object.field("gpu_unmatched_times", counters.unmatched_times);
1134 try object.field("gpu_duplicate_queries", counters.duplicate_queries);
1135 try object.field("gpu_duplicate_times", counters.duplicate_times);
1136 try object.field("gpu_timestamp_regressions", counters.gpu_timestamp_regressions);
1137 try object.field(
1138 "gpu_cpu_timestamp_regressions",
1139 counters.cpu_timestamp_regressions,
1140 );
1141 try object.field("gpu_duration_population", "completed_valid_pairs");
1142 try object.field("gpu_duration_evidence", analyzer.gpuDurationEvidence());
1143 }
1144
1145 fn writeSystemCaptureFields(object: pretty_json.Object, analyzer: *const Analyzer) !void {
1146 try object.field("system_thread_contexts", analyzer.counters.system_thread_contexts);
1147 try object.field("system_thread_pid_maps", analyzer.counters.system_thread_pid_maps);
1148 try object.field("system_cpu_topology", analyzer.counters.system_cpu_topology);
1149 try object.field("system_sys_time", analyzer.counters.system_sys_time);
1150 try object.field("system_sys_power", analyzer.counters.system_sys_power);
1151 try object.field("system_computed_power", analyzer.counters.system_computed_power);
1152 try object.field("system_hw_samples", analyzer.counters.system_hw_samples);
1153 }
1154
1155 fn writeContextCaptureFields(object: pretty_json.Object, analyzer: *const Analyzer) !void {
1156 const counters = analyzer.context.counters;
1157 try object.field("context_switches", counters.switches);
1158 try object.field("thread_wakeups", counters.wakeups);
1159 try object.field("context_running_ns", analyzer.context.runningNs());
1160 try object.field("context_blocked_ns", analyzer.context.blockedNs());
1161 try object.field("context_ready_ns", analyzer.context.readyNs());
1162 try object.field("context_unknown_off_cpu_ns", analyzer.context.unknownOffCpuNs());
1163 try object.field("context_ready_latency_samples", counters.ready_latency_samples);
1164 try object.field("context_redundant_wakeups", counters.redundant_wakeups);
1165 try object.field("context_ready_time_regressions", counters.ready_time_regressions);
1166 try object.field(
1167 "context_pending_ready_intervals",
1168 analyzer.context.pendingReadyIntervals(),
1169 );
1170 try object.field("context_ready_latency_evidence", analyzer.context.readyLatencyEvidence());
1171 }
1172
1173 fn writeMemoryCaptureFields(object: pretty_json.Object, analyzer: *const Analyzer) !void {
1174 const counters = analyzer.memory.counters;
1175 try object.field("allocations", counters.allocations);
1176 try object.field("frees", counters.frees);
1177 try object.field("live_bytes", counters.live_bytes);
1178 try object.field("high_water_live_bytes", counters.high_water_live_bytes);
1179 try object.field("allocated_bytes", counters.allocated_bytes);
1180 try object.field("freed_bytes", counters.freed_bytes);
1181 try object.field("completed_lifetimes", counters.completed_lifetimes);
1182 try object.field("lifetime_samples", counters.lifetime_samples);
1183 try object.field("right_censored_allocations", analyzer.memory.active.count());
1184 try object.field("unmatched_frees", counters.unmatched_frees);
1185 try object.field("duplicate_allocations", counters.duplicate_allocations);
1186 try object.field("memory_timestamp_regressions", counters.timestamp_regressions);
1187 try object.field("untracked_allocations", counters.untracked_allocations);
1188 try object.field("size_mismatches", counters.size_mismatches);
1189 try object.field("unmatched_zone_ends", analyzer.counters.unmatched_zone_ends);
1190 }
1191
1192 fn writeLockCaptureFields(object: pretty_json.Object, analyzer: *const Analyzer) !void {
1193 const counters = analyzer.locks.counters;
1194 try object.field("locks", analyzer.locks.locks.count());
1195 try object.field("lock_waits", counters.completed_waits);
1196 try object.field("lock_holds", counters.completed_holds);
1197 try object.field("lock_wait_samples", counters.wait_samples);
1198 try object.field("lock_hold_samples", counters.hold_samples);
1199 try object.field("lock_pending_waits", analyzer.locks.pending_waits.count());
1200 try object.field("lock_pending_holds", analyzer.locks.pending_holds.count());
1201 try object.field("lock_duplicate_waits", counters.duplicate_waits);
1202 try object.field("lock_duplicate_obtains", counters.duplicate_obtains);
1203 try object.field("lock_timestamp_regressions", counters.timestamp_regressions);
1204 try object.field("lock_unmatched_releases", counters.unmatched_releases);
1205 try object.field("lock_duration_evidence", analyzer.locks.durationEvidence());
1206 }
1207
1208 fn writePlotSummaryText(writer: *std.Io.Writer, plot: PlotSummary) !void {
1209 try writer.writeAll("plot name=");
1210 try pretty_json.writeString(writer, plot.name);
1211 try writer.print(
1212 " count={d} min={d} max={d} last={d} mean={d} configured={} " ++
1213 "configurations={d} configuration_conflicts={d}",
1214 .{
1215 plot.count,
1216 plot.min,
1217 plot.max,
1218 plot.last,
1219 plot.mean,
1220 plot.configured,
1221 plot.configurations,
1222 plot.configuration_conflicts,
1223 },
1224 );
1225 if (plot.kind) |kind| {
1226 try writer.writeAll(" kind=");
1227 try pretty_json.writeString(writer, kind);
1228 }
1229 if (plot.unit) |unit| {
1230 try writer.writeAll(" unit=");
1231 try pretty_json.writeString(writer, unit);
1232 }
1233 try writer.print(" step={} fill={}", .{ plot.step, plot.fill });
1234 if (plot.color) |color| try writer.print(" color={d}", .{color});
1235 try writer.writeByte('\n');
1236 }
1237
1238 fn writePlotSummaryJson(writer: *std.Io.Writer, plot: PlotSummary) !void {
1239 var stream = pretty_json.Writer.init(writer, .minified);
1240 const object = try stream.object();
1241 try object.field("kind", "plot");
1242 try object.field("name", plot.name);
1243 if (plot.kind) |kind| try object.field("plot_kind", kind);
1244 try object.field("unit", plot.unit);
1245 try object.field("configured", plot.configured);
1246 try object.field("step", plot.step);
1247 try object.field("fill", plot.fill);
1248 try object.field("color", plot.color);
1249 try object.field("configurations", plot.configurations);
1250 try object.field("configuration_conflicts", plot.configuration_conflicts);
1251 try object.field("count", plot.count);
1252 try object.field("min", plot.min);
1253 try object.field("max", plot.max);
1254 try object.field("last", plot.last);
1255 try object.field("mean", plot.mean);
1256 try object.endLine();
1257 }
1258
1259 fn writeGpuCaptureText(writer: *std.Io.Writer, analyzer: *const Analyzer) !void {
1260 const counters = analyzer.gpu.counters;
1261 try writer.print(
1262 "tracy gpu_contexts={d} gpu_zones={d} gpu_completed_zones={d} " ++
1263 "gpu_duration_samples={d} gpu_cpu_duration_samples={d} gpu_times={d} " ++
1264 "gpu_annotations={d} gpu_ns={d} gpu_cpu_ns={d} gpu_unmatched_ends={d} " ++
1265 "gpu_unmatched_times={d} gpu_duplicate_queries={d} gpu_duplicate_times={d} " ++
1266 "gpu_timestamp_regressions={d} gpu_cpu_timestamp_regressions={d} " ++
1267 "gpu_duration_population=completed_valid_pairs gpu_duration_evidence={s}\n",
1268 .{
1269 analyzer.gpu.contexts.count(), counters.zone_begins,
1270 counters.completed_zones, counters.gpu_duration_samples,
1271 counters.cpu_duration_samples, counters.gpu_times,
1272 counters.annotations, analyzer.gpu.gpuNs(),
1273 analyzer.gpu.cpuNs(), counters.unmatched_ends,
1274 counters.unmatched_times, counters.duplicate_queries,
1275 counters.duplicate_times, counters.gpu_timestamp_regressions,
1276 counters.cpu_timestamp_regressions, analyzer.gpuDurationEvidence(),
1277 },
1278 );
1279 }
1280
1281 fn writeGpuSummaryText(writer: *std.Io.Writer, row: GpuSummary) !void {
1282 try writer.writeAll("gpu label=");
1283 try pretty_json.writeString(writer, row.label);
1284 try writer.print(
1285 " count={d} gpu_ns={d} cpu_ns={d} zones={d} completed_zones={d}",
1286 .{ row.count, row.gpu_ns, row.cpu_ns, row.zones, row.completed_zones },
1287 );
1288 try gpu_mod.writeDurationFieldsText(writer, row);
1289 if (row.context) |context| try writer.print(" context={d}", .{context});
1290 if (row.thread) |thread| try writer.print(" thread={d}", .{thread});
1291 if (row.annotations != 0) try writer.print(" annotations={d}", .{row.annotations});
1292 try writer.writeByte('\n');
1293 }
1294
1295 fn writeGpuSummaryJson(writer: *std.Io.Writer, row: GpuSummary) !void {
1296 var stream = pretty_json.Writer.init(writer, .minified);
1297 const object = try stream.object();
1298 try object.field("kind", "gpu");
1299 try object.field("label", row.label);
1300 try object.field("count", row.count);
1301 try object.field("gpu_ns", row.gpu_ns);
1302 try object.field("cpu_ns", row.cpu_ns);
1303 try object.field("zones", row.zones);
1304 try object.field("completed_zones", row.completed_zones);
1305 try gpu_mod.writeDurationFields(object, row);
1306 if (row.context) |context| try object.field("context", context);
1307 if (row.thread) |thread| try object.field("thread", thread);
1308 if (row.annotations != 0) try object.field("annotations", row.annotations);
1309 try object.endLine();
1310 }
1311
1312 fn writeMemorySummaryText(writer: *std.Io.Writer, item: MemorySummary) !void {
1313 try writer.writeAll("memory name=");
1314 try pretty_json.writeString(writer, item.name);
1315 try writer.print(
1316 " live_bytes={d} high_water_live_bytes={d} allocated_bytes={d} " ++
1317 "freed_bytes={d} allocations={d} frees={d} " ++
1318 "right_censored_allocations={d} completed_lifetimes={d} " ++
1319 "lifetime_samples={d} lifetime_mean_ns={d} lifetime_min_ns={d} " ++
1320 "lifetime_p50_ns={d} lifetime_p90_ns={d} lifetime_p99_ns={d} " ++
1321 "lifetime_max_ns={d}\n",
1322 .{
1323 item.live_bytes, item.high_water_live_bytes,
1324 item.allocated_bytes, item.freed_bytes,
1325 item.allocations, item.frees,
1326 item.active_allocations, item.completed_lifetimes,
1327 item.lifetime_samples, item.meanLifetimeNs(),
1328 item.min_lifetime_ns, item.p50_lifetime_ns,
1329 item.p90_lifetime_ns, item.p99_lifetime_ns,
1330 item.max_lifetime_ns,
1331 },
1332 );
1333 }
1334
1335 fn writeMemorySummaryJson(writer: *std.Io.Writer, item: MemorySummary) !void {
1336 var stream = pretty_json.Writer.init(writer, .minified);
1337 const object = try stream.object();
1338 try object.field("kind", "memory");
1339 try object.field("name", item.name);
1340 try object.field("live_bytes", item.live_bytes);
1341 try object.field("high_water_live_bytes", item.high_water_live_bytes);
1342 try object.field("allocated_bytes", item.allocated_bytes);
1343 try object.field("freed_bytes", item.freed_bytes);
1344 try object.field("allocations", item.allocations);
1345 try object.field("frees", item.frees);
1346 try object.field("right_censored_allocations", item.active_allocations);
1347 try object.field("completed_lifetimes", item.completed_lifetimes);
1348 try object.field("lifetime_samples", item.lifetime_samples);
1349 try object.field("lifetime_mean_ns", item.meanLifetimeNs());
1350 try object.field("lifetime_min_ns", item.min_lifetime_ns);
1351 try object.field("lifetime_p50_ns", item.p50_lifetime_ns);
1352 try object.field("lifetime_p90_ns", item.p90_lifetime_ns);
1353 try object.field("lifetime_p99_ns", item.p99_lifetime_ns);
1354 try object.field("lifetime_max_ns", item.max_lifetime_ns);
1355 try object.endLine();
1356 }
1357
1358 fn writeContextLatencyText(writer: *std.Io.Writer, row: context_mod.Summary) !void {
1359 if (row.ready_latency_samples == 0) return;
1360 try writer.print(
1361 " ready_latency_samples={d} ready_latency_mean_ns={d} " ++
1362 "ready_latency_min_ns={d} ready_latency_p50_ns={d} " ++
1363 "ready_latency_p90_ns={d} ready_latency_p99_ns={d} " ++
1364 "ready_latency_max_ns={d}",
1365 .{
1366 row.ready_latency_samples,
1367 row.ready_latency_mean_ns,
1368 row.ready_latency_min_ns,
1369 row.ready_latency_p50_ns,
1370 row.ready_latency_p90_ns,
1371 row.ready_latency_p99_ns,
1372 row.ready_latency_max_ns,
1373 },
1374 );
1375 }
1376
1377 fn writeContextLatencyFields(object: pretty_json.Object, row: context_mod.Summary) !void {
1378 if (row.ready_latency_samples == 0) return;
1379 try object.field("ready_latency_samples", row.ready_latency_samples);
1380 try object.field("ready_latency_mean_ns", row.ready_latency_mean_ns);
1381 try object.field("ready_latency_min_ns", row.ready_latency_min_ns);
1382 try object.field("ready_latency_p50_ns", row.ready_latency_p50_ns);
1383 try object.field("ready_latency_p90_ns", row.ready_latency_p90_ns);
1384 try object.field("ready_latency_p99_ns", row.ready_latency_p99_ns);
1385 try object.field("ready_latency_max_ns", row.ready_latency_max_ns);
1386 }
1387
1388 fn systemGroupTag(group: system_mod.Group) []const u8 {
1389 return switch (group) {
1390 .kind => "kind",
1391 .thread => "thread",
1392 .cpu => "cpu",
1393 .address => "address",
1394 .topology => "topology",
1395 .name => "name",
1396 .none => "none",
1397 };
1398 }
1399
1400 fn writeSystemSummaryTextFields(writer: *std.Io.Writer, row: SystemSummary) !void {
1401 if (row.kind) |kind| {
1402 try writer.writeAll(" kind=");
1403 try pretty_json.writeString(writer, kind);
1404 }
1405 if (row.thread) |thread| try writer.print(" thread={d}", .{thread});
1406 if (row.pid) |pid| try writer.print(" pid={d}", .{pid});
1407 if (row.cpu) |cpu| try writer.print(" cpu={d}", .{cpu});
1408 if (row.cpu_package) |package| try writer.print(" cpu_package={d}", .{package});
1409 if (row.cpu_die) |die| try writer.print(" cpu_die={d}", .{die});
1410 if (row.cpu_core) |core| try writer.print(" cpu_core={d}", .{core});
1411 if (row.address) |address| try writer.print(" address=0x{x}", .{address});
1412 if (row.value_count != 0) {
1413 try writer.print(
1414 " values={d} value_min={d} value_max={d} value_mean={d}",
1415 .{ row.value_count, row.value_min, row.value_max, row.valueMean() },
1416 );
1417 }
1418 if (row.value_last) |value| try writer.print(" value_last={d}", .{value});
1419 if (row.power_delta_uj != 0) try writer.print(" power_delta_uj={d}", .{row.power_delta_uj});
1420 if (row.first_ns != 0) try writer.print(" first_ns={d}", .{row.first_ns});
1421 if (row.last_ns != 0) try writer.print(" last_ns={d}", .{row.last_ns});
1422 }
1423
1424 fn writeSystemSummaryFields(object: pretty_json.Object, row: SystemSummary) !void {
1425 if (row.kind) |kind| try object.field("system_kind", kind);
1426 if (row.thread) |thread| try object.field("thread", thread);
1427 if (row.pid) |pid| try object.field("pid", pid);
1428 if (row.cpu) |cpu| try object.field("cpu", cpu);
1429 if (row.cpu_package) |package| try object.field("cpu_package", package);
1430 if (row.cpu_die) |die| try object.field("cpu_die", die);
1431 if (row.cpu_core) |core| try object.field("cpu_core", core);
1432 if (row.address) |address| try object.field("address", address);
1433 if (row.value_count != 0) {
1434 try object.field("values", row.value_count);
1435 try object.field("value_min", row.value_min);
1436 try object.field("value_max", row.value_max);
1437 try object.field("value_mean", row.valueMean());
1438 }
1439 if (row.value_last) |value| try object.field("value_last", value);
1440 if (row.power_delta_uj != 0) try object.field("power_delta_uj", row.power_delta_uj);
1441 if (row.first_ns != 0) try object.field("first_ns", row.first_ns);
1442 if (row.last_ns != 0) try object.field("last_ns", row.last_ns);
1443 }
1444
1445 fn zoneGreaterThan(_: void, left: ZoneSummary, right: ZoneSummary) bool {
1446 if (left.total_ns != right.total_ns) return left.total_ns > right.total_ns;
1447 if (left.count != right.count) return left.count > right.count;
1448 return std.mem.lessThan(u8, left.name, right.name);
1449 }
1450
1451 fn zonePrefixGreaterThan(_: void, left: ZonePrefixSummary, right: ZonePrefixSummary) bool {
1452 if (left.total_ns != right.total_ns) return left.total_ns > right.total_ns;
1453 if (left.count != right.count) return left.count > right.count;
1454 return std.mem.lessThan(u8, left.name, right.name);
1455 }
1456
1457 fn zoneThreadGreaterThan(_: void, left: ZoneThreadSummary, right: ZoneThreadSummary) bool {
1458 if (left.total_ns != right.total_ns) return left.total_ns > right.total_ns;
1459 if (left.count != right.count) return left.count > right.count;
1460 return left.thread < right.thread;
1461 }
1462
1463 fn durationLessThan(_: void, left: u64, right: u64) bool {
1464 return left < right;
1465 }
1466
1467 test "capture integrity accepts one contiguous trace lifecycle" {
1468 var trace = std.Io.Writer.Allocating.init(std.testing.allocator);
1469 defer trace.deinit();
1470 try (event.TraceEvent{ .seq = 1, .kind = .start }).writeJsonLine(&trace.writer);
1471 try (event.TraceEvent{ .seq = 2, .kind = .message }).writeJsonLine(&trace.writer);
1472 try (event.TraceEvent{ .seq = 3, .kind = .stop }).writeJsonLine(&trace.writer);
1473
1474 var analyzer = Analyzer.init(std.testing.allocator);
1475 defer analyzer.deinit();
1476 try analyzer.ingestJsonlBytes(trace.written());
1477 const integrity = analyzer.captureIntegrity();
1478 try std.testing.expectEqualStrings("complete", integrity.status);
1479 try std.testing.expect(integrity.message == null);
1480 try std.testing.expectEqual(@as(u64, 3), integrity.sequenced_event_count);
1481 try std.testing.expectEqual(@as(?u64, 1), integrity.first_sequence);
1482 try std.testing.expectEqual(@as(?u64, 3), integrity.last_sequence);
1483 try std.testing.expectEqualStrings("complete", analyzer.context.captureIntegrity().status);
1484 }
1485
1486 test "summary preserves context ready latency evidence" {
1487 var trace = std.Io.Writer.Allocating.init(std.testing.allocator);
1488 defer trace.deinit();
1489 try (event.TraceEvent{ .seq = 1, .kind = .start, .time_ns = 10 })
1490 .writeJsonLine(&trace.writer);
1491 try (event.TraceEvent{
1492 .seq = 2,
1493 .kind = .thread_wakeup,
1494 .time_ns = 20,
1495 .target_thread = 7,
1496 }).writeJsonLine(&trace.writer);
1497 try (event.TraceEvent{
1498 .seq = 3,
1499 .kind = .context_switch,
1500 .time_ns = 70,
1501 .prev_thread = 8,
1502 .next_thread = 7,
1503 }).writeJsonLine(&trace.writer);
1504 try (event.TraceEvent{ .seq = 4, .kind = .stop, .time_ns = 80 })
1505 .writeJsonLine(&trace.writer);
1506
1507 var analyzer = Analyzer.init(std.testing.allocator);
1508 defer analyzer.deinit();
1509 try analyzer.ingestJsonlBytes(trace.written());
1510 try std.testing.expectEqualStrings("complete", analyzer.context.readyLatencyEvidence());
1511 try std.testing.expectEqual(@as(u64, 1), analyzer.context.counters.ready_latency_samples);
1512
1513 var text = std.Io.Writer.Allocating.init(std.testing.allocator);
1514 defer text.deinit();
1515 try analyzer.writeSummary(&text.writer, .{});
1516 try std.testing.expect(std.mem.indexOf(
1517 u8,
1518 text.written(),
1519 "context_ready_latency_evidence=complete",
1520 ) != null);
1521 try std.testing.expect(std.mem.indexOf(
1522 u8,
1523 text.written(),
1524 "ready_latency_p99_ns=50",
1525 ) != null);
1526
1527 var jsonl = std.Io.Writer.Allocating.init(std.testing.allocator);
1528 defer jsonl.deinit();
1529 try analyzer.writeSummaryJsonl(&jsonl.writer, .{});
1530 try std.testing.expect(std.mem.indexOf(
1531 u8,
1532 jsonl.written(),
1533 "\"context_ready_latency_evidence\":\"complete\"",
1534 ) != null);
1535 }
1536
1537 test "summary preserves lock duration evidence" {
1538 var trace = std.Io.Writer.Allocating.init(std.testing.allocator);
1539 defer trace.deinit();
1540 try (event.TraceEvent{ .seq = 1, .kind = .start, .time_ns = 10 })
1541 .writeJsonLine(&trace.writer);
1542 try (event.TraceEvent{
1543 .seq = 2,
1544 .kind = .lock_announce,
1545 .time_ns = 15,
1546 .thread = 7,
1547 .id = 9,
1548 .name = "queue",
1549 }).writeJsonLine(&trace.writer);
1550 try (event.TraceEvent{ .seq = 3, .kind = .lock_wait, .time_ns = 20, .thread = 7, .id = 9 })
1551 .writeJsonLine(&trace.writer);
1552 try (event.TraceEvent{ .seq = 4, .kind = .lock_obtain, .time_ns = 70, .thread = 7, .id = 9 })
1553 .writeJsonLine(&trace.writer);
1554 try (event.TraceEvent{ .seq = 5, .kind = .lock_release, .time_ns = 100, .thread = 7, .id = 9 })
1555 .writeJsonLine(&trace.writer);
1556 try (event.TraceEvent{ .seq = 6, .kind = .stop, .time_ns = 110 })
1557 .writeJsonLine(&trace.writer);
1558 const flight_report = summaryLockFlightReport();
1559 try flight_report.writeJsonl(&trace.writer);
1560
1561 var analyzer = Analyzer.init(std.testing.allocator);
1562 defer analyzer.deinit();
1563 try analyzer.ingestJsonlBytes(trace.written());
1564 try std.testing.expectEqualStrings("complete", analyzer.locks.durationEvidence());
1565 try std.testing.expectEqualDeep(
1566 flight_report,
1567 analyzer.locks.captureIntegrity().flight_report.?,
1568 );
1569 try std.testing.expectEqualDeep(
1570 flight_report,
1571 analyzer.memory.captureIntegrity().flight_report.?,
1572 );
1573 try std.testing.expectEqual(@as(u64, 1), analyzer.locks.counters.wait_samples);
1574 try std.testing.expectEqual(@as(u64, 1), analyzer.locks.counters.hold_samples);
1575 var rows = try analyzer.collectLocks();
1576 defer rows.deinit(std.testing.allocator);
1577 try std.testing.expectEqual(@as(u64, 50), rows.items[0].wait_p99_ns);
1578 try std.testing.expectEqual(@as(u64, 30), rows.items[0].hold_p99_ns);
1579
1580 var text = std.Io.Writer.Allocating.init(std.testing.allocator);
1581 defer text.deinit();
1582 try analyzer.writeSummary(&text.writer, .{});
1583 try std.testing.expect(std.mem.indexOf(
1584 u8,
1585 text.written(),
1586 "lock_duration_evidence=complete",
1587 ) != null);
1588 try std.testing.expect(std.mem.indexOf(u8, text.written(), "wait_p99_ns=50") != null);
1589
1590 var jsonl = std.Io.Writer.Allocating.init(std.testing.allocator);
1591 defer jsonl.deinit();
1592 try analyzer.writeSummaryJsonl(&jsonl.writer, .{});
1593 try std.testing.expect(std.mem.indexOf(
1594 u8,
1595 jsonl.written(),
1596 "\"lock_duration_evidence\":\"complete\"",
1597 ) != null);
1598 try std.testing.expect(std.mem.indexOf(u8, jsonl.written(), "\"hold_p99_ns\":30") != null);
1599 }
1600
1601 fn summaryLockFlightReport() transport.Report {
1602 return .{
1603 .policy = .overwrite_oldest,
1604 .state = .accepting,
1605 .capacity_bytes = 64,
1606 .retained_bytes = 32,
1607 .event_capacity_bytes = 16,
1608 .writer_capacity_bytes = 8,
1609 .observed_events = 6,
1610 .stored_events = 6,
1611 .retained_events = 6,
1612 .overwritten_events = 0,
1613 .dropped_events = 0,
1614 .oversized_events = 0,
1615 .partial_event_bytes = 0,
1616 .discarding_oversized_event = false,
1617 };
1618 }
1619
1620 test "capture integrity counts sequence gaps as lower bound missing rows" {
1621 var trace = std.Io.Writer.Allocating.init(std.testing.allocator);
1622 defer trace.deinit();
1623 try (event.TraceEvent{ .seq = 1, .kind = .start }).writeJsonLine(&trace.writer);
1624 try (event.TraceEvent{ .seq = 4, .kind = .message }).writeJsonLine(&trace.writer);
1625 try (event.TraceEvent{ .seq = 7, .kind = .stop }).writeJsonLine(&trace.writer);
1626
1627 var analyzer = Analyzer.init(std.testing.allocator);
1628 defer analyzer.deinit();
1629 try analyzer.ingestJsonlBytes(trace.written());
1630 const integrity = analyzer.captureIntegrity();
1631 try std.testing.expectEqualStrings("sequence_gaps", integrity.status);
1632 try std.testing.expectEqual(@as(u64, 2), integrity.sequence_gap_count);
1633 try std.testing.expectEqual(@as(u64, 4), integrity.missing_sequence_event_count);
1634
1635 var jsonl = std.Io.Writer.Allocating.init(std.testing.allocator);
1636 defer jsonl.deinit();
1637 try analyzer.writeSummaryJsonl(&jsonl.writer, .{});
1638 var lines = std.mem.splitScalar(u8, jsonl.written(), '\n');
1639 const summary_line = lines.next().?;
1640 var parsed = try std.json.parseFromSlice(
1641 std.json.Value,
1642 std.testing.allocator,
1643 summary_line,
1644 .{},
1645 );
1646 defer parsed.deinit();
1647 try std.testing.expect(std.mem.indexOf(
1648 u8,
1649 jsonl.written(),
1650 "\"capture_integrity\":{\"method\":\"tracy_event_sequence_and_lifecycle_v1\"",
1651 ) != null);
1652 try std.testing.expect(std.mem.indexOf(
1653 u8,
1654 jsonl.written(),
1655 "\"missing_sequence_event_count\":4",
1656 ) != null);
1657
1658 var text = std.Io.Writer.Allocating.init(std.testing.allocator);
1659 defer text.deinit();
1660 try analyzer.writeSummary(&text.writer, .{});
1661 try std.testing.expect(std.mem.indexOf(
1662 u8,
1663 text.written(),
1664 "capture_integrity=sequence_gaps",
1665 ) != null);
1666 try std.testing.expect(std.mem.indexOf(u8, text.written(), "first_sequence=1") != null);
1667 }
1668
1669 test "capture integrity classifies metadata lifecycle and balance failures" {
1670 var analyzer = Analyzer.init(std.testing.allocator);
1671 defer analyzer.deinit();
1672 try std.testing.expectEqualStrings("no_events", analyzer.captureIntegrity().status);
1673
1674 analyzer.capture.counters.events = 2;
1675 analyzer.capture.counters.unsequenced_events = 2;
1676 try std.testing.expectEqualStrings(
1677 "missing_sequence_metadata",
1678 analyzer.captureIntegrity().status,
1679 );
1680
1681 analyzer.capture.counters.unsequenced_events = 0;
1682 analyzer.capture.counters.sequenced_events = 2;
1683 analyzer.capture.first_sequence = 1;
1684 analyzer.capture.last_sequence = 2;
1685 analyzer.capture.counters.start_events = 1;
1686 analyzer.capture.start_sequence = 1;
1687 try std.testing.expectEqualStrings("missing_stop_event", analyzer.captureIntegrity().status);
1688
1689 analyzer.capture.counters.stop_events = 1;
1690 analyzer.capture.stop_sequence = 2;
1691 analyzer.counters.unmatched_zone_ends = 1;
1692 try std.testing.expectEqualStrings("unbalanced_events", analyzer.captureIntegrity().status);
1693
1694 analyzer.capture.stop_sequence = 1;
1695 try std.testing.expectEqualStrings("lifecycle_not_bounded", analyzer.captureIntegrity().status);
1696
1697 analyzer.capture.counters.sequence_regressions = 1;
1698 try std.testing.expectEqualStrings(
1699 "non_monotonic_sequence",
1700 analyzer.captureIntegrity().status,
1701 );
1702 }
1703
1704 test "capture integrity rejects active fiber and gpu lifecycles" {
1705 var fiber_trace = std.Io.Writer.Allocating.init(std.testing.allocator);
1706 defer fiber_trace.deinit();
1707 try (event.TraceEvent{ .seq = 1, .kind = .start }).writeJsonLine(&fiber_trace.writer);
1708 try (event.TraceEvent{
1709 .seq = 2,
1710 .kind = .fiber_enter,
1711 .thread = 1,
1712 .id = 7,
1713 }).writeJsonLine(&fiber_trace.writer);
1714 try (event.TraceEvent{ .seq = 3, .kind = .stop }).writeJsonLine(&fiber_trace.writer);
1715 var fiber_analyzer = Analyzer.init(std.testing.allocator);
1716 defer fiber_analyzer.deinit();
1717 try fiber_analyzer.ingestJsonlBytes(fiber_trace.written());
1718 try std.testing.expectEqualStrings(
1719 "unbalanced_events",
1720 fiber_analyzer.captureIntegrity().status,
1721 );
1722
1723 var gpu_trace = std.Io.Writer.Allocating.init(std.testing.allocator);
1724 defer gpu_trace.deinit();
1725 try (event.TraceEvent{ .seq = 1, .kind = .start }).writeJsonLine(&gpu_trace.writer);
1726 try (event.TraceEvent{
1727 .seq = 2,
1728 .kind = .gpu_zone_begin,
1729 .thread = 1,
1730 .gpu_context = 1,
1731 .gpu_query = 2,
1732 }).writeJsonLine(&gpu_trace.writer);
1733 try (event.TraceEvent{ .seq = 3, .kind = .stop }).writeJsonLine(&gpu_trace.writer);
1734 var gpu_analyzer = Analyzer.init(std.testing.allocator);
1735 defer gpu_analyzer.deinit();
1736 try gpu_analyzer.ingestJsonlBytes(gpu_trace.written());
1737 try std.testing.expectEqualStrings(
1738 "unbalanced_events",
1739 gpu_analyzer.captureIntegrity().status,
1740 );
1741 }
1742
1743 test "summary excludes regressed zone timestamps from duration samples" {
1744 var trace = std.Io.Writer.Allocating.init(std.testing.allocator);
1745 defer trace.deinit();
1746 const rows = [_]event.TraceEvent{
1747 .{ .seq = 1, .kind = .start, .time_ns = 10 },
1748 .{
1749 .seq = 2,
1750 .kind = .zone_begin,
1751 .time_ns = 100,
1752 .thread = 7,
1753 .id = 1,
1754 .name = "agent.phase",
1755 },
1756 .{ .seq = 3, .kind = .zone_end, .time_ns = 90, .thread = 7, .id = 1 },
1757 .{ .seq = 4, .kind = .stop, .time_ns = 110 },
1758 };
1759 for (rows) |row| try row.writeJsonLine(&trace.writer);
1760
1761 var analyzer = Analyzer.init(std.testing.allocator);
1762 defer analyzer.deinit();
1763 try analyzer.ingestJsonlBytes(trace.written());
1764 const zone = analyzer.zone_summaries.get("agent.phase").?;
1765 try std.testing.expectEqual(@as(u64, 1), zone.count);
1766 try std.testing.expectEqual(@as(u64, 0), zone.duration_samples);
1767 try std.testing.expectEqual(@as(u64, 0), zone.total_ns);
1768 try std.testing.expectEqual(@as(u64, 1), analyzer.counters.zone_timestamp_regressions);
1769 try std.testing.expectEqualStrings("complete", analyzer.captureIntegrity().status);
1770 try std.testing.expectEqualStrings("partial", analyzer.zoneDurationEvidence());
1771
1772 var output = std.Io.Writer.Allocating.init(std.testing.allocator);
1773 defer output.deinit();
1774 try analyzer.writeSummaryJsonl(&output.writer, .{});
1775 try std.testing.expect(
1776 std.mem.indexOf(u8, output.written(), "\"zone_duration_evidence\":\"partial\"") != null,
1777 );
1778 try std.testing.expect(
1779 std.mem.indexOf(u8, output.written(), "\"count\":1,\"duration_samples\":0") != null,
1780 );
1781 }
1782
1783 test "summary aggregates zones, plots, and allocation pressure" {
1784 var out = std.Io.Writer.Allocating.init(std.testing.allocator);
1785 defer out.deinit();
1786 try (event.TraceEvent{ .seq = 1, .kind = .start, .time_ns = 100, .thread = 1, .name = "test" }).writeJsonLine(&out.writer);
1787 try (event.TraceEvent{ .seq = 2, .kind = .thread_name, .time_ns = 105, .thread = 1, .name = "main" }).writeJsonLine(&out.writer);
1788 try (event.TraceEvent{ .seq = 3, .kind = .zone_begin, .time_ns = 110, .thread = 1, .id = 7, .name = "agent.phase", .file = "phase.zig", .line = 9 }).writeJsonLine(&out.writer);
1789 try (event.TraceEvent{ .seq = 3, .kind = .zone_end, .time_ns = 160, .thread = 1, .id = 7 }).writeJsonLine(&out.writer);
1790 try (event.TraceEvent{ .seq = 4, .kind = .plot, .time_ns = 170, .thread = 1, .name = "sample.ns", .value_i64 = 12, .plot_kind = "int" }).writeJsonLine(&out.writer);
1791 try (event.TraceEvent{ .seq = 5, .kind = .alloc, .time_ns = 180, .thread = 1, .name = "arena", .address = 4096, .size = 64 }).writeJsonLine(&out.writer);
1792 try (event.TraceEvent{ .seq = 6, .kind = .free, .time_ns = 190, .thread = 1, .name = "arena", .address = 4096 }).writeJsonLine(&out.writer);
1793 try (event.TraceEvent{ .seq = 7, .kind = .sample, .time_ns = 195, .thread = 1, .id = 3, .name = "tick", .file = "phase.zig", .line = 9, .sample_kind = "cpu" }).writeJsonLine(&out.writer);
1794 try (event.TraceEvent{ .seq = 8, .kind = .sample_frame, .time_ns = 195, .thread = 1, .id = 3, .name = "tick", .file = "phase.zig", .line = 9, .sample_kind = "cpu" }).writeJsonLine(&out.writer);
1795 try (event.TraceEvent{ .seq = 9, .kind = .context_switch, .time_ns = 196, .thread = 1, .cpu = 0, .next_thread = 10 }).writeJsonLine(&out.writer);
1796 try (event.TraceEvent{ .seq = 10, .kind = .thread_wakeup, .time_ns = 197, .thread = 1, .target_thread = 10, .cpu = 0 }).writeJsonLine(&out.writer);
1797 try (event.TraceEvent{ .seq = 11, .kind = .gpu_context, .time_ns = 198, .thread = 1, .gpu_context = 1, .gpu_period = 1.0 }).writeJsonLine(&out.writer);
1798 try (event.TraceEvent{ .seq = 12, .kind = .gpu_zone_begin, .time_ns = 199, .thread = 1, .gpu_context = 1, .gpu_query = 10, .name = "draw" }).writeJsonLine(&out.writer);
1799 try (event.TraceEvent{ .seq = 13, .kind = .gpu_zone_end, .time_ns = 210, .thread = 1, .gpu_context = 1, .gpu_query = 11 }).writeJsonLine(&out.writer);
1800 try (event.TraceEvent{ .seq = 14, .kind = .gpu_time, .time_ns = 211, .thread = 1, .gpu_context = 1, .gpu_query = 10, .gpu_time = 100 }).writeJsonLine(&out.writer);
1801 try (event.TraceEvent{ .seq = 15, .kind = .gpu_time, .time_ns = 212, .thread = 1, .gpu_context = 1, .gpu_query = 11, .gpu_time = 140 }).writeJsonLine(&out.writer);
1802 try (event.TraceEvent{ .seq = 16, .kind = .thread_pid, .time_ns = 213, .thread = 10, .pid = 900 }).writeJsonLine(&out.writer);
1803 try (event.TraceEvent{ .seq = 17, .kind = .cpu_topology, .time_ns = 214, .thread = 1, .cpu = 0, .cpu_package = 1, .cpu_die = 0, .cpu_core = 2 }).writeJsonLine(&out.writer);
1804 try (event.TraceEvent{ .seq = 18, .kind = .sys_time, .time_ns = 215, .thread = 1, .value_f64 = 0.25 }).writeJsonLine(&out.writer);
1805 try (event.TraceEvent{ .seq = 19, .kind = .sys_power, .time_ns = 216, .thread = 1, .name = "package", .power_delta_uj = 100 }).writeJsonLine(&out.writer);
1806 try (event.TraceEvent{ .seq = 20, .kind = .hw_sample, .time_ns = 217, .thread = 10, .cpu = 0, .address = 0x1234, .hw_sample_kind = "cache-miss" }).writeJsonLine(&out.writer);
1807 try (event.TraceEvent{ .seq = 21, .kind = .lock_announce, .time_ns = 218, .thread = 1, .id = 44, .name = "scheduler-lock", .lock_kind = "lock" }).writeJsonLine(&out.writer);
1808 try (event.TraceEvent{ .seq = 22, .kind = .lock_wait, .time_ns = 219, .thread = 1, .id = 44 }).writeJsonLine(&out.writer);
1809 try (event.TraceEvent{ .seq = 23, .kind = .lock_obtain, .time_ns = 229, .thread = 1, .id = 44 }).writeJsonLine(&out.writer);
1810 try (event.TraceEvent{ .seq = 24, .kind = .lock_release, .time_ns = 245, .thread = 1, .id = 44 }).writeJsonLine(&out.writer);
1811 try (event.TraceEvent{ .seq = 25, .kind = .stop, .time_ns = 250, .thread = 1 }).writeJsonLine(&out.writer);
1812
1813 var analyzer = Analyzer.init(std.testing.allocator);
1814 defer analyzer.deinit();
1815 try analyzer.ingestJsonlBytes(out.written());
1816 try std.testing.expectEqual(@as(u64, 1), analyzer.counters.completed_zones);
1817 try std.testing.expectEqual(@as(u64, 50), analyzer.zone_summaries.get("agent.phase").?.total_ns);
1818 try std.testing.expectEqual(@as(u64, 50), analyzer.zone_summaries.get("agent.phase").?.percentileNs(50));
1819 try std.testing.expectEqual(@as(u64, 50), analyzer.zone_prefix_summaries.get("agent").?.total_ns);
1820 try std.testing.expectEqual(@as(u64, 50), analyzer.zone_thread_summaries.get(1).?.total_ns);
1821 try std.testing.expectEqualStrings("main", analyzer.zone_thread_summaries.get(1).?.name.?);
1822 try std.testing.expectEqual(@as(u64, 64), analyzer.memory.counters.allocated_bytes);
1823 try std.testing.expectEqual(@as(u64, 64), analyzer.memory.counters.freed_bytes);
1824 try std.testing.expectEqual(@as(u64, 0), analyzer.memory.counters.live_bytes);
1825 try std.testing.expectEqual(@as(u64, 64), analyzer.memory.summaries.get("arena").?.high_water_live_bytes);
1826 try std.testing.expectEqual(@as(u64, 1), analyzer.samples.counters.samples);
1827 try std.testing.expectEqual(@as(u64, 1), analyzer.samples.counters.frames);
1828 try std.testing.expectEqual(@as(u64, 1), analyzer.context.counters.switches);
1829 try std.testing.expectEqual(@as(u64, 1), analyzer.context.counters.wakeups);
1830 try std.testing.expectEqual(@as(u64, 1), analyzer.gpu.counters.completed_zones);
1831 try std.testing.expectEqual(@as(u64, 40), analyzer.gpu.gpuNs());
1832 try std.testing.expectEqual(@as(u64, 1), analyzer.system.counters.thread_pid_maps);
1833 try std.testing.expectEqual(@as(u64, 1), analyzer.system.counters.cpu_topology);
1834 try std.testing.expectEqual(@as(u64, 1), analyzer.system.counters.sys_time);
1835 try std.testing.expectEqual(@as(u64, 1), analyzer.system.counters.sys_power);
1836 try std.testing.expectEqual(@as(u64, 1), analyzer.system.counters.hw_samples);
1837 try std.testing.expectEqual(@as(u64, 1), analyzer.locks.counters.completed_waits);
1838 try std.testing.expectEqual(@as(u64, 1), analyzer.locks.counters.completed_holds);
1839
1840 var jsonl = std.Io.Writer.Allocating.init(std.testing.allocator);
1841 defer jsonl.deinit();
1842 try analyzer.writeSummaryJsonl(&jsonl.writer, .{ .top = 4 });
1843 const text = jsonl.written();
1844 try std.testing.expect(std.mem.indexOf(u8, text, "\"kind\":\"summary\"") != null);
1845 try std.testing.expect(std.mem.indexOf(u8, text, "\"kind\":\"zone\"") != null);
1846 try std.testing.expect(std.mem.indexOf(u8, text, "\"p50_ns\":50") != null);
1847 try std.testing.expect(std.mem.indexOf(u8, text, "\"kind\":\"zone_prefix\"") != null);
1848 try std.testing.expect(std.mem.indexOf(u8, text, "\"kind\":\"zone_thread\"") != null);
1849 try std.testing.expect(std.mem.indexOf(u8, text, "\"kind\":\"sample\"") != null);
1850 try std.testing.expect(std.mem.indexOf(u8, text, "\"kind\":\"lock\"") != null);
1851 try std.testing.expect(std.mem.indexOf(u8, text, "\"kind\":\"context\"") != null);
1852 try std.testing.expect(std.mem.indexOf(u8, text, "\"lock_waits\":1") != null);
1853 try std.testing.expect(std.mem.indexOf(u8, text, "\"context_switches\":1") != null);
1854 }
1855
1856 test "summary preserves completed GPU duration tails" {
1857 var trace = std.Io.Writer.Allocating.init(std.testing.allocator);
1858 defer trace.deinit();
1859 try (event.TraceEvent{ .seq = 1, .kind = .start, .time_ns = 1 })
1860 .writeJsonLine(&trace.writer);
1861 try (event.TraceEvent{
1862 .seq = 2,
1863 .kind = .gpu_zone_begin,
1864 .time_ns = 100,
1865 .thread = 1,
1866 .name = "draw",
1867 .gpu_context = 1,
1868 .gpu_query = 10,
1869 }).writeJsonLine(&trace.writer);
1870 try (event.TraceEvent{
1871 .seq = 3,
1872 .kind = .gpu_zone_end,
1873 .time_ns = 120,
1874 .thread = 1,
1875 .gpu_context = 1,
1876 .gpu_query = 11,
1877 }).writeJsonLine(&trace.writer);
1878 try (event.TraceEvent{
1879 .seq = 4,
1880 .kind = .gpu_time,
1881 .time_ns = 121,
1882 .gpu_context = 1,
1883 .gpu_query = 10,
1884 .gpu_time = 100,
1885 }).writeJsonLine(&trace.writer);
1886 try (event.TraceEvent{
1887 .seq = 5,
1888 .kind = .gpu_time,
1889 .time_ns = 122,
1890 .gpu_context = 1,
1891 .gpu_query = 11,
1892 .gpu_time = 140,
1893 }).writeJsonLine(&trace.writer);
1894 try (event.TraceEvent{ .seq = 6, .kind = .stop, .time_ns = 130 })
1895 .writeJsonLine(&trace.writer);
1896 var analyzer = Analyzer.init(std.testing.allocator);
1897 defer analyzer.deinit();
1898 try analyzer.ingestJsonlBytes(trace.written());
1899 try std.testing.expectEqualStrings("complete", analyzer.gpuDurationEvidence());
1900
1901 var text = std.Io.Writer.Allocating.init(std.testing.allocator);
1902 defer text.deinit();
1903 try analyzer.writeSummary(&text.writer, .{});
1904 try expectSummaryContains(text.written(), "gpu_duration_evidence=complete");
1905 try expectSummaryContains(text.written(), "gpu_p99_ns=40");
1906 var jsonl = std.Io.Writer.Allocating.init(std.testing.allocator);
1907 defer jsonl.deinit();
1908 try analyzer.writeSummaryJsonl(&jsonl.writer, .{});
1909 try expectSummaryContains(jsonl.written(), "\"gpu_duration_evidence\":\"complete\"");
1910 try expectSummaryContains(jsonl.written(), "\"gpu_p99_ns\":40");
1911 }
1912
1913 test "summary marks sized frees without prior allocation unmatched" {
1914 var out = std.Io.Writer.Allocating.init(std.testing.allocator);
1915 defer out.deinit();
1916 try (event.TraceEvent{ .seq = 1, .kind = .free, .time_ns = 100, .thread = 1, .name = "external", .address = 4096, .size = 96 }).writeJsonLine(&out.writer);
1917
1918 var analyzer = Analyzer.init(std.testing.allocator);
1919 defer analyzer.deinit();
1920 try analyzer.ingestJsonlBytes(out.written());
1921 try std.testing.expectEqual(@as(u64, 1), analyzer.memory.counters.frees);
1922 try std.testing.expectEqual(@as(u64, 96), analyzer.memory.counters.freed_bytes);
1923 try std.testing.expectEqual(@as(u64, 1), analyzer.memory.counters.unmatched_frees);
1924 try std.testing.expectEqual(@as(u64, 96), analyzer.memory.summaries.get("external").?.freed_bytes);
1925 }
1926
1927 test "summary preserves completed memory lifetime evidence" {
1928 var trace = std.Io.Writer.Allocating.init(std.testing.allocator);
1929 defer trace.deinit();
1930 try (event.TraceEvent{ .seq = 1, .kind = .start, .time_ns = 1 })
1931 .writeJsonLine(&trace.writer);
1932 try (event.TraceEvent{ .seq = 2, .kind = .message, .time_ns = 10, .text = "load" })
1933 .writeJsonLine(&trace.writer);
1934 try (event.TraceEvent{
1935 .seq = 3,
1936 .kind = .alloc,
1937 .time_ns = 100,
1938 .thread = 1,
1939 .name = "arena",
1940 .address = 4096,
1941 .size = 64,
1942 }).writeJsonLine(&trace.writer);
1943 try (event.TraceEvent{
1944 .seq = 4,
1945 .kind = .free,
1946 .time_ns = 180,
1947 .thread = 2,
1948 .name = "arena",
1949 .address = 4096,
1950 .size = 64,
1951 }).writeJsonLine(&trace.writer);
1952 try (event.TraceEvent{ .seq = 5, .kind = .stop, .time_ns = 200 })
1953 .writeJsonLine(&trace.writer);
1954
1955 var analyzer = Analyzer.init(std.testing.allocator);
1956 defer analyzer.deinit();
1957 try analyzer.ingestJsonlBytes(trace.written());
1958 try std.testing.expectEqual(@as(u64, 5), analyzer.memory.capture.counters.events);
1959 try std.testing.expectEqualStrings("complete", analyzer.memory.lifetimeEvidence());
1960 var rows = try analyzer.collectMemory();
1961 defer rows.deinit(std.testing.allocator);
1962 try std.testing.expectEqual(@as(u64, 1), rows.items[0].lifetime_samples);
1963 try std.testing.expectEqual(@as(u64, 80), rows.items[0].p99_lifetime_ns);
1964
1965 var jsonl = std.Io.Writer.Allocating.init(std.testing.allocator);
1966 defer jsonl.deinit();
1967 try analyzer.writeSummaryJsonl(&jsonl.writer, .{});
1968 try std.testing.expect(std.mem.indexOf(
1969 u8,
1970 jsonl.written(),
1971 "\"memory_lifetime_evidence\":\"complete\"",
1972 ) != null);
1973 try std.testing.expect(std.mem.indexOf(
1974 u8,
1975 jsonl.written(),
1976 "\"lifetime_p99_ns\":80",
1977 ) != null);
1978 }
1979
1980 test "summary retains plot configuration metadata" {
1981 var trace = std.Io.Writer.Allocating.init(std.testing.allocator);
1982 defer trace.deinit();
1983 try (event.TraceEvent{ .seq = 1, .kind = .start }).writeJsonLine(&trace.writer);
1984 try (event.TraceEvent{
1985 .seq = 2,
1986 .kind = .plot_config,
1987 .name = "queue.depth",
1988 .color = 12,
1989 .plot_unit = "count",
1990 .plot_step = true,
1991 }).writeJsonLine(&trace.writer);
1992 try (event.TraceEvent{
1993 .seq = 3,
1994 .kind = .plot,
1995 .name = "queue.depth",
1996 .value_i64 = 4,
1997 .plot_kind = "int",
1998 }).writeJsonLine(&trace.writer);
1999 try (event.TraceEvent{ .seq = 4, .kind = .stop }).writeJsonLine(&trace.writer);
2000
2001 var analyzer = Analyzer.init(std.testing.allocator);
2002 defer analyzer.deinit();
2003 try analyzer.ingestJsonlBytes(trace.written());
2004 var jsonl = std.Io.Writer.Allocating.init(std.testing.allocator);
2005 defer jsonl.deinit();
2006 try analyzer.writeSummaryJsonl(&jsonl.writer, .{});
2007 const text = jsonl.written();
2008 try std.testing.expect(std.mem.indexOf(u8, text, "\"plot_configurations\":1") != null);
2009 try std.testing.expect(std.mem.indexOf(u8, text, "\"name\":\"queue.depth\"") != null);
2010 try std.testing.expect(std.mem.indexOf(u8, text, "\"unit\":\"count\"") != null);
2011 try std.testing.expect(std.mem.indexOf(u8, text, "\"configured\":true") != null);
2012 try std.testing.expect(std.mem.indexOf(u8, text, "\"step\":true") != null);
2013 try std.testing.expect(std.mem.indexOf(u8, text, "\"color\":12") != null);
2014 }
2015
2016 fn expectSummaryContains(haystack: []const u8, needle: []const u8) !void {
2017 try std.testing.expect(std.mem.indexOf(u8, haystack, needle) != null);
2018 }