lib/tracy/src/compare.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const pretty_json = @import("pretty").json;
3 const event = @import("event.zig");
4 const frame_mod = @import("frame.zig");
5 const gpu_mod = @import("gpu.zig");
6 const memory_mod = @import("memory.zig");
7 const plot_mod = @import("plot.zig");
8 const report = @import("report.zig");
9 const summary = @import("summary.zig");
10
11 pub const schema = "tracy.compare/v0";
12
13 pub const Options = struct {
14 top: usize = 20,
15 frame_budget_ns: ?u64 = null,
16 min_frame_delta_ns: u64 = 0,
17 min_zone_delta_ns: u64 = 0,
18 min_gpu_delta_ns: u64 = 0,
19 min_memory_delta_bytes: u64 = 0,
20 min_memory_lifetime_delta_ns: u64 = 0,
21 include_stable: bool = false,
22 };
23
24 pub const Analyzer = struct {
25 summary: summary.Analyzer,
26 frames: frame_mod.Analyzer,
27
28 pub fn init(allocator: std.mem.Allocator) Analyzer {
29 return .{
30 .summary = summary.Analyzer.init(allocator),
31 .frames = frame_mod.Analyzer.init(allocator),
32 };
33 }
34
35 pub fn deinit(self: *Analyzer) void {
36 self.frames.deinit();
37 self.summary.deinit();
38 self.* = undefined;
39 }
40
41 pub fn ingestJsonlBytes(self: *Analyzer, bytes: []const u8) !void {
42 var lines = std.mem.splitScalar(u8, bytes, '\n');
43 while (lines.next()) |line| try self.ingestJsonLine(line);
44 }
45
46 pub fn ingestJsonLine(self: *Analyzer, line: []const u8) !void {
47 try self.summary.ingestJsonLine(line);
48 try self.frames.ingestJsonLine(line);
49 }
50 };
51
52 const Status = enum {
53 changed,
54 new,
55 removed,
56 stable,
57
58 fn tag(self: Status) []const u8 {
59 return switch (self) {
60 .changed => "changed",
61 .new => "new",
62 .removed => "removed",
63 .stable => "stable",
64 };
65 }
66 };
67
68 const Counts = struct {
69 shared: usize = 0,
70 changed: usize = 0,
71 stable: usize = 0,
72 new: usize = 0,
73 removed: usize = 0,
74
75 fn observe(self: *Counts, status: Status) void {
76 switch (status) {
77 .changed => {
78 self.shared += 1;
79 self.changed += 1;
80 },
81 .stable => {
82 self.shared += 1;
83 self.stable += 1;
84 },
85 .new => self.new += 1,
86 .removed => self.removed += 1,
87 }
88 }
89 };
90
91 const ZoneMetric = enum {
92 total,
93 mean,
94 min,
95 p50,
96 p90,
97 p99,
98 max,
99
100 fn tag(self: ZoneMetric) []const u8 {
101 return @tagName(self);
102 }
103 };
104
105 const ZoneValues = struct {
106 count: u64 = 0,
107 duration_samples: u64 = 0,
108 total_ns: u64 = 0,
109 mean_ns: u64 = 0,
110 min_ns: u64 = 0,
111 p50_ns: u64 = 0,
112 p90_ns: u64 = 0,
113 p99_ns: u64 = 0,
114 max_ns: u64 = 0,
115
116 fn from(summary_value: ?summary.ZoneSummary) ZoneValues {
117 const row = summary_value orelse return .{};
118 return .{
119 .count = row.count,
120 .duration_samples = row.duration_samples,
121 .total_ns = row.total_ns,
122 .mean_ns = row.meanNs(),
123 .min_ns = if (row.duration_samples == 0) 0 else row.min_ns,
124 .p50_ns = row.percentileNs(50),
125 .p90_ns = row.percentileNs(90),
126 .p99_ns = row.percentileNs(99),
127 .max_ns = row.max_ns,
128 };
129 }
130
131 fn value(self: ZoneValues, metric: ZoneMetric) u64 {
132 return switch (metric) {
133 .total => self.total_ns,
134 .mean => self.mean_ns,
135 .min => self.min_ns,
136 .p50 => self.p50_ns,
137 .p90 => self.p90_ns,
138 .p99 => self.p99_ns,
139 .max => self.max_ns,
140 };
141 }
142 };
143
144 const ZoneDelta = struct {
145 name: []const u8,
146 status: Status,
147 baseline: ZoneValues,
148 run: ZoneValues,
149 baseline_evidence: []const u8,
150 run_evidence: []const u8,
151
152 fn evidence(self: ZoneDelta) []const u8 {
153 if (!std.mem.eql(u8, self.baseline_evidence, "complete")) return "partial";
154 if (!std.mem.eql(u8, self.run_evidence, "complete")) return "partial";
155 if (self.baseline.count != self.baseline.duration_samples) return "partial";
156 if (self.run.count != self.run.duration_samples) return "partial";
157 return "complete";
158 }
159
160 fn populationChanged(self: ZoneDelta) bool {
161 if (self.baseline.count != self.run.count) return true;
162 return self.baseline.duration_samples != self.run.duration_samples;
163 }
164
165 fn delta(self: ZoneDelta, metric: ZoneMetric) i128 {
166 return signedDelta(self.baseline.value(metric), self.run.value(metric));
167 }
168
169 fn magnitude(self: ZoneDelta) u128 {
170 var result: u128 = 0;
171 inline for (std.meta.tags(ZoneMetric)) |metric| {
172 result = @max(result, absI128(self.delta(metric)));
173 }
174 return result;
175 }
176 };
177
178 const MemoryValues = struct {
179 allocations: u64 = 0,
180 frees: u64 = 0,
181 unmatched_frees: u64 = 0,
182 duplicate_allocations: u64 = 0,
183 right_censored_allocations: u64 = 0,
184 completed_lifetimes: u64 = 0,
185 lifetime_samples: u64 = 0,
186 timestamp_regressions: u64 = 0,
187 untracked_allocations: u64 = 0,
188 size_mismatches: u64 = 0,
189 allocated_bytes: u64 = 0,
190 freed_bytes: u64 = 0,
191 live_bytes: u64 = 0,
192 high_water_live_bytes: u64 = 0,
193 total_lifetime_ns: u64 = 0,
194 mean_lifetime_ns: u64 = 0,
195 min_lifetime_ns: u64 = 0,
196 p50_lifetime_ns: u64 = 0,
197 p90_lifetime_ns: u64 = 0,
198 p99_lifetime_ns: u64 = 0,
199 max_lifetime_ns: u64 = 0,
200
201 fn from(summary_value: ?memory_mod.Summary) MemoryValues {
202 const value = summary_value orelse return .{};
203 return .{
204 .allocations = value.allocations,
205 .frees = value.frees,
206 .unmatched_frees = value.unmatched_frees,
207 .duplicate_allocations = value.duplicate_allocations,
208 .right_censored_allocations = value.active_allocations,
209 .completed_lifetimes = value.completed_lifetimes,
210 .lifetime_samples = value.lifetime_samples,
211 .timestamp_regressions = value.timestamp_regressions,
212 .untracked_allocations = value.untracked_allocations,
213 .size_mismatches = value.size_mismatches,
214 .allocated_bytes = value.allocated_bytes,
215 .freed_bytes = value.freed_bytes,
216 .live_bytes = value.live_bytes,
217 .high_water_live_bytes = value.high_water_live_bytes,
218 .total_lifetime_ns = value.total_lifetime_ns,
219 .mean_lifetime_ns = value.meanLifetimeNs(),
220 .min_lifetime_ns = value.min_lifetime_ns,
221 .p50_lifetime_ns = value.p50_lifetime_ns,
222 .p90_lifetime_ns = value.p90_lifetime_ns,
223 .p99_lifetime_ns = value.p99_lifetime_ns,
224 .max_lifetime_ns = value.max_lifetime_ns,
225 };
226 }
227 };
228
229 const MemoryDelta = struct {
230 name: []const u8,
231 status: Status,
232 baseline: MemoryValues,
233 run: MemoryValues,
234 baseline_lifetime_evidence: []const u8,
235 run_lifetime_evidence: []const u8,
236
237 fn evidence(self: MemoryDelta) []const u8 {
238 if (!std.mem.eql(u8, self.baseline_lifetime_evidence, "complete")) return "partial";
239 if (!std.mem.eql(u8, self.run_lifetime_evidence, "complete")) return "partial";
240 return "complete";
241 }
242
243 fn byteMagnitude(self: MemoryDelta) u128 {
244 var result: u128 = 0;
245 inline for (.{
246 signedDelta(self.baseline.high_water_live_bytes, self.run.high_water_live_bytes),
247 signedDelta(self.baseline.live_bytes, self.run.live_bytes),
248 signedDelta(self.baseline.allocated_bytes, self.run.allocated_bytes),
249 signedDelta(self.baseline.freed_bytes, self.run.freed_bytes),
250 }) |delta| result = @max(result, absI128(delta));
251 return result;
252 }
253
254 fn lifetimeMagnitude(self: MemoryDelta) u128 {
255 var result: u128 = 0;
256 inline for (.{
257 signedDelta(self.baseline.total_lifetime_ns, self.run.total_lifetime_ns),
258 signedDelta(self.baseline.mean_lifetime_ns, self.run.mean_lifetime_ns),
259 signedDelta(self.baseline.min_lifetime_ns, self.run.min_lifetime_ns),
260 signedDelta(self.baseline.p50_lifetime_ns, self.run.p50_lifetime_ns),
261 signedDelta(self.baseline.p90_lifetime_ns, self.run.p90_lifetime_ns),
262 signedDelta(self.baseline.p99_lifetime_ns, self.run.p99_lifetime_ns),
263 signedDelta(self.baseline.max_lifetime_ns, self.run.max_lifetime_ns),
264 }) |delta| result = @max(result, absI128(delta));
265 return result;
266 }
267
268 fn populationChanged(self: MemoryDelta) bool {
269 if (self.baseline.allocations != self.run.allocations) return true;
270 if (self.baseline.frees != self.run.frees) return true;
271 if (self.baseline.right_censored_allocations != self.run.right_censored_allocations) {
272 return true;
273 }
274 if (self.baseline.completed_lifetimes != self.run.completed_lifetimes) return true;
275 return self.baseline.lifetime_samples != self.run.lifetime_samples;
276 }
277
278 fn anomaliesChanged(self: MemoryDelta) bool {
279 if (self.baseline.unmatched_frees != self.run.unmatched_frees) return true;
280 if (self.baseline.duplicate_allocations != self.run.duplicate_allocations) return true;
281 if (self.baseline.timestamp_regressions != self.run.timestamp_regressions) return true;
282 if (self.baseline.untracked_allocations != self.run.untracked_allocations) return true;
283 return self.baseline.size_mismatches != self.run.size_mismatches;
284 }
285 };
286
287 const PlotCompatibility = enum {
288 matching,
289 mismatch,
290 unknown,
291 not_applicable,
292
293 fn tag(self: PlotCompatibility) []const u8 {
294 return switch (self) {
295 .matching => "matching",
296 .mismatch => "mismatch",
297 .unknown => "unknown",
298 .not_applicable => "not_applicable",
299 };
300 }
301 };
302
303 const PlotMetric = enum {
304 min,
305 p50,
306 p90,
307 p99,
308 max,
309 mean,
310 last,
311 range,
312
313 fn tag(self: PlotMetric) []const u8 {
314 return @tagName(self);
315 }
316
317 fn value(self: PlotMetric, plot: plot_mod.Summary) f64 {
318 return switch (self) {
319 .min => plot.min,
320 .p50 => plot.p50,
321 .p90 => plot.p90,
322 .p99 => plot.p99,
323 .max => plot.max,
324 .mean => plot.mean,
325 .last => plot.last,
326 .range => plot.range,
327 };
328 }
329 };
330
331 const PlotDelta = struct {
332 name: []const u8,
333 status: Status,
334 partial: bool,
335 compatibility: PlotCompatibility,
336 baseline: ?plot_mod.Summary,
337 run: ?plot_mod.Summary,
338
339 fn evidence(self: PlotDelta) []const u8 {
340 return if (self.partial) "partial" else "complete";
341 }
342
343 fn valuesComparable(self: PlotDelta) bool {
344 if (self.baseline == null or self.run == null) return false;
345 return self.compatibility != .mismatch;
346 }
347
348 fn magnitude(self: PlotDelta) f64 {
349 if (!self.valuesComparable()) {
350 const base_extent = if (self.baseline) |plot|
351 @max(absF64(plot.min), absF64(plot.max))
352 else
353 0;
354 const run_extent = if (self.run) |plot|
355 @max(absF64(plot.min), absF64(plot.max))
356 else
357 0;
358 return @max(base_extent, run_extent);
359 }
360 var result: f64 = 0;
361 inline for (std.meta.tags(PlotMetric)) |metric| {
362 result = @max(result, absF64(self.delta(metric).?));
363 }
364 return result;
365 }
366
367 fn delta(self: PlotDelta, metric: PlotMetric) ?f64 {
368 if (!self.valuesComparable()) return null;
369 return metric.value(self.run.?) - metric.value(self.baseline.?);
370 }
371 };
372
373 const GpuDelta = struct {
374 name: []const u8,
375 status: Status,
376 base_count: u64 = 0,
377 run_count: u64 = 0,
378 base_completed_zones: u64 = 0,
379 run_completed_zones: u64 = 0,
380 base_total_gpu_ns: u64 = 0,
381 run_total_gpu_ns: u64 = 0,
382 base_mean_gpu_ns: u64 = 0,
383 run_mean_gpu_ns: u64 = 0,
384
385 fn deltaTotalGpuNs(self: GpuDelta) i128 {
386 return signedDelta(self.base_total_gpu_ns, self.run_total_gpu_ns);
387 }
388
389 fn deltaMeanGpuNs(self: GpuDelta) i128 {
390 return signedDelta(self.base_mean_gpu_ns, self.run_mean_gpu_ns);
391 }
392
393 fn magnitude(self: GpuDelta) u128 {
394 return @max(
395 absI128(self.deltaTotalGpuNs()),
396 absI128(self.deltaMeanGpuNs()),
397 );
398 }
399
400 fn evidence(self: GpuDelta) []const u8 {
401 if (self.base_count != self.base_completed_zones) return "partial";
402 if (self.run_count != self.run_completed_zones) return "partial";
403 return "complete";
404 }
405 };
406
407 const FrameDelta = struct {
408 name: []const u8,
409 status: Status,
410 partial: bool,
411 budget_ns: ?u64,
412 base_marks: u64 = 0,
413 run_marks: u64 = 0,
414 base_frames: u64 = 0,
415 run_frames: u64 = 0,
416 base_mean_ns: u64 = 0,
417 run_mean_ns: u64 = 0,
418 base_p50_ns: u64 = 0,
419 run_p50_ns: u64 = 0,
420 base_p90_ns: u64 = 0,
421 run_p90_ns: u64 = 0,
422 base_p99_ns: u64 = 0,
423 run_p99_ns: u64 = 0,
424 base_max_ns: u64 = 0,
425 run_max_ns: u64 = 0,
426 base_discrepancy_ns: u64 = 0,
427 run_discrepancy_ns: u64 = 0,
428 base_over_budget_frames: u64 = 0,
429 run_over_budget_frames: u64 = 0,
430 base_over_budget_per_mille: u64 = 0,
431 run_over_budget_per_mille: u64 = 0,
432 base_missed_intervals: u64 = 0,
433 run_missed_intervals: u64 = 0,
434 base_missed_per_1000_frames: u64 = 0,
435 run_missed_per_1000_frames: u64 = 0,
436 base_total_overrun_ns: u64 = 0,
437 run_total_overrun_ns: u64 = 0,
438 base_max_overrun_ns: u64 = 0,
439 run_max_overrun_ns: u64 = 0,
440 base_longest_overrun_streak: u64 = 0,
441 run_longest_overrun_streak: u64 = 0,
442
443 fn evidence(self: FrameDelta) []const u8 {
444 return if (self.partial) "partial" else "complete";
445 }
446
447 fn magnitude(self: FrameDelta) u128 {
448 var result: u128 = 0;
449 inline for (.{
450 signedDelta(self.base_mean_ns, self.run_mean_ns),
451 signedDelta(self.base_p50_ns, self.run_p50_ns),
452 signedDelta(self.base_p90_ns, self.run_p90_ns),
453 signedDelta(self.base_p99_ns, self.run_p99_ns),
454 signedDelta(self.base_max_ns, self.run_max_ns),
455 signedDelta(self.base_discrepancy_ns, self.run_discrepancy_ns),
456 }) |delta| result = @max(result, absI128(delta));
457 return result;
458 }
459
460 fn populationChanged(self: FrameDelta) bool {
461 return self.base_marks != self.run_marks or self.base_frames != self.run_frames;
462 }
463
464 fn budgetChanged(self: FrameDelta) bool {
465 if (self.budget_ns == null) return false;
466 if (self.base_over_budget_frames != self.run_over_budget_frames) return true;
467 if (self.base_over_budget_per_mille != self.run_over_budget_per_mille) return true;
468 if (self.base_missed_intervals != self.run_missed_intervals) return true;
469 if (self.base_missed_per_1000_frames != self.run_missed_per_1000_frames) return true;
470 if (self.base_total_overrun_ns != self.run_total_overrun_ns) return true;
471 if (self.base_max_overrun_ns != self.run_max_overrun_ns) return true;
472 return self.base_longest_overrun_streak != self.run_longest_overrun_streak;
473 }
474 };
475
476 const FrameEvidence = struct {
477 baseline: std.ArrayListUnmanaged(frame_mod.Summary) = .empty,
478 run: std.ArrayListUnmanaged(frame_mod.Summary) = .empty,
479 deltas: std.ArrayListUnmanaged(FrameDelta) = .empty,
480
481 fn init(
482 allocator: std.mem.Allocator,
483 baseline: *frame_mod.Analyzer,
484 run: *frame_mod.Analyzer,
485 options: Options,
486 ) !FrameEvidence {
487 if (options.frame_budget_ns == 0) return error.InvalidBudget;
488 var result: FrameEvidence = .{};
489 errdefer result.deinit(allocator);
490 const frame_options = frame_mod.Options{
491 .top = options.top,
492 .sort = .name,
493 .budget_ns = options.frame_budget_ns,
494 };
495 result.baseline = try frame_mod.collectSummaries(allocator, baseline, frame_options);
496 result.run = try frame_mod.collectSummaries(allocator, run, frame_options);
497 const partial = frameCapturePartial(baseline) or frameCapturePartial(run);
498 result.deltas = try collectFrameDeltas(
499 allocator,
500 result.baseline.items,
501 result.run.items,
502 partial,
503 options,
504 );
505 return result;
506 }
507
508 fn deinit(self: *FrameEvidence, allocator: std.mem.Allocator) void {
509 self.deltas.deinit(allocator);
510 self.run.deinit(allocator);
511 self.baseline.deinit(allocator);
512 self.* = undefined;
513 }
514 };
515
516 const PlotEvidence = struct {
517 baseline: std.ArrayListUnmanaged(plot_mod.Summary) = .empty,
518 run: std.ArrayListUnmanaged(plot_mod.Summary) = .empty,
519 deltas: std.ArrayListUnmanaged(PlotDelta) = .empty,
520
521 fn init(
522 allocator: std.mem.Allocator,
523 baseline: *summary.Analyzer,
524 run: *summary.Analyzer,
525 options: Options,
526 ) !PlotEvidence {
527 var result: PlotEvidence = .{};
528 errdefer result.deinit(allocator);
529 result.baseline = try baseline.plots.collectSummaries(allocator, .{ .sort = .name });
530 result.run = try run.plots.collectSummaries(allocator, .{ .sort = .name });
531 const partial = plotCapturePartial(baseline) or plotCapturePartial(run);
532 result.deltas = try collectPlotDeltas(
533 allocator,
534 result.baseline.items,
535 result.run.items,
536 partial,
537 options,
538 );
539 return result;
540 }
541
542 fn deinit(self: *PlotEvidence, allocator: std.mem.Allocator) void {
543 self.deltas.deinit(allocator);
544 self.run.deinit(allocator);
545 self.baseline.deinit(allocator);
546 self.* = undefined;
547 }
548 };
549
550 const MemoryEvidence = struct {
551 baseline: std.ArrayListUnmanaged(memory_mod.Summary) = .empty,
552 run: std.ArrayListUnmanaged(memory_mod.Summary) = .empty,
553 deltas: std.ArrayListUnmanaged(MemoryDelta) = .empty,
554 baseline_allocator: ?std.mem.Allocator = null,
555 run_allocator: ?std.mem.Allocator = null,
556
557 fn init(
558 allocator: std.mem.Allocator,
559 baseline: *summary.Analyzer,
560 run: *summary.Analyzer,
561 options: Options,
562 ) !MemoryEvidence {
563 var result: MemoryEvidence = .{};
564 errdefer result.deinit(allocator);
565 result.baseline_allocator = baseline.allocator;
566 result.baseline = try baseline.collectMemory();
567 result.run_allocator = run.allocator;
568 result.run = try run.collectMemory();
569 result.deltas = try collectMemoryDeltas(
570 allocator,
571 result.baseline.items,
572 result.run.items,
573 baseline.memory.lifetimeEvidence(),
574 run.memory.lifetimeEvidence(),
575 options,
576 );
577 return result;
578 }
579
580 fn deinit(self: *MemoryEvidence, allocator: std.mem.Allocator) void {
581 self.deltas.deinit(allocator);
582 if (self.run_allocator) |actual| self.run.deinit(actual);
583 if (self.baseline_allocator) |actual| self.baseline.deinit(actual);
584 self.* = undefined;
585 }
586 };
587
588 const GpuEvidence = struct {
589 baseline: std.ArrayListUnmanaged(gpu_mod.Summary) = .empty,
590 run: std.ArrayListUnmanaged(gpu_mod.Summary) = .empty,
591 deltas: std.ArrayListUnmanaged(GpuDelta) = .empty,
592 baseline_allocator: ?std.mem.Allocator = null,
593 run_allocator: ?std.mem.Allocator = null,
594
595 fn init(
596 allocator: std.mem.Allocator,
597 baseline: *summary.Analyzer,
598 run: *summary.Analyzer,
599 options: Options,
600 ) !GpuEvidence {
601 var result: GpuEvidence = .{};
602 errdefer result.deinit(allocator);
603 result.baseline_allocator = baseline.allocator;
604 result.baseline = try baseline.collectGpu();
605 result.run_allocator = run.allocator;
606 result.run = try run.collectGpu();
607 result.deltas = try collectGpuDeltas(
608 allocator,
609 result.baseline.items,
610 result.run.items,
611 options,
612 );
613 return result;
614 }
615
616 fn deinit(self: *GpuEvidence, allocator: std.mem.Allocator) void {
617 self.deltas.deinit(allocator);
618 if (self.run_allocator) |actual| gpu_mod.deinitSummaries(actual, &self.run);
619 if (self.baseline_allocator) |actual| gpu_mod.deinitSummaries(actual, &self.baseline);
620 self.* = undefined;
621 }
622 };
623
624 const Evidence = struct {
625 frames: FrameEvidence = .{},
626 plots: PlotEvidence = .{},
627 zones: std.ArrayListUnmanaged(ZoneDelta) = .empty,
628 memory: MemoryEvidence = .{},
629 gpu: GpuEvidence = .{},
630
631 fn init(
632 allocator: std.mem.Allocator,
633 baseline: *Analyzer,
634 run: *Analyzer,
635 options: Options,
636 ) !Evidence {
637 var result: Evidence = .{};
638 errdefer result.deinit(allocator);
639 result.frames = try FrameEvidence.init(
640 allocator,
641 &baseline.frames,
642 &run.frames,
643 options,
644 );
645 result.plots = try PlotEvidence.init(
646 allocator,
647 &baseline.summary,
648 &run.summary,
649 options,
650 );
651 result.zones = try collectZoneDeltas(
652 allocator,
653 &baseline.summary,
654 &run.summary,
655 options,
656 );
657 result.memory = try MemoryEvidence.init(
658 allocator,
659 &baseline.summary,
660 &run.summary,
661 options,
662 );
663 result.gpu = try GpuEvidence.init(
664 allocator,
665 &baseline.summary,
666 &run.summary,
667 options,
668 );
669 return result;
670 }
671
672 fn deinit(self: *Evidence, allocator: std.mem.Allocator) void {
673 self.gpu.deinit(allocator);
674 self.memory.deinit(allocator);
675 self.zones.deinit(allocator);
676 self.plots.deinit(allocator);
677 self.frames.deinit(allocator);
678 self.* = undefined;
679 }
680 };
681
682 pub fn writeTextFromJsonlPaths(
683 allocator: std.mem.Allocator,
684 baseline_path: []const u8,
685 run_path: []const u8,
686 writer: *std.Io.Writer,
687 options: Options,
688 ) !void {
689 var baseline = Analyzer.init(allocator);
690 defer baseline.deinit();
691 try ingestPath(&baseline, baseline_path);
692
693 var run = Analyzer.init(allocator);
694 defer run.deinit();
695 try ingestPath(&run, run_path);
696
697 try writeText(allocator, &baseline, &run, writer, options);
698 }
699
700 pub fn writeJsonlFromJsonlPaths(
701 allocator: std.mem.Allocator,
702 baseline_path: []const u8,
703 run_path: []const u8,
704 writer: *std.Io.Writer,
705 options: Options,
706 ) !void {
707 var baseline = Analyzer.init(allocator);
708 defer baseline.deinit();
709 try ingestPath(&baseline, baseline_path);
710
711 var run = Analyzer.init(allocator);
712 defer run.deinit();
713 try ingestPath(&run, run_path);
714
715 try writeJsonl(allocator, &baseline, &run, writer, options);
716 }
717
718 pub fn ingestPath(analyzer: *Analyzer, path: []const u8) !void {
719 return report.ingestJsonlPath(analyzer, path);
720 }
721
722 pub fn writeText(
723 allocator: std.mem.Allocator,
724 baseline: *Analyzer,
725 run: *Analyzer,
726 writer: *std.Io.Writer,
727 options: Options,
728 ) !void {
729 var evidence = try Evidence.init(allocator, baseline, run, options);
730 defer evidence.deinit(allocator);
731 try writeCaptureText(&baseline.summary, &run.summary, writer);
732 try writeFrameText(baseline, run, writer, evidence.frames.deltas.items, options);
733 try writePlotText(
734 &baseline.summary,
735 &run.summary,
736 writer,
737 evidence.plots.deltas.items,
738 options.top,
739 );
740 try writeZoneText(
741 &baseline.summary,
742 &run.summary,
743 writer,
744 evidence.zones.items,
745 options.top,
746 );
747 try writeMemoryText(
748 &baseline.summary,
749 &run.summary,
750 writer,
751 evidence.memory.deltas.items,
752 options.top,
753 );
754 try writeGpuText(
755 &baseline.summary,
756 &run.summary,
757 writer,
758 evidence.gpu.deltas.items,
759 options.top,
760 );
761 }
762
763 fn writeCaptureText(
764 baseline: *summary.Analyzer,
765 run: *summary.Analyzer,
766 writer: *std.Io.Writer,
767 ) !void {
768 const base_integrity = baseline.captureIntegrity();
769 const run_integrity = run.captureIntegrity();
770 try writer.print(
771 "tracy compare capture baseline_integrity={s} run_integrity={s} " ++
772 "baseline_unbalanced_events={d} run_unbalanced_events={d}\n",
773 .{
774 base_integrity.status,
775 run_integrity.status,
776 base_integrity.unbalanced_event_count,
777 run_integrity.unbalanced_event_count,
778 },
779 );
780 }
781
782 fn writeFrameText(
783 baseline: *Analyzer,
784 run: *Analyzer,
785 writer: *std.Io.Writer,
786 rows: []const FrameDelta,
787 options: Options,
788 ) !void {
789 const counts = countStatuses(rows);
790 try writer.print(
791 "tracy compare frames shared={d} changed={d} stable={d} new={d} removed={d} " ++
792 "partial={d} baseline_marks={d} run_marks={d} baseline_frames={d} " ++
793 "run_frames={d} baseline_out_of_order_marks={d} run_out_of_order_marks={d}",
794 .{
795 counts.shared,
796 counts.changed,
797 counts.stable,
798 counts.new,
799 counts.removed,
800 framePartialCount(rows),
801 baseline.frames.counters.marks,
802 run.frames.counters.marks,
803 baseline.frames.counters.frames,
804 run.frames.counters.frames,
805 baseline.frames.counters.out_of_order_marks,
806 run.frames.counters.out_of_order_marks,
807 },
808 );
809 try writer.writeAll(" budget_ns=");
810 try writeOptionalU64Text(writer, options.frame_budget_ns);
811 try writer.writeByte('\n');
812 for (rows[0..@min(options.top, rows.len)]) |row| {
813 try writeFrameTextRow(writer, row);
814 if (row.budget_ns != null) try writeFrameBudgetTextRow(writer, row);
815 }
816 }
817
818 fn writeFrameTextRow(writer: *std.Io.Writer, row: FrameDelta) !void {
819 try writer.print("frame status={s} evidence={s} name=", .{
820 row.status.tag(),
821 row.evidence(),
822 });
823 try pretty_json.writeString(writer, row.name);
824 try writer.print(
825 " base_marks={d} run_marks={d} delta_marks={d} base_frames={d} " ++
826 "run_frames={d} delta_frames={d} base_mean_ns={d} run_mean_ns={d} " ++
827 "delta_mean_ns={d} base_p50_ns={d} run_p50_ns={d} delta_p50_ns={d} " ++
828 "base_p90_ns={d} run_p90_ns={d} delta_p90_ns={d} base_p99_ns={d} " ++
829 "run_p99_ns={d} delta_p99_ns={d} base_max_ns={d} run_max_ns={d} " ++
830 "delta_max_ns={d} base_discrepancy_ns={d} run_discrepancy_ns={d} " ++
831 "delta_discrepancy_ns={d}\n",
832 .{
833 row.base_marks,
834 row.run_marks,
835 signedDelta(row.base_marks, row.run_marks),
836 row.base_frames,
837 row.run_frames,
838 signedDelta(row.base_frames, row.run_frames),
839 row.base_mean_ns,
840 row.run_mean_ns,
841 signedDelta(row.base_mean_ns, row.run_mean_ns),
842 row.base_p50_ns,
843 row.run_p50_ns,
844 signedDelta(row.base_p50_ns, row.run_p50_ns),
845 row.base_p90_ns,
846 row.run_p90_ns,
847 signedDelta(row.base_p90_ns, row.run_p90_ns),
848 row.base_p99_ns,
849 row.run_p99_ns,
850 signedDelta(row.base_p99_ns, row.run_p99_ns),
851 row.base_max_ns,
852 row.run_max_ns,
853 signedDelta(row.base_max_ns, row.run_max_ns),
854 row.base_discrepancy_ns,
855 row.run_discrepancy_ns,
856 signedDelta(row.base_discrepancy_ns, row.run_discrepancy_ns),
857 },
858 );
859 }
860
861 fn writeFrameBudgetTextRow(writer: *std.Io.Writer, row: FrameDelta) !void {
862 try writer.print("frame-budget status={s} evidence={s} name=", .{
863 row.status.tag(),
864 row.evidence(),
865 });
866 try pretty_json.writeString(writer, row.name);
867 try writer.print(
868 " budget_ns={d} base_over_budget_frames={d} run_over_budget_frames={d} " ++
869 "delta_over_budget_frames={d} base_over_budget_per_mille={d} " ++
870 "run_over_budget_per_mille={d} delta_over_budget_per_mille={d} " ++
871 "base_missed_intervals={d} run_missed_intervals={d} " ++
872 "delta_missed_intervals={d} base_missed_per_1000_frames={d} " ++
873 "run_missed_per_1000_frames={d} delta_missed_per_1000_frames={d} " ++
874 "base_total_overrun_ns={d} run_total_overrun_ns={d} " ++
875 "delta_total_overrun_ns={d} base_max_overrun_ns={d} " ++
876 "run_max_overrun_ns={d} delta_max_overrun_ns={d} " ++
877 "base_longest_overrun_streak={d} run_longest_overrun_streak={d} " ++
878 "delta_longest_overrun_streak={d}\n",
879 .{
880 row.budget_ns.?,
881 row.base_over_budget_frames,
882 row.run_over_budget_frames,
883 signedDelta(row.base_over_budget_frames, row.run_over_budget_frames),
884 row.base_over_budget_per_mille,
885 row.run_over_budget_per_mille,
886 signedDelta(row.base_over_budget_per_mille, row.run_over_budget_per_mille),
887 row.base_missed_intervals,
888 row.run_missed_intervals,
889 signedDelta(row.base_missed_intervals, row.run_missed_intervals),
890 row.base_missed_per_1000_frames,
891 row.run_missed_per_1000_frames,
892 signedDelta(row.base_missed_per_1000_frames, row.run_missed_per_1000_frames),
893 row.base_total_overrun_ns,
894 row.run_total_overrun_ns,
895 signedDelta(row.base_total_overrun_ns, row.run_total_overrun_ns),
896 row.base_max_overrun_ns,
897 row.run_max_overrun_ns,
898 signedDelta(row.base_max_overrun_ns, row.run_max_overrun_ns),
899 row.base_longest_overrun_streak,
900 row.run_longest_overrun_streak,
901 signedDelta(
902 row.base_longest_overrun_streak,
903 row.run_longest_overrun_streak,
904 ),
905 },
906 );
907 }
908
909 fn writePlotText(
910 baseline: *summary.Analyzer,
911 run: *summary.Analyzer,
912 writer: *std.Io.Writer,
913 rows: []const PlotDelta,
914 top: usize,
915 ) !void {
916 const counts = countStatuses(rows);
917 try writer.print(
918 "tracy compare plots shared={d} changed={d} stable={d} new={d} removed={d} " ++
919 "partial={d} unit_mismatch={d} unit_unknown={d} " ++
920 "baseline_samples={d} run_samples={d} baseline_configurations={d} " ++
921 "run_configurations={d} baseline_configuration_conflicts={d} " ++
922 "run_configuration_conflicts={d} baseline_ignored_missing_values={d} " ++
923 "run_ignored_missing_values={d} baseline_ignored_nonfinite={d} " ++
924 "run_ignored_nonfinite={d}\n",
925 .{
926 counts.shared,
927 counts.changed,
928 counts.stable,
929 counts.new,
930 counts.removed,
931 plotPartialCount(rows),
932 plotCompatibilityCount(rows, .mismatch),
933 plotCompatibilityCount(rows, .unknown),
934 baseline.plots.counters.samples,
935 run.plots.counters.samples,
936 baseline.plots.counters.configurations,
937 run.plots.counters.configurations,
938 baseline.plots.counters.configuration_conflicts,
939 run.plots.counters.configuration_conflicts,
940 baseline.plots.counters.ignored_missing_values,
941 run.plots.counters.ignored_missing_values,
942 baseline.plots.counters.ignored_nonfinite,
943 run.plots.counters.ignored_nonfinite,
944 },
945 );
946 for (rows[0..@min(top, rows.len)]) |row| try writePlotTextRow(writer, row);
947 }
948
949 fn writePlotTextRow(writer: *std.Io.Writer, row: PlotDelta) !void {
950 const baseline = plotOrEmpty(row.baseline, row.name);
951 const run = plotOrEmpty(row.run, row.name);
952 try writer.print(
953 "plot status={s} evidence={s} unit_compatibility={s} name=",
954 .{ row.status.tag(), row.evidence(), row.compatibility.tag() },
955 );
956 try pretty_json.writeString(writer, row.name);
957 try writePlotDefinitionText(writer, row);
958 try writer.print(
959 " base_samples={d} run_samples={d} delta_samples={d} " ++
960 "base_threads={d} run_threads={d} delta_threads={d} " ++
961 "base_duration_ns={d} run_duration_ns={d} delta_duration_ns={d}",
962 .{
963 baseline.count,
964 run.count,
965 signedDelta(baseline.count, run.count),
966 baseline.threads,
967 run.threads,
968 signedDelta(baseline.threads, run.threads),
969 baseline.duration_ns,
970 run.duration_ns,
971 signedDelta(baseline.duration_ns, run.duration_ns),
972 },
973 );
974 try writer.writeByte('\n');
975 try writePlotStyleTextRow(writer, row);
976 try writePlotDistributionTextRow(writer, row);
977 }
978
979 fn writePlotDefinitionText(writer: *std.Io.Writer, row: PlotDelta) !void {
980 const baseline = plotOrEmpty(row.baseline, row.name);
981 const run = plotOrEmpty(row.run, row.name);
982 try writer.writeAll(" base_kind=");
983 try writeOptionalStringText(writer, baseline.kind);
984 try writer.writeAll(" run_kind=");
985 try writeOptionalStringText(writer, run.kind);
986 try writer.writeAll(" base_unit=");
987 try writeOptionalStringText(writer, baseline.unit);
988 try writer.writeAll(" run_unit=");
989 try writeOptionalStringText(writer, run.unit);
990 }
991
992 fn writePlotStyleTextRow(writer: *std.Io.Writer, row: PlotDelta) !void {
993 const baseline = plotOrEmpty(row.baseline, row.name);
994 const run = plotOrEmpty(row.run, row.name);
995 try writer.print("plot-style status={s} evidence={s} name=", .{
996 row.status.tag(),
997 row.evidence(),
998 });
999 try pretty_json.writeString(writer, row.name);
1000 try writer.print(
1001 " base_configured={} run_configured={} base_step={} run_step={} " ++
1002 "base_fill={} run_fill={} base_color=",
1003 .{
1004 baseline.configured,
1005 run.configured,
1006 baseline.step,
1007 run.step,
1008 baseline.fill,
1009 run.fill,
1010 },
1011 );
1012 try writeOptionalU32Text(writer, baseline.color);
1013 try writer.writeAll(" run_color=");
1014 try writeOptionalU32Text(writer, run.color);
1015 try writer.print(
1016 " base_configurations={d} run_configurations={d} " ++
1017 "base_configuration_conflicts={d} run_configuration_conflicts={d}",
1018 .{
1019 baseline.configurations,
1020 run.configurations,
1021 baseline.configuration_conflicts,
1022 run.configuration_conflicts,
1023 },
1024 );
1025 try writer.writeByte('\n');
1026 }
1027
1028 fn writePlotDistributionTextRow(writer: *std.Io.Writer, row: PlotDelta) !void {
1029 const baseline = plotOrEmpty(row.baseline, row.name);
1030 const run = plotOrEmpty(row.run, row.name);
1031 try writer.print(
1032 "plot-distribution status={s} evidence={s} unit_compatibility={s} name=",
1033 .{ row.status.tag(), row.evidence(), row.compatibility.tag() },
1034 );
1035 try pretty_json.writeString(writer, row.name);
1036 inline for (std.meta.tags(PlotMetric)) |metric| {
1037 try writePlotMetricText(
1038 writer,
1039 metric.tag(),
1040 metric.value(baseline),
1041 metric.value(run),
1042 row.delta(metric),
1043 );
1044 }
1045 try writer.writeByte('\n');
1046 }
1047
1048 fn writePlotMetricText(
1049 writer: *std.Io.Writer,
1050 label: []const u8,
1051 baseline: f64,
1052 run: f64,
1053 delta: ?f64,
1054 ) !void {
1055 try writer.print(" base_{s}={d} run_{s}={d} delta_{s}=", .{
1056 label,
1057 baseline,
1058 label,
1059 run,
1060 label,
1061 });
1062 try writeOptionalF64Text(writer, delta);
1063 }
1064
1065 fn writeZoneText(
1066 baseline: *summary.Analyzer,
1067 run: *summary.Analyzer,
1068 writer: *std.Io.Writer,
1069 rows: []const ZoneDelta,
1070 top: usize,
1071 ) !void {
1072 const counts = countStatuses(rows);
1073 try writer.print(
1074 "tracy compare zones shared={d} changed={d} stable={d} new={d} removed={d} " ++
1075 "partial={d} baseline_duration_evidence={s} run_duration_evidence={s} " ++
1076 "baseline_duration_samples={d} run_duration_samples={d} " ++
1077 "baseline_timestamp_regressions={d} run_timestamp_regressions={d} " ++
1078 "baseline_duration_ns={d} run_duration_ns={d} delta_duration_ns={d}\n",
1079 .{
1080 counts.shared,
1081 counts.changed,
1082 counts.stable,
1083 counts.new,
1084 counts.removed,
1085 zonePartialCount(rows),
1086 baseline.zoneDurationEvidence(),
1087 run.zoneDurationEvidence(),
1088 baseline.counters.zone_duration_samples,
1089 run.counters.zone_duration_samples,
1090 baseline.counters.zone_timestamp_regressions,
1091 run.counters.zone_timestamp_regressions,
1092 baseline.durationNs(),
1093 run.durationNs(),
1094 signedDelta(baseline.durationNs(), run.durationNs()),
1095 },
1096 );
1097 for (rows[0..@min(top, rows.len)]) |zone| try writeZoneTextRow(writer, zone);
1098 }
1099
1100 fn writeZoneTextRow(writer: *std.Io.Writer, zone: ZoneDelta) !void {
1101 try writer.print("zone status={s} evidence={s} name=", .{
1102 zone.status.tag(),
1103 zone.evidence(),
1104 });
1105 try pretty_json.writeString(writer, zone.name);
1106 try writer.print(
1107 " base_count={d} run_count={d} " ++
1108 "base_duration_samples={d} run_duration_samples={d}",
1109 .{
1110 zone.baseline.count,
1111 zone.run.count,
1112 zone.baseline.duration_samples,
1113 zone.run.duration_samples,
1114 },
1115 );
1116 inline for (std.meta.tags(ZoneMetric)) |metric| {
1117 try writeZoneMetricText(writer, metric, zone);
1118 }
1119 try writer.writeByte('\n');
1120 }
1121
1122 fn writeZoneMetricText(
1123 writer: *std.Io.Writer,
1124 metric: ZoneMetric,
1125 zone: ZoneDelta,
1126 ) !void {
1127 try writer.print(" base_{s}_ns={d} run_{s}_ns={d} delta_{s}_ns={d}", .{
1128 metric.tag(),
1129 zone.baseline.value(metric),
1130 metric.tag(),
1131 zone.run.value(metric),
1132 metric.tag(),
1133 zone.delta(metric),
1134 });
1135 }
1136
1137 fn writeMemoryText(
1138 baseline: *summary.Analyzer,
1139 run: *summary.Analyzer,
1140 writer: *std.Io.Writer,
1141 rows: []const MemoryDelta,
1142 top: usize,
1143 ) !void {
1144 const counts = countStatuses(rows);
1145 try writer.print(
1146 "tracy compare memory shared={d} changed={d} stable={d} new={d} removed={d} " ++
1147 "partial={d} baseline_lifetime_evidence={s} run_lifetime_evidence={s}\n",
1148 .{
1149 counts.shared,
1150 counts.changed,
1151 counts.stable,
1152 counts.new,
1153 counts.removed,
1154 memoryPartialCount(rows),
1155 baseline.memory.lifetimeEvidence(),
1156 run.memory.lifetimeEvidence(),
1157 },
1158 );
1159 for (rows[0..@min(top, rows.len)]) |item| try writeMemoryTextRow(writer, item);
1160 }
1161
1162 fn writeMemoryTextRow(writer: *std.Io.Writer, item: MemoryDelta) !void {
1163 try writer.print("memory status={s} name=", .{item.status.tag()});
1164 try pretty_json.writeString(writer, item.name);
1165 try writer.print(" evidence={s}\n", .{item.evidence()});
1166 try writeMemoryBytesText(writer, item);
1167 try writeMemoryLifetimesText(writer, item);
1168 try writeMemoryIntegrityText(writer, item);
1169 }
1170
1171 fn writeMemoryBytesText(writer: *std.Io.Writer, item: MemoryDelta) !void {
1172 try writer.print(
1173 " bytes base_high_water_live_bytes={d} run_high_water_live_bytes={d} " ++
1174 "delta_high_water_live_bytes={d} base_live_bytes={d} run_live_bytes={d} " ++
1175 "delta_live_bytes={d} base_allocated_bytes={d} run_allocated_bytes={d} " ++
1176 "delta_allocated_bytes={d} base_freed_bytes={d} run_freed_bytes={d} " ++
1177 "delta_freed_bytes={d}\n",
1178 .{
1179 item.baseline.high_water_live_bytes,
1180 item.run.high_water_live_bytes,
1181 signedDelta(item.baseline.high_water_live_bytes, item.run.high_water_live_bytes),
1182 item.baseline.live_bytes,
1183 item.run.live_bytes,
1184 signedDelta(item.baseline.live_bytes, item.run.live_bytes),
1185 item.baseline.allocated_bytes,
1186 item.run.allocated_bytes,
1187 signedDelta(item.baseline.allocated_bytes, item.run.allocated_bytes),
1188 item.baseline.freed_bytes,
1189 item.run.freed_bytes,
1190 signedDelta(item.baseline.freed_bytes, item.run.freed_bytes),
1191 },
1192 );
1193 }
1194
1195 fn writeMemoryLifetimesText(writer: *std.Io.Writer, item: MemoryDelta) !void {
1196 try writer.print(
1197 " lifetimes base_completed={d} run_completed={d} base_samples={d} run_samples={d} " ++
1198 "base_total_ns={d} run_total_ns={d} delta_total_ns={d} base_mean_ns={d} " ++
1199 "run_mean_ns={d} delta_mean_ns={d} base_min_ns={d} run_min_ns={d} " ++
1200 "delta_min_ns={d} base_p50_ns={d} run_p50_ns={d} delta_p50_ns={d} " ++
1201 "base_p90_ns={d} run_p90_ns={d} delta_p90_ns={d} base_p99_ns={d} " ++
1202 "run_p99_ns={d} delta_p99_ns={d} base_max_ns={d} run_max_ns={d} " ++
1203 "delta_max_ns={d}\n",
1204 .{
1205 item.baseline.completed_lifetimes,
1206 item.run.completed_lifetimes,
1207 item.baseline.lifetime_samples,
1208 item.run.lifetime_samples,
1209 item.baseline.total_lifetime_ns,
1210 item.run.total_lifetime_ns,
1211 signedDelta(item.baseline.total_lifetime_ns, item.run.total_lifetime_ns),
1212 item.baseline.mean_lifetime_ns,
1213 item.run.mean_lifetime_ns,
1214 signedDelta(item.baseline.mean_lifetime_ns, item.run.mean_lifetime_ns),
1215 item.baseline.min_lifetime_ns,
1216 item.run.min_lifetime_ns,
1217 signedDelta(item.baseline.min_lifetime_ns, item.run.min_lifetime_ns),
1218 item.baseline.p50_lifetime_ns,
1219 item.run.p50_lifetime_ns,
1220 signedDelta(item.baseline.p50_lifetime_ns, item.run.p50_lifetime_ns),
1221 item.baseline.p90_lifetime_ns,
1222 item.run.p90_lifetime_ns,
1223 signedDelta(item.baseline.p90_lifetime_ns, item.run.p90_lifetime_ns),
1224 item.baseline.p99_lifetime_ns,
1225 item.run.p99_lifetime_ns,
1226 signedDelta(item.baseline.p99_lifetime_ns, item.run.p99_lifetime_ns),
1227 item.baseline.max_lifetime_ns,
1228 item.run.max_lifetime_ns,
1229 signedDelta(item.baseline.max_lifetime_ns, item.run.max_lifetime_ns),
1230 },
1231 );
1232 }
1233
1234 fn writeMemoryIntegrityText(writer: *std.Io.Writer, item: MemoryDelta) !void {
1235 try writer.print(
1236 " integrity baseline_evidence={s} run_evidence={s} base_allocations={d} " ++
1237 "run_allocations={d} base_frees={d} run_frees={d} base_right_censored={d} " ++
1238 "run_right_censored={d} base_unmatched_frees={d} run_unmatched_frees={d} " ++
1239 "base_duplicate_allocations={d} run_duplicate_allocations={d} " ++
1240 "base_timestamp_regressions={d} run_timestamp_regressions={d} " ++
1241 "base_untracked_allocations={d} run_untracked_allocations={d} " ++
1242 "base_size_mismatches={d} run_size_mismatches={d}\n",
1243 .{
1244 item.baseline_lifetime_evidence,
1245 item.run_lifetime_evidence,
1246 item.baseline.allocations,
1247 item.run.allocations,
1248 item.baseline.frees,
1249 item.run.frees,
1250 item.baseline.right_censored_allocations,
1251 item.run.right_censored_allocations,
1252 item.baseline.unmatched_frees,
1253 item.run.unmatched_frees,
1254 item.baseline.duplicate_allocations,
1255 item.run.duplicate_allocations,
1256 item.baseline.timestamp_regressions,
1257 item.run.timestamp_regressions,
1258 item.baseline.untracked_allocations,
1259 item.run.untracked_allocations,
1260 item.baseline.size_mismatches,
1261 item.run.size_mismatches,
1262 },
1263 );
1264 }
1265
1266 fn writeGpuText(
1267 baseline: *summary.Analyzer,
1268 run: *summary.Analyzer,
1269 writer: *std.Io.Writer,
1270 rows: []const GpuDelta,
1271 top: usize,
1272 ) !void {
1273 const counts = countStatuses(rows);
1274 try writer.print(
1275 "tracy compare gpu shared={d} changed={d} stable={d} new={d} removed={d} " ++
1276 "partial={d} baseline_incomplete_zones={d} run_incomplete_zones={d} " ++
1277 "baseline_unmatched_ends={d} run_unmatched_ends={d} " ++
1278 "baseline_unmatched_times={d} run_unmatched_times={d}\n",
1279 .{
1280 counts.shared,
1281 counts.changed,
1282 counts.stable,
1283 counts.new,
1284 counts.removed,
1285 gpuPartialCount(rows),
1286 baseline.gpu.incompleteZoneCount(),
1287 run.gpu.incompleteZoneCount(),
1288 baseline.gpu.counters.unmatched_ends,
1289 run.gpu.counters.unmatched_ends,
1290 baseline.gpu.counters.unmatched_times,
1291 run.gpu.counters.unmatched_times,
1292 },
1293 );
1294 for (rows[0..@min(top, rows.len)]) |row| try writeGpuTextRow(writer, row);
1295 }
1296
1297 fn writeGpuTextRow(writer: *std.Io.Writer, row: GpuDelta) !void {
1298 try writer.print("gpu status={s} evidence={s} name=", .{
1299 row.status.tag(),
1300 row.evidence(),
1301 });
1302 try pretty_json.writeString(writer, row.name);
1303 try writer.print(
1304 " base_total_gpu_ns={d} run_total_gpu_ns={d} delta_total_gpu_ns={d} " ++
1305 "base_mean_gpu_ns={d} run_mean_gpu_ns={d} delta_mean_gpu_ns={d} " ++
1306 "base_count={d} run_count={d} base_completed_zones={d} " ++
1307 "run_completed_zones={d}\n",
1308 .{
1309 row.base_total_gpu_ns,
1310 row.run_total_gpu_ns,
1311 row.deltaTotalGpuNs(),
1312 row.base_mean_gpu_ns,
1313 row.run_mean_gpu_ns,
1314 row.deltaMeanGpuNs(),
1315 row.base_count,
1316 row.run_count,
1317 row.base_completed_zones,
1318 row.run_completed_zones,
1319 },
1320 );
1321 }
1322
1323 pub fn writeJsonl(
1324 allocator: std.mem.Allocator,
1325 baseline: *Analyzer,
1326 run: *Analyzer,
1327 writer: *std.Io.Writer,
1328 options: Options,
1329 ) !void {
1330 var evidence = try Evidence.init(allocator, baseline, run, options);
1331 defer evidence.deinit(allocator);
1332 try writeComparisonSummaryJson(baseline, run, writer, evidence, options);
1333 try writeFrameJson(writer, evidence.frames.deltas.items, options.top);
1334 try writePlotJson(writer, evidence.plots.deltas.items, options.top);
1335 try writeZoneJson(writer, evidence.zones.items, options.top);
1336 try writeMemoryJson(writer, evidence.memory.deltas.items, options.top);
1337 try writeGpuJson(writer, evidence.gpu.deltas.items, options.top);
1338 }
1339
1340 fn writeComparisonSummaryJson(
1341 baseline: *Analyzer,
1342 run: *Analyzer,
1343 writer: *std.Io.Writer,
1344 evidence: Evidence,
1345 options: Options,
1346 ) !void {
1347 var stream = pretty_json.Writer.init(writer, .minified);
1348 const object = try stream.object();
1349 try object.field("schema", schema);
1350 try object.field("kind", "summary");
1351 try writeComparisonCountFields(object, evidence);
1352 try writePlotCountFields(object, baseline, run, evidence.plots.deltas.items);
1353 try writeZoneEvidenceFields(object, baseline, run);
1354 try object.field("baseline_duration_ns", baseline.summary.durationNs());
1355 try object.field("run_duration_ns", run.summary.durationNs());
1356 try object.field(
1357 "delta_duration_ns",
1358 signedDelta(baseline.summary.durationNs(), run.summary.durationNs()),
1359 );
1360 try object.field("frame_budget_ns", options.frame_budget_ns);
1361 try writeComparisonIntegrityFields(object, baseline, run);
1362 try object.endLine();
1363 }
1364
1365 fn writeZoneEvidenceFields(
1366 object: pretty_json.Object,
1367 baseline: *Analyzer,
1368 run: *Analyzer,
1369 ) !void {
1370 try object.field(
1371 "baseline_zone_duration_samples",
1372 baseline.summary.counters.zone_duration_samples,
1373 );
1374 try object.field(
1375 "run_zone_duration_samples",
1376 run.summary.counters.zone_duration_samples,
1377 );
1378 try object.field(
1379 "baseline_zone_timestamp_regressions",
1380 baseline.summary.counters.zone_timestamp_regressions,
1381 );
1382 try object.field(
1383 "run_zone_timestamp_regressions",
1384 run.summary.counters.zone_timestamp_regressions,
1385 );
1386 try object.field(
1387 "baseline_zone_duration_evidence",
1388 baseline.summary.zoneDurationEvidence(),
1389 );
1390 try object.field("run_zone_duration_evidence", run.summary.zoneDurationEvidence());
1391 }
1392
1393 fn writeComparisonCountFields(object: pretty_json.Object, evidence: Evidence) !void {
1394 const frame_counts = countStatuses(evidence.frames.deltas.items);
1395 const zone_counts = countStatuses(evidence.zones.items);
1396 const memory_counts = countStatuses(evidence.memory.deltas.items);
1397 const gpu_counts = countStatuses(evidence.gpu.deltas.items);
1398 try object.field("frame_shared", frame_counts.shared);
1399 try object.field("frame_changed", frame_counts.changed);
1400 try object.field("frame_stable", frame_counts.stable);
1401 try object.field("frame_new", frame_counts.new);
1402 try object.field("frame_removed", frame_counts.removed);
1403 try object.field("frame_partial", framePartialCount(evidence.frames.deltas.items));
1404 try object.field("zone_shared", zone_counts.shared);
1405 try object.field("zone_changed", zone_counts.changed);
1406 try object.field("zone_stable", zone_counts.stable);
1407 try object.field("zone_new", zone_counts.new);
1408 try object.field("zone_removed", zone_counts.removed);
1409 try object.field("zone_partial", zonePartialCount(evidence.zones.items));
1410 try object.field("memory_shared", memory_counts.shared);
1411 try object.field("memory_changed", memory_counts.changed);
1412 try object.field("memory_stable", memory_counts.stable);
1413 try object.field("memory_new", memory_counts.new);
1414 try object.field("memory_removed", memory_counts.removed);
1415 try object.field("memory_partial", memoryPartialCount(evidence.memory.deltas.items));
1416 try object.field("gpu_shared", gpu_counts.shared);
1417 try object.field("gpu_changed", gpu_counts.changed);
1418 try object.field("gpu_stable", gpu_counts.stable);
1419 try object.field("gpu_new", gpu_counts.new);
1420 try object.field("gpu_removed", gpu_counts.removed);
1421 try object.field("gpu_partial", gpuPartialCount(evidence.gpu.deltas.items));
1422 }
1423
1424 fn writePlotCountFields(
1425 object: pretty_json.Object,
1426 baseline: *Analyzer,
1427 run: *Analyzer,
1428 rows: []const PlotDelta,
1429 ) !void {
1430 const counts = countStatuses(rows);
1431 try object.field("plot_shared", counts.shared);
1432 try object.field("plot_changed", counts.changed);
1433 try object.field("plot_stable", counts.stable);
1434 try object.field("plot_new", counts.new);
1435 try object.field("plot_removed", counts.removed);
1436 try object.field("plot_partial", plotPartialCount(rows));
1437 try object.field("plot_unit_mismatch", plotCompatibilityCount(rows, .mismatch));
1438 try object.field("plot_unit_unknown", plotCompatibilityCount(rows, .unknown));
1439 try object.field("baseline_plot_samples", baseline.summary.plots.counters.samples);
1440 try object.field("run_plot_samples", run.summary.plots.counters.samples);
1441 try object.field(
1442 "baseline_plot_configurations",
1443 baseline.summary.plots.counters.configurations,
1444 );
1445 try object.field(
1446 "run_plot_configurations",
1447 run.summary.plots.counters.configurations,
1448 );
1449 try object.field(
1450 "baseline_plot_configuration_conflicts",
1451 baseline.summary.plots.counters.configuration_conflicts,
1452 );
1453 try object.field(
1454 "run_plot_configuration_conflicts",
1455 run.summary.plots.counters.configuration_conflicts,
1456 );
1457 try object.field(
1458 "baseline_plot_ignored_missing_values",
1459 baseline.summary.plots.counters.ignored_missing_values,
1460 );
1461 try object.field(
1462 "run_plot_ignored_missing_values",
1463 run.summary.plots.counters.ignored_missing_values,
1464 );
1465 try object.field(
1466 "baseline_plot_ignored_nonfinite",
1467 baseline.summary.plots.counters.ignored_nonfinite,
1468 );
1469 try object.field(
1470 "run_plot_ignored_nonfinite",
1471 run.summary.plots.counters.ignored_nonfinite,
1472 );
1473 }
1474
1475 fn writeComparisonIntegrityFields(
1476 object: pretty_json.Object,
1477 baseline: *Analyzer,
1478 run: *Analyzer,
1479 ) !void {
1480 const base_integrity = baseline.summary.captureIntegrity();
1481 const run_integrity = run.summary.captureIntegrity();
1482 try object.field("baseline_capture_integrity", base_integrity.status);
1483 try object.field("run_capture_integrity", run_integrity.status);
1484 try object.field("baseline_unbalanced_events", base_integrity.unbalanced_event_count);
1485 try object.field("run_unbalanced_events", run_integrity.unbalanced_event_count);
1486 try object.field(
1487 "baseline_frame_out_of_order_marks",
1488 baseline.frames.counters.out_of_order_marks,
1489 );
1490 try object.field(
1491 "run_frame_out_of_order_marks",
1492 run.frames.counters.out_of_order_marks,
1493 );
1494 try object.field(
1495 "baseline_gpu_incomplete_zones",
1496 baseline.summary.gpu.incompleteZoneCount(),
1497 );
1498 try object.field(
1499 "run_gpu_incomplete_zones",
1500 run.summary.gpu.incompleteZoneCount(),
1501 );
1502 try object.field(
1503 "baseline_gpu_unmatched_ends",
1504 baseline.summary.gpu.counters.unmatched_ends,
1505 );
1506 try object.field(
1507 "run_gpu_unmatched_ends",
1508 run.summary.gpu.counters.unmatched_ends,
1509 );
1510 try object.field(
1511 "baseline_gpu_unmatched_times",
1512 baseline.summary.gpu.counters.unmatched_times,
1513 );
1514 try object.field(
1515 "run_gpu_unmatched_times",
1516 run.summary.gpu.counters.unmatched_times,
1517 );
1518 }
1519
1520 fn writeFrameJson(writer: *std.Io.Writer, rows: []const FrameDelta, top: usize) !void {
1521 for (rows[0..@min(top, rows.len)]) |row| {
1522 try writeFrameJsonRow(writer, row);
1523 if (row.budget_ns != null) try writeFrameBudgetJsonRow(writer, row);
1524 }
1525 }
1526
1527 fn writeFrameJsonRow(writer: *std.Io.Writer, row: FrameDelta) !void {
1528 var stream = pretty_json.Writer.init(writer, .minified);
1529 const object = try stream.object();
1530 try writeFrameFields(object, "frame", row);
1531 try object.field("base_marks", row.base_marks);
1532 try object.field("run_marks", row.run_marks);
1533 try object.field("delta_marks", signedDelta(row.base_marks, row.run_marks));
1534 try object.field("base_frames", row.base_frames);
1535 try object.field("run_frames", row.run_frames);
1536 try object.field("delta_frames", signedDelta(row.base_frames, row.run_frames));
1537 try object.field("base_mean_ns", row.base_mean_ns);
1538 try object.field("run_mean_ns", row.run_mean_ns);
1539 try object.field("delta_mean_ns", signedDelta(row.base_mean_ns, row.run_mean_ns));
1540 try object.field("base_p50_ns", row.base_p50_ns);
1541 try object.field("run_p50_ns", row.run_p50_ns);
1542 try object.field("delta_p50_ns", signedDelta(row.base_p50_ns, row.run_p50_ns));
1543 try object.field("base_p90_ns", row.base_p90_ns);
1544 try object.field("run_p90_ns", row.run_p90_ns);
1545 try object.field("delta_p90_ns", signedDelta(row.base_p90_ns, row.run_p90_ns));
1546 try object.field("base_p99_ns", row.base_p99_ns);
1547 try object.field("run_p99_ns", row.run_p99_ns);
1548 try object.field("delta_p99_ns", signedDelta(row.base_p99_ns, row.run_p99_ns));
1549 try object.field("base_max_ns", row.base_max_ns);
1550 try object.field("run_max_ns", row.run_max_ns);
1551 try object.field("delta_max_ns", signedDelta(row.base_max_ns, row.run_max_ns));
1552 try object.field("base_discrepancy_ns", row.base_discrepancy_ns);
1553 try object.field("run_discrepancy_ns", row.run_discrepancy_ns);
1554 try object.field(
1555 "delta_discrepancy_ns",
1556 signedDelta(row.base_discrepancy_ns, row.run_discrepancy_ns),
1557 );
1558 try object.endLine();
1559 }
1560
1561 fn writeFrameBudgetJsonRow(writer: *std.Io.Writer, row: FrameDelta) !void {
1562 var stream = pretty_json.Writer.init(writer, .minified);
1563 const object = try stream.object();
1564 try writeFrameFields(object, "frame_budget", row);
1565 try object.field("budget_ns", row.budget_ns.?);
1566 try object.field("base_over_budget_frames", row.base_over_budget_frames);
1567 try object.field("run_over_budget_frames", row.run_over_budget_frames);
1568 try object.field(
1569 "delta_over_budget_frames",
1570 signedDelta(row.base_over_budget_frames, row.run_over_budget_frames),
1571 );
1572 try object.field("base_over_budget_per_mille", row.base_over_budget_per_mille);
1573 try object.field("run_over_budget_per_mille", row.run_over_budget_per_mille);
1574 try object.field(
1575 "delta_over_budget_per_mille",
1576 signedDelta(row.base_over_budget_per_mille, row.run_over_budget_per_mille),
1577 );
1578 try object.field("base_missed_intervals", row.base_missed_intervals);
1579 try object.field("run_missed_intervals", row.run_missed_intervals);
1580 try object.field(
1581 "delta_missed_intervals",
1582 signedDelta(row.base_missed_intervals, row.run_missed_intervals),
1583 );
1584 try object.field("base_missed_per_1000_frames", row.base_missed_per_1000_frames);
1585 try object.field("run_missed_per_1000_frames", row.run_missed_per_1000_frames);
1586 try object.field(
1587 "delta_missed_per_1000_frames",
1588 signedDelta(row.base_missed_per_1000_frames, row.run_missed_per_1000_frames),
1589 );
1590 try object.field("base_total_overrun_ns", row.base_total_overrun_ns);
1591 try object.field("run_total_overrun_ns", row.run_total_overrun_ns);
1592 try object.field(
1593 "delta_total_overrun_ns",
1594 signedDelta(row.base_total_overrun_ns, row.run_total_overrun_ns),
1595 );
1596 try object.field("base_max_overrun_ns", row.base_max_overrun_ns);
1597 try object.field("run_max_overrun_ns", row.run_max_overrun_ns);
1598 try object.field(
1599 "delta_max_overrun_ns",
1600 signedDelta(row.base_max_overrun_ns, row.run_max_overrun_ns),
1601 );
1602 try object.field("base_longest_overrun_streak", row.base_longest_overrun_streak);
1603 try object.field("run_longest_overrun_streak", row.run_longest_overrun_streak);
1604 try object.field(
1605 "delta_longest_overrun_streak",
1606 signedDelta(row.base_longest_overrun_streak, row.run_longest_overrun_streak),
1607 );
1608 try object.endLine();
1609 }
1610
1611 fn writeFrameFields(object: pretty_json.Object, kind: []const u8, row: FrameDelta) !void {
1612 try object.field("schema", schema);
1613 try object.field("kind", kind);
1614 try object.field("status", row.status.tag());
1615 try object.field("evidence", row.evidence());
1616 try object.field("name", row.name);
1617 }
1618
1619 fn writePlotJson(writer: *std.Io.Writer, rows: []const PlotDelta, top: usize) !void {
1620 for (rows[0..@min(top, rows.len)]) |row| {
1621 const baseline = plotOrEmpty(row.baseline, row.name);
1622 const run = plotOrEmpty(row.run, row.name);
1623 var stream = pretty_json.Writer.init(writer, .minified);
1624 const object = try stream.object();
1625 try object.field("schema", schema);
1626 try object.field("kind", "plot");
1627 try object.field("status", row.status.tag());
1628 try object.field("evidence", row.evidence());
1629 try object.field("unit_compatibility", row.compatibility.tag());
1630 try object.field("name", row.name);
1631 try writePlotDefinitionFields(object, row);
1632 try object.field("base_samples", baseline.count);
1633 try object.field("run_samples", run.count);
1634 try object.field("delta_samples", signedDelta(baseline.count, run.count));
1635 try object.field("base_threads", baseline.threads);
1636 try object.field("run_threads", run.threads);
1637 try object.field("delta_threads", signedDelta(baseline.threads, run.threads));
1638 try object.field("base_duration_ns", baseline.duration_ns);
1639 try object.field("run_duration_ns", run.duration_ns);
1640 try object.field(
1641 "delta_duration_ns",
1642 signedDelta(baseline.duration_ns, run.duration_ns),
1643 );
1644 try writePlotDistributionFields(object, row);
1645 try object.endLine();
1646 }
1647 }
1648
1649 fn writePlotDefinitionFields(object: pretty_json.Object, row: PlotDelta) !void {
1650 const baseline = plotOrEmpty(row.baseline, row.name);
1651 const run = plotOrEmpty(row.run, row.name);
1652 try object.field("base_present", row.baseline != null);
1653 try object.field("run_present", row.run != null);
1654 try object.field("base_kind", baseline.kind);
1655 try object.field("run_kind", run.kind);
1656 try object.field("base_unit", baseline.unit);
1657 try object.field("run_unit", run.unit);
1658 try object.field("base_configured", baseline.configured);
1659 try object.field("run_configured", run.configured);
1660 try object.field("base_step", baseline.step);
1661 try object.field("run_step", run.step);
1662 try object.field("base_fill", baseline.fill);
1663 try object.field("run_fill", run.fill);
1664 try object.field("base_color", baseline.color);
1665 try object.field("run_color", run.color);
1666 try object.field("base_configurations", baseline.configurations);
1667 try object.field("run_configurations", run.configurations);
1668 try object.field("base_configuration_conflicts", baseline.configuration_conflicts);
1669 try object.field("run_configuration_conflicts", run.configuration_conflicts);
1670 }
1671
1672 fn writePlotDistributionFields(object: pretty_json.Object, row: PlotDelta) !void {
1673 const baseline = plotOrEmpty(row.baseline, row.name);
1674 const run = plotOrEmpty(row.run, row.name);
1675 inline for (std.meta.tags(PlotMetric)) |metric| {
1676 try writePlotMetricFields(
1677 object,
1678 metric.tag(),
1679 metric.value(baseline),
1680 metric.value(run),
1681 row.delta(metric),
1682 );
1683 }
1684 }
1685
1686 fn writePlotMetricFields(
1687 object: pretty_json.Object,
1688 label: []const u8,
1689 baseline: f64,
1690 run: f64,
1691 delta: ?f64,
1692 ) !void {
1693 try object.fieldParts(&.{ "base_", label }, baseline);
1694 try object.fieldParts(&.{ "run_", label }, run);
1695 try object.fieldParts(&.{ "delta_", label }, delta);
1696 }
1697
1698 fn writeZoneJson(writer: *std.Io.Writer, rows: []const ZoneDelta, top: usize) !void {
1699 for (rows[0..@min(top, rows.len)]) |zone| {
1700 var stream = pretty_json.Writer.init(writer, .minified);
1701 const object = try stream.object();
1702 try object.field("schema", schema);
1703 try object.field("kind", "zone");
1704 try object.field("status", zone.status.tag());
1705 try object.field("evidence", zone.evidence());
1706 try object.field("name", zone.name);
1707 try object.field("base_count", zone.baseline.count);
1708 try object.field("run_count", zone.run.count);
1709 try object.field("base_duration_samples", zone.baseline.duration_samples);
1710 try object.field("run_duration_samples", zone.run.duration_samples);
1711 inline for (std.meta.tags(ZoneMetric)) |metric| {
1712 try writeZoneMetricFields(object, metric, zone);
1713 }
1714 try object.endLine();
1715 }
1716 }
1717
1718 fn writeZoneMetricFields(
1719 object: pretty_json.Object,
1720 metric: ZoneMetric,
1721 zone: ZoneDelta,
1722 ) !void {
1723 try object.fieldParts(
1724 &.{ "base_", metric.tag(), "_ns" },
1725 zone.baseline.value(metric),
1726 );
1727 try object.fieldParts(
1728 &.{ "run_", metric.tag(), "_ns" },
1729 zone.run.value(metric),
1730 );
1731 try object.fieldParts(&.{ "delta_", metric.tag(), "_ns" }, zone.delta(metric));
1732 }
1733
1734 fn writeDeltaFields(
1735 object: pretty_json.Object,
1736 label: []const u8,
1737 baseline: anytype,
1738 run: anytype,
1739 delta: anytype,
1740 ) !void {
1741 try object.fieldParts(&.{ "base_", label }, baseline);
1742 try object.fieldParts(&.{ "run_", label }, run);
1743 try object.fieldParts(&.{ "delta_", label }, delta);
1744 }
1745
1746 fn writeMemoryJson(writer: *std.Io.Writer, rows: []const MemoryDelta, top: usize) !void {
1747 for (rows[0..@min(top, rows.len)]) |item| try writeMemoryJsonRow(writer, item);
1748 }
1749
1750 fn writeMemoryJsonRow(writer: *std.Io.Writer, item: MemoryDelta) !void {
1751 var stream = pretty_json.Writer.init(writer, .minified);
1752 const object = try stream.object();
1753 try object.field("schema", schema);
1754 try object.field("kind", "memory");
1755 try object.field("status", item.status.tag());
1756 try object.field("evidence", item.evidence());
1757 try object.field("name", item.name);
1758 try writeMemoryByteFields(object, item);
1759 try writeMemoryLifetimeFields(object, item);
1760 try writeMemoryIntegrityFields(object, item);
1761 try object.endLine();
1762 }
1763
1764 fn writeMemoryByteFields(object: pretty_json.Object, item: MemoryDelta) !void {
1765 try writeDeltaFields(
1766 object,
1767 "high_water_live_bytes",
1768 item.baseline.high_water_live_bytes,
1769 item.run.high_water_live_bytes,
1770 signedDelta(item.baseline.high_water_live_bytes, item.run.high_water_live_bytes),
1771 );
1772 try writeDeltaFields(
1773 object,
1774 "live_bytes",
1775 item.baseline.live_bytes,
1776 item.run.live_bytes,
1777 signedDelta(item.baseline.live_bytes, item.run.live_bytes),
1778 );
1779 try writeDeltaFields(
1780 object,
1781 "allocated_bytes",
1782 item.baseline.allocated_bytes,
1783 item.run.allocated_bytes,
1784 signedDelta(item.baseline.allocated_bytes, item.run.allocated_bytes),
1785 );
1786 try writeDeltaFields(
1787 object,
1788 "freed_bytes",
1789 item.baseline.freed_bytes,
1790 item.run.freed_bytes,
1791 signedDelta(item.baseline.freed_bytes, item.run.freed_bytes),
1792 );
1793 }
1794
1795 fn writeMemoryLifetimeFields(object: pretty_json.Object, item: MemoryDelta) !void {
1796 try object.field("base_completed_lifetimes", item.baseline.completed_lifetimes);
1797 try object.field("run_completed_lifetimes", item.run.completed_lifetimes);
1798 try object.field("base_lifetime_samples", item.baseline.lifetime_samples);
1799 try object.field("run_lifetime_samples", item.run.lifetime_samples);
1800 try writeDeltaFields(
1801 object,
1802 "lifetime_total_ns",
1803 item.baseline.total_lifetime_ns,
1804 item.run.total_lifetime_ns,
1805 signedDelta(item.baseline.total_lifetime_ns, item.run.total_lifetime_ns),
1806 );
1807 try writeDeltaFields(
1808 object,
1809 "lifetime_mean_ns",
1810 item.baseline.mean_lifetime_ns,
1811 item.run.mean_lifetime_ns,
1812 signedDelta(item.baseline.mean_lifetime_ns, item.run.mean_lifetime_ns),
1813 );
1814 try writeDeltaFields(
1815 object,
1816 "lifetime_min_ns",
1817 item.baseline.min_lifetime_ns,
1818 item.run.min_lifetime_ns,
1819 signedDelta(item.baseline.min_lifetime_ns, item.run.min_lifetime_ns),
1820 );
1821 try writeDeltaFields(
1822 object,
1823 "lifetime_p50_ns",
1824 item.baseline.p50_lifetime_ns,
1825 item.run.p50_lifetime_ns,
1826 signedDelta(item.baseline.p50_lifetime_ns, item.run.p50_lifetime_ns),
1827 );
1828 try writeDeltaFields(
1829 object,
1830 "lifetime_p90_ns",
1831 item.baseline.p90_lifetime_ns,
1832 item.run.p90_lifetime_ns,
1833 signedDelta(item.baseline.p90_lifetime_ns, item.run.p90_lifetime_ns),
1834 );
1835 try writeDeltaFields(
1836 object,
1837 "lifetime_p99_ns",
1838 item.baseline.p99_lifetime_ns,
1839 item.run.p99_lifetime_ns,
1840 signedDelta(item.baseline.p99_lifetime_ns, item.run.p99_lifetime_ns),
1841 );
1842 try writeDeltaFields(
1843 object,
1844 "lifetime_max_ns",
1845 item.baseline.max_lifetime_ns,
1846 item.run.max_lifetime_ns,
1847 signedDelta(item.baseline.max_lifetime_ns, item.run.max_lifetime_ns),
1848 );
1849 }
1850
1851 fn writeMemoryIntegrityFields(object: pretty_json.Object, item: MemoryDelta) !void {
1852 try object.field("baseline_lifetime_evidence", item.baseline_lifetime_evidence);
1853 try object.field("run_lifetime_evidence", item.run_lifetime_evidence);
1854 try object.field("base_allocations", item.baseline.allocations);
1855 try object.field("run_allocations", item.run.allocations);
1856 try object.field("base_frees", item.baseline.frees);
1857 try object.field("run_frees", item.run.frees);
1858 try object.field(
1859 "base_right_censored_allocations",
1860 item.baseline.right_censored_allocations,
1861 );
1862 try object.field(
1863 "run_right_censored_allocations",
1864 item.run.right_censored_allocations,
1865 );
1866 try object.field("base_unmatched_frees", item.baseline.unmatched_frees);
1867 try object.field("run_unmatched_frees", item.run.unmatched_frees);
1868 try object.field("base_duplicate_allocations", item.baseline.duplicate_allocations);
1869 try object.field("run_duplicate_allocations", item.run.duplicate_allocations);
1870 try object.field("base_timestamp_regressions", item.baseline.timestamp_regressions);
1871 try object.field("run_timestamp_regressions", item.run.timestamp_regressions);
1872 try object.field("base_untracked_allocations", item.baseline.untracked_allocations);
1873 try object.field("run_untracked_allocations", item.run.untracked_allocations);
1874 try object.field("base_size_mismatches", item.baseline.size_mismatches);
1875 try object.field("run_size_mismatches", item.run.size_mismatches);
1876 }
1877
1878 fn writeGpuJson(writer: *std.Io.Writer, rows: []const GpuDelta, top: usize) !void {
1879 for (rows[0..@min(top, rows.len)]) |row| {
1880 var stream = pretty_json.Writer.init(writer, .minified);
1881 const object = try stream.object();
1882 try object.field("schema", schema);
1883 try object.field("kind", "gpu");
1884 try object.field("status", row.status.tag());
1885 try object.field("evidence", row.evidence());
1886 try object.field("name", row.name);
1887 try object.field("base_total_gpu_ns", row.base_total_gpu_ns);
1888 try object.field("run_total_gpu_ns", row.run_total_gpu_ns);
1889 try object.field("delta_total_gpu_ns", row.deltaTotalGpuNs());
1890 try object.field("base_mean_gpu_ns", row.base_mean_gpu_ns);
1891 try object.field("run_mean_gpu_ns", row.run_mean_gpu_ns);
1892 try object.field("delta_mean_gpu_ns", row.deltaMeanGpuNs());
1893 try object.field("base_count", row.base_count);
1894 try object.field("run_count", row.run_count);
1895 try object.field("base_completed_zones", row.base_completed_zones);
1896 try object.field("run_completed_zones", row.run_completed_zones);
1897 try object.endLine();
1898 }
1899 }
1900
1901 fn collectFrameDeltas(
1902 allocator: std.mem.Allocator,
1903 baseline: []frame_mod.Summary,
1904 run: []frame_mod.Summary,
1905 partial: bool,
1906 options: Options,
1907 ) !std.ArrayListUnmanaged(FrameDelta) {
1908 var rows: std.ArrayListUnmanaged(FrameDelta) = .empty;
1909 errdefer rows.deinit(allocator);
1910 std.mem.sort(frame_mod.Summary, baseline, {}, frameSummaryNameLessThan);
1911 std.mem.sort(frame_mod.Summary, run, {}, frameSummaryNameLessThan);
1912 var base_index: usize = 0;
1913 var run_index: usize = 0;
1914 try mergeFrameDeltas(
1915 allocator,
1916 &rows,
1917 baseline,
1918 run,
1919 &base_index,
1920 &run_index,
1921 partial,
1922 options,
1923 );
1924 try appendFrameTails(
1925 allocator,
1926 &rows,
1927 baseline[base_index..],
1928 run[run_index..],
1929 partial,
1930 options,
1931 );
1932 std.mem.sort(FrameDelta, rows.items, {}, frameDeltaGreaterThan);
1933 return rows;
1934 }
1935
1936 fn mergeFrameDeltas(
1937 allocator: std.mem.Allocator,
1938 rows: *std.ArrayListUnmanaged(FrameDelta),
1939 baseline: []const frame_mod.Summary,
1940 run: []const frame_mod.Summary,
1941 base_index: *usize,
1942 run_index: *usize,
1943 partial: bool,
1944 options: Options,
1945 ) !void {
1946 while (base_index.* < baseline.len and run_index.* < run.len) {
1947 const base_row = baseline[base_index.*];
1948 const run_row = run[run_index.*];
1949 if (std.mem.eql(u8, base_row.name, run_row.name)) {
1950 try appendFrameDelta(
1951 allocator,
1952 rows,
1953 base_row.name,
1954 base_row,
1955 run_row,
1956 partial,
1957 options,
1958 );
1959 base_index.* += 1;
1960 run_index.* += 1;
1961 } else if (std.mem.lessThan(u8, base_row.name, run_row.name)) {
1962 try appendFrameDelta(
1963 allocator,
1964 rows,
1965 base_row.name,
1966 base_row,
1967 null,
1968 partial,
1969 options,
1970 );
1971 base_index.* += 1;
1972 } else {
1973 try appendFrameDelta(
1974 allocator,
1975 rows,
1976 run_row.name,
1977 null,
1978 run_row,
1979 partial,
1980 options,
1981 );
1982 run_index.* += 1;
1983 }
1984 }
1985 }
1986
1987 fn appendFrameTails(
1988 allocator: std.mem.Allocator,
1989 rows: *std.ArrayListUnmanaged(FrameDelta),
1990 baseline: []const frame_mod.Summary,
1991 run: []const frame_mod.Summary,
1992 partial: bool,
1993 options: Options,
1994 ) !void {
1995 for (baseline) |row| {
1996 try appendFrameDelta(
1997 allocator,
1998 rows,
1999 row.name,
2000 row,
2001 null,
2002 partial,
2003 options,
2004 );
2005 }
2006 for (run) |row| {
2007 try appendFrameDelta(
2008 allocator,
2009 rows,
2010 row.name,
2011 null,
2012 row,
2013 partial,
2014 options,
2015 );
2016 }
2017 }
2018
2019 fn appendFrameDelta(
2020 allocator: std.mem.Allocator,
2021 rows: *std.ArrayListUnmanaged(FrameDelta),
2022 name: []const u8,
2023 baseline: ?frame_mod.Summary,
2024 run: ?frame_mod.Summary,
2025 partial: bool,
2026 options: Options,
2027 ) !void {
2028 const row = frameDelta(name, baseline, run, partial, options.frame_budget_ns);
2029 if (includeFrame(row, options)) try rows.append(allocator, row);
2030 }
2031
2032 fn frameSummaryNameLessThan(_: void, left: frame_mod.Summary, right: frame_mod.Summary) bool {
2033 return std.mem.lessThan(u8, left.name, right.name);
2034 }
2035
2036 fn collectPlotDeltas(
2037 allocator: std.mem.Allocator,
2038 baseline: []plot_mod.Summary,
2039 run: []plot_mod.Summary,
2040 partial: bool,
2041 options: Options,
2042 ) !std.ArrayListUnmanaged(PlotDelta) {
2043 var rows: std.ArrayListUnmanaged(PlotDelta) = .empty;
2044 errdefer rows.deinit(allocator);
2045 std.mem.sort(plot_mod.Summary, baseline, {}, plotSummaryNameLessThan);
2046 std.mem.sort(plot_mod.Summary, run, {}, plotSummaryNameLessThan);
2047 var base_index: usize = 0;
2048 var run_index: usize = 0;
2049 while (base_index < baseline.len and run_index < run.len) {
2050 const base_row = baseline[base_index];
2051 const run_row = run[run_index];
2052 if (std.mem.eql(u8, base_row.name, run_row.name)) {
2053 try appendPlotDelta(
2054 allocator,
2055 &rows,
2056 base_row.name,
2057 base_row,
2058 run_row,
2059 partial,
2060 options,
2061 );
2062 base_index += 1;
2063 run_index += 1;
2064 } else if (std.mem.lessThan(u8, base_row.name, run_row.name)) {
2065 try appendPlotDelta(allocator, &rows, base_row.name, base_row, null, partial, options);
2066 base_index += 1;
2067 } else {
2068 try appendPlotDelta(allocator, &rows, run_row.name, null, run_row, partial, options);
2069 run_index += 1;
2070 }
2071 }
2072 for (baseline[base_index..]) |row| {
2073 try appendPlotDelta(allocator, &rows, row.name, row, null, partial, options);
2074 }
2075 for (run[run_index..]) |row| {
2076 try appendPlotDelta(allocator, &rows, row.name, null, row, partial, options);
2077 }
2078 std.mem.sort(PlotDelta, rows.items, {}, plotDeltaGreaterThan);
2079 return rows;
2080 }
2081
2082 fn appendPlotDelta(
2083 allocator: std.mem.Allocator,
2084 rows: *std.ArrayListUnmanaged(PlotDelta),
2085 name: []const u8,
2086 baseline: ?plot_mod.Summary,
2087 run: ?plot_mod.Summary,
2088 partial: bool,
2089 options: Options,
2090 ) !void {
2091 const row = plotDelta(name, baseline, run, partial);
2092 if (includePlot(row, options)) try rows.append(allocator, row);
2093 }
2094
2095 fn plotSummaryNameLessThan(_: void, left: plot_mod.Summary, right: plot_mod.Summary) bool {
2096 return std.mem.lessThan(u8, left.name, right.name);
2097 }
2098
2099 fn collectZoneDeltas(
2100 allocator: std.mem.Allocator,
2101 baseline: *summary.Analyzer,
2102 run: *summary.Analyzer,
2103 options: Options,
2104 ) !std.ArrayListUnmanaged(ZoneDelta) {
2105 var baseline_rows = try baseline.collectZones();
2106 defer baseline_rows.deinit(baseline.allocator);
2107 var run_rows = try run.collectZones();
2108 defer run_rows.deinit(run.allocator);
2109 std.mem.sort(summary.ZoneSummary, baseline_rows.items, {}, zoneSummaryNameLessThan);
2110 std.mem.sort(summary.ZoneSummary, run_rows.items, {}, zoneSummaryNameLessThan);
2111 return try correlateZoneDeltas(
2112 allocator,
2113 baseline_rows.items,
2114 run_rows.items,
2115 baseline.zoneDurationEvidence(),
2116 run.zoneDurationEvidence(),
2117 options,
2118 );
2119 }
2120
2121 fn correlateZoneDeltas(
2122 allocator: std.mem.Allocator,
2123 baseline: []const summary.ZoneSummary,
2124 run: []const summary.ZoneSummary,
2125 baseline_evidence: []const u8,
2126 run_evidence: []const u8,
2127 options: Options,
2128 ) !std.ArrayListUnmanaged(ZoneDelta) {
2129 var rows: std.ArrayListUnmanaged(ZoneDelta) = .empty;
2130 errdefer rows.deinit(allocator);
2131 const indices = try appendOverlappingZoneDeltas(
2132 allocator,
2133 &rows,
2134 baseline,
2135 run,
2136 baseline_evidence,
2137 run_evidence,
2138 options,
2139 );
2140 for (baseline[indices.baseline..]) |row| try appendZoneDelta(
2141 allocator,
2142 &rows,
2143 row.name,
2144 row,
2145 null,
2146 baseline_evidence,
2147 run_evidence,
2148 options,
2149 );
2150 for (run[indices.run..]) |row| try appendZoneDelta(
2151 allocator,
2152 &rows,
2153 row.name,
2154 null,
2155 row,
2156 baseline_evidence,
2157 run_evidence,
2158 options,
2159 );
2160 std.mem.sort(ZoneDelta, rows.items, {}, zoneDeltaGreaterThan);
2161 return rows;
2162 }
2163
2164 const ZoneIndices = struct {
2165 baseline: usize,
2166 run: usize,
2167 };
2168
2169 fn appendOverlappingZoneDeltas(
2170 allocator: std.mem.Allocator,
2171 rows: *std.ArrayListUnmanaged(ZoneDelta),
2172 baseline: []const summary.ZoneSummary,
2173 run: []const summary.ZoneSummary,
2174 baseline_evidence: []const u8,
2175 run_evidence: []const u8,
2176 options: Options,
2177 ) !ZoneIndices {
2178 var baseline_index: usize = 0;
2179 var run_index: usize = 0;
2180 while (baseline_index < baseline.len and run_index < run.len) {
2181 const base_row = baseline[baseline_index];
2182 const run_row = run[run_index];
2183 if (std.mem.eql(u8, base_row.name, run_row.name)) {
2184 try appendZoneDelta(
2185 allocator,
2186 rows,
2187 base_row.name,
2188 base_row,
2189 run_row,
2190 baseline_evidence,
2191 run_evidence,
2192 options,
2193 );
2194 baseline_index += 1;
2195 run_index += 1;
2196 } else if (std.mem.lessThan(u8, base_row.name, run_row.name)) {
2197 try appendZoneDelta(
2198 allocator,
2199 rows,
2200 base_row.name,
2201 base_row,
2202 null,
2203 baseline_evidence,
2204 run_evidence,
2205 options,
2206 );
2207 baseline_index += 1;
2208 } else {
2209 try appendZoneDelta(
2210 allocator,
2211 rows,
2212 run_row.name,
2213 null,
2214 run_row,
2215 baseline_evidence,
2216 run_evidence,
2217 options,
2218 );
2219 run_index += 1;
2220 }
2221 }
2222 return .{ .baseline = baseline_index, .run = run_index };
2223 }
2224
2225 fn appendZoneDelta(
2226 allocator: std.mem.Allocator,
2227 rows: *std.ArrayListUnmanaged(ZoneDelta),
2228 name: []const u8,
2229 baseline: ?summary.ZoneSummary,
2230 run: ?summary.ZoneSummary,
2231 baseline_evidence: []const u8,
2232 run_evidence: []const u8,
2233 options: Options,
2234 ) !void {
2235 const row = zoneDelta(name, baseline, run, baseline_evidence, run_evidence);
2236 if (includeZone(row, options)) try rows.append(allocator, row);
2237 }
2238
2239 fn zoneSummaryNameLessThan(
2240 _: void,
2241 left: summary.ZoneSummary,
2242 right: summary.ZoneSummary,
2243 ) bool {
2244 return std.mem.lessThan(u8, left.name, right.name);
2245 }
2246
2247 fn collectMemoryDeltas(
2248 allocator: std.mem.Allocator,
2249 baseline: []memory_mod.Summary,
2250 run: []memory_mod.Summary,
2251 baseline_evidence: []const u8,
2252 run_evidence: []const u8,
2253 options: Options,
2254 ) !std.ArrayListUnmanaged(MemoryDelta) {
2255 var rows: std.ArrayListUnmanaged(MemoryDelta) = .empty;
2256 errdefer rows.deinit(allocator);
2257 std.mem.sort(memory_mod.Summary, baseline, {}, memorySummaryNameLessThan);
2258 std.mem.sort(memory_mod.Summary, run, {}, memorySummaryNameLessThan);
2259 var base_index: usize = 0;
2260 var run_index: usize = 0;
2261 while (base_index < baseline.len and run_index < run.len) {
2262 const base_row = baseline[base_index];
2263 const run_row = run[run_index];
2264 if (std.mem.eql(u8, base_row.name, run_row.name)) {
2265 try appendMemoryDelta(
2266 allocator,
2267 &rows,
2268 base_row.name,
2269 base_row,
2270 run_row,
2271 baseline_evidence,
2272 run_evidence,
2273 options,
2274 );
2275 base_index += 1;
2276 run_index += 1;
2277 } else if (std.mem.lessThan(u8, base_row.name, run_row.name)) {
2278 try appendMemoryDelta(
2279 allocator,
2280 &rows,
2281 base_row.name,
2282 base_row,
2283 null,
2284 baseline_evidence,
2285 run_evidence,
2286 options,
2287 );
2288 base_index += 1;
2289 } else {
2290 try appendMemoryDelta(
2291 allocator,
2292 &rows,
2293 run_row.name,
2294 null,
2295 run_row,
2296 baseline_evidence,
2297 run_evidence,
2298 options,
2299 );
2300 run_index += 1;
2301 }
2302 }
2303 try appendMemoryTails(
2304 allocator,
2305 &rows,
2306 baseline[base_index..],
2307 run[run_index..],
2308 baseline_evidence,
2309 run_evidence,
2310 options,
2311 );
2312 std.mem.sort(MemoryDelta, rows.items, {}, memoryDeltaGreaterThan);
2313 return rows;
2314 }
2315
2316 fn appendMemoryTails(
2317 allocator: std.mem.Allocator,
2318 rows: *std.ArrayListUnmanaged(MemoryDelta),
2319 baseline: []const memory_mod.Summary,
2320 run: []const memory_mod.Summary,
2321 baseline_evidence: []const u8,
2322 run_evidence: []const u8,
2323 options: Options,
2324 ) !void {
2325 for (baseline) |row| {
2326 try appendMemoryDelta(
2327 allocator,
2328 rows,
2329 row.name,
2330 row,
2331 null,
2332 baseline_evidence,
2333 run_evidence,
2334 options,
2335 );
2336 }
2337 for (run) |row| {
2338 try appendMemoryDelta(
2339 allocator,
2340 rows,
2341 row.name,
2342 null,
2343 row,
2344 baseline_evidence,
2345 run_evidence,
2346 options,
2347 );
2348 }
2349 }
2350
2351 fn appendMemoryDelta(
2352 allocator: std.mem.Allocator,
2353 rows: *std.ArrayListUnmanaged(MemoryDelta),
2354 name: []const u8,
2355 baseline: ?memory_mod.Summary,
2356 run: ?memory_mod.Summary,
2357 baseline_evidence: []const u8,
2358 run_evidence: []const u8,
2359 options: Options,
2360 ) !void {
2361 const row = memoryDelta(name, baseline, run, baseline_evidence, run_evidence);
2362 if (includeMemory(row, options)) try rows.append(allocator, row);
2363 }
2364
2365 fn memorySummaryNameLessThan(
2366 _: void,
2367 left: memory_mod.Summary,
2368 right: memory_mod.Summary,
2369 ) bool {
2370 return std.mem.lessThan(u8, left.name, right.name);
2371 }
2372
2373 fn collectGpuDeltas(
2374 allocator: std.mem.Allocator,
2375 baseline: []gpu_mod.Summary,
2376 run: []gpu_mod.Summary,
2377 options: Options,
2378 ) !std.ArrayListUnmanaged(GpuDelta) {
2379 var rows: std.ArrayListUnmanaged(GpuDelta) = .empty;
2380 errdefer rows.deinit(allocator);
2381 std.mem.sort(gpu_mod.Summary, baseline, {}, gpuSummaryNameLessThan);
2382 std.mem.sort(gpu_mod.Summary, run, {}, gpuSummaryNameLessThan);
2383 var base_index: usize = 0;
2384 var run_index: usize = 0;
2385 while (base_index < baseline.len and run_index < run.len) {
2386 const base_row = baseline[base_index];
2387 const run_row = run[run_index];
2388 if (std.mem.eql(u8, base_row.label, run_row.label)) {
2389 try appendGpuDelta(allocator, &rows, base_row.label, base_row, run_row, options);
2390 base_index += 1;
2391 run_index += 1;
2392 } else if (std.mem.lessThan(u8, base_row.label, run_row.label)) {
2393 try appendGpuDelta(allocator, &rows, base_row.label, base_row, null, options);
2394 base_index += 1;
2395 } else {
2396 try appendGpuDelta(allocator, &rows, run_row.label, null, run_row, options);
2397 run_index += 1;
2398 }
2399 }
2400 while (base_index < baseline.len) : (base_index += 1) {
2401 const row = baseline[base_index];
2402 try appendGpuDelta(allocator, &rows, row.label, row, null, options);
2403 }
2404 while (run_index < run.len) : (run_index += 1) {
2405 const row = run[run_index];
2406 try appendGpuDelta(allocator, &rows, row.label, null, row, options);
2407 }
2408 std.mem.sort(GpuDelta, rows.items, {}, gpuDeltaGreaterThan);
2409 return rows;
2410 }
2411
2412 fn appendGpuDelta(
2413 allocator: std.mem.Allocator,
2414 rows: *std.ArrayListUnmanaged(GpuDelta),
2415 name: []const u8,
2416 baseline: ?gpu_mod.Summary,
2417 run: ?gpu_mod.Summary,
2418 options: Options,
2419 ) !void {
2420 const row = gpuDelta(name, baseline, run);
2421 if (includeGpu(row, options)) try rows.append(allocator, row);
2422 }
2423
2424 fn gpuSummaryNameLessThan(_: void, left: gpu_mod.Summary, right: gpu_mod.Summary) bool {
2425 return std.mem.lessThan(u8, left.label, right.label);
2426 }
2427
2428 fn plotDelta(
2429 name: []const u8,
2430 baseline: ?plot_mod.Summary,
2431 run: ?plot_mod.Summary,
2432 capture_partial: bool,
2433 ) PlotDelta {
2434 var result = PlotDelta{
2435 .name = name,
2436 .status = .stable,
2437 .partial = capture_partial or plotConfigCaveat(baseline) or plotConfigCaveat(run),
2438 .compatibility = plotCompatibility(baseline, run),
2439 .baseline = baseline,
2440 .run = run,
2441 };
2442 result.status = plotStatusFor(result);
2443 return result;
2444 }
2445
2446 fn plotCompatibility(
2447 baseline: ?plot_mod.Summary,
2448 run: ?plot_mod.Summary,
2449 ) PlotCompatibility {
2450 const base_row = baseline orelse return .not_applicable;
2451 const run_row = run orelse return .not_applicable;
2452 if (!base_row.configured or !run_row.configured) return .unknown;
2453 const base_unit = base_row.unit orelse return .unknown;
2454 const run_unit = run_row.unit orelse return .unknown;
2455 return if (std.mem.eql(u8, base_unit, run_unit)) .matching else .mismatch;
2456 }
2457
2458 fn plotConfigCaveat(value: ?plot_mod.Summary) bool {
2459 const row = value orelse return false;
2460 if (!row.configured) return true;
2461 return row.configuration_conflicts != 0;
2462 }
2463
2464 fn plotStatusFor(row: PlotDelta) Status {
2465 const baseline = row.baseline orelse return .new;
2466 const run = row.run orelse return .removed;
2467 if (!plotDefinitionEqual(baseline, run)) return .changed;
2468 if (!plotValuesEqual(baseline, run)) return .changed;
2469 return .stable;
2470 }
2471
2472 fn plotDefinitionEqual(baseline: plot_mod.Summary, run: plot_mod.Summary) bool {
2473 if (!optionalStringEqual(baseline.kind, run.kind)) return false;
2474 if (!optionalStringEqual(baseline.unit, run.unit)) return false;
2475 if (baseline.configured != run.configured) return false;
2476 if (baseline.step != run.step) return false;
2477 if (baseline.fill != run.fill) return false;
2478 return baseline.color == run.color;
2479 }
2480
2481 fn plotValuesEqual(baseline: plot_mod.Summary, run: plot_mod.Summary) bool {
2482 if (baseline.count != run.count) return false;
2483 if (baseline.threads != run.threads) return false;
2484 if (baseline.duration_ns != run.duration_ns) return false;
2485 inline for (std.meta.tags(PlotMetric)) |metric| {
2486 if (metric.value(baseline) != metric.value(run)) return false;
2487 }
2488 return true;
2489 }
2490
2491 fn zoneDelta(
2492 name: []const u8,
2493 baseline: ?summary.ZoneSummary,
2494 run: ?summary.ZoneSummary,
2495 baseline_evidence: []const u8,
2496 run_evidence: []const u8,
2497 ) ZoneDelta {
2498 const baseline_values = ZoneValues.from(baseline);
2499 const run_values = ZoneValues.from(run);
2500 return .{
2501 .name = name,
2502 .status = zoneStatusFor(
2503 baseline_values,
2504 run_values,
2505 baseline != null,
2506 run != null,
2507 ),
2508 .baseline = baseline_values,
2509 .run = run_values,
2510 .baseline_evidence = baseline_evidence,
2511 .run_evidence = run_evidence,
2512 };
2513 }
2514
2515 fn zoneStatusFor(
2516 baseline: ZoneValues,
2517 run: ZoneValues,
2518 has_baseline: bool,
2519 has_run: bool,
2520 ) Status {
2521 if (!has_baseline and has_run) return .new;
2522 if (has_baseline and !has_run) return .removed;
2523 if (std.meta.eql(baseline, run)) return .stable;
2524 return .changed;
2525 }
2526
2527 fn memoryDelta(
2528 name: []const u8,
2529 baseline: ?memory_mod.Summary,
2530 run: ?memory_mod.Summary,
2531 baseline_evidence: []const u8,
2532 run_evidence: []const u8,
2533 ) MemoryDelta {
2534 const baseline_values = MemoryValues.from(baseline);
2535 const run_values = MemoryValues.from(run);
2536 return .{
2537 .name = name,
2538 .status = memoryStatusFor(
2539 baseline_values,
2540 run_values,
2541 baseline != null,
2542 run != null,
2543 ),
2544 .baseline = baseline_values,
2545 .run = run_values,
2546 .baseline_lifetime_evidence = baseline_evidence,
2547 .run_lifetime_evidence = run_evidence,
2548 };
2549 }
2550
2551 fn memoryStatusFor(
2552 baseline: MemoryValues,
2553 run: MemoryValues,
2554 has_baseline: bool,
2555 has_run: bool,
2556 ) Status {
2557 if (!has_baseline and has_run) return .new;
2558 if (has_baseline and !has_run) return .removed;
2559 if (std.meta.eql(baseline, run)) return .stable;
2560 return .changed;
2561 }
2562
2563 const FrameSide = enum {
2564 baseline,
2565 run,
2566 };
2567
2568 fn frameDelta(
2569 name: []const u8,
2570 baseline: ?frame_mod.Summary,
2571 run: ?frame_mod.Summary,
2572 partial: bool,
2573 budget_ns: ?u64,
2574 ) FrameDelta {
2575 var result = FrameDelta{
2576 .name = name,
2577 .status = .stable,
2578 .partial = partial,
2579 .budget_ns = budget_ns,
2580 };
2581 if (baseline) |row| applyFrameSummary(&result, row, .baseline);
2582 if (run) |row| applyFrameSummary(&result, row, .run);
2583 result.status = frameStatusFor(baseline != null, run != null, result);
2584 return result;
2585 }
2586
2587 fn applyFrameSummary(result: *FrameDelta, row: frame_mod.Summary, side: FrameSide) void {
2588 const budget = row.budget orelse frame_mod.BudgetSummary{
2589 .budget_ns = result.budget_ns orelse 1,
2590 };
2591 const over_budget_per_mille = perThousand(budget.over_budget_frames, row.frames);
2592 const missed_per_1000 = perThousand(budget.estimated_missed_intervals, row.frames);
2593 switch (side) {
2594 .baseline => {
2595 result.base_marks = row.marks;
2596 result.base_frames = row.frames;
2597 result.base_mean_ns = row.mean_ns;
2598 result.base_p50_ns = row.p50_ns;
2599 result.base_p90_ns = row.p90_ns;
2600 result.base_p99_ns = row.p99_ns;
2601 result.base_max_ns = row.max_ns;
2602 result.base_discrepancy_ns = row.frame_time_discrepancy_ns;
2603 result.base_over_budget_frames = budget.over_budget_frames;
2604 result.base_over_budget_per_mille = over_budget_per_mille;
2605 result.base_missed_intervals = budget.estimated_missed_intervals;
2606 result.base_missed_per_1000_frames = missed_per_1000;
2607 result.base_total_overrun_ns = budget.total_overrun_ns;
2608 result.base_max_overrun_ns = budget.max_overrun_ns;
2609 result.base_longest_overrun_streak = budget.longest_overrun_streak;
2610 },
2611 .run => {
2612 result.run_marks = row.marks;
2613 result.run_frames = row.frames;
2614 result.run_mean_ns = row.mean_ns;
2615 result.run_p50_ns = row.p50_ns;
2616 result.run_p90_ns = row.p90_ns;
2617 result.run_p99_ns = row.p99_ns;
2618 result.run_max_ns = row.max_ns;
2619 result.run_discrepancy_ns = row.frame_time_discrepancy_ns;
2620 result.run_over_budget_frames = budget.over_budget_frames;
2621 result.run_over_budget_per_mille = over_budget_per_mille;
2622 result.run_missed_intervals = budget.estimated_missed_intervals;
2623 result.run_missed_per_1000_frames = missed_per_1000;
2624 result.run_total_overrun_ns = budget.total_overrun_ns;
2625 result.run_max_overrun_ns = budget.max_overrun_ns;
2626 result.run_longest_overrun_streak = budget.longest_overrun_streak;
2627 },
2628 }
2629 }
2630
2631 fn frameStatusFor(has_baseline: bool, has_run: bool, row: FrameDelta) Status {
2632 if (!has_baseline and has_run) return .new;
2633 if (has_baseline and !has_run) return .removed;
2634 if (row.populationChanged()) return .changed;
2635 if (row.magnitude() != 0) return .changed;
2636 if (row.budgetChanged()) return .changed;
2637 return .stable;
2638 }
2639
2640 fn perThousand(numerator: u64, denominator: u64) u64 {
2641 if (denominator == 0) return 0;
2642 const value = (@as(u128, numerator) * 1_000) / denominator;
2643 return @intCast(@min(value, std.math.maxInt(u64)));
2644 }
2645
2646 fn gpuDelta(
2647 name: []const u8,
2648 baseline: ?gpu_mod.Summary,
2649 run: ?gpu_mod.Summary,
2650 ) GpuDelta {
2651 const base_total = if (baseline) |row| row.gpu_ns else 0;
2652 const run_total = if (run) |row| row.gpu_ns else 0;
2653 const base_completed = if (baseline) |row| row.completed_zones else 0;
2654 const run_completed = if (run) |row| row.completed_zones else 0;
2655 const base_mean = meanCompleted(base_total, base_completed);
2656 const run_mean = meanCompleted(run_total, run_completed);
2657 return .{
2658 .name = name,
2659 .status = gpuStatusFor(baseline, run),
2660 .base_count = if (baseline) |row| row.count else 0,
2661 .run_count = if (run) |row| row.count else 0,
2662 .base_completed_zones = base_completed,
2663 .run_completed_zones = run_completed,
2664 .base_total_gpu_ns = base_total,
2665 .run_total_gpu_ns = run_total,
2666 .base_mean_gpu_ns = base_mean,
2667 .run_mean_gpu_ns = run_mean,
2668 };
2669 }
2670
2671 fn gpuStatusFor(baseline: ?gpu_mod.Summary, run: ?gpu_mod.Summary) Status {
2672 const base_row = baseline orelse return .new;
2673 const run_row = run orelse return .removed;
2674 if (base_row.gpu_ns != run_row.gpu_ns) return .changed;
2675 if (base_row.count != run_row.count) return .changed;
2676 if (base_row.completed_zones != run_row.completed_zones) return .changed;
2677 if (meanCompleted(base_row.gpu_ns, base_row.completed_zones) !=
2678 meanCompleted(run_row.gpu_ns, run_row.completed_zones)) return .changed;
2679 return .stable;
2680 }
2681
2682 fn meanCompleted(total: u64, completed: u64) u64 {
2683 if (completed == 0) return 0;
2684 return total / completed;
2685 }
2686
2687 fn includeZone(row: ZoneDelta, options: Options) bool {
2688 if (!std.mem.eql(u8, row.evidence(), "complete")) return true;
2689 if (row.populationChanged()) return true;
2690 if (row.status == .stable) return options.include_stable;
2691 return row.magnitude() >= options.min_zone_delta_ns;
2692 }
2693
2694 fn includeMemory(row: MemoryDelta, options: Options) bool {
2695 if (!std.mem.eql(u8, row.evidence(), "complete")) return true;
2696 if (row.populationChanged() or row.anomaliesChanged()) return true;
2697 if (row.status == .stable) return options.include_stable;
2698 const byte_changed = row.byteMagnitude() != 0 and
2699 row.byteMagnitude() >= options.min_memory_delta_bytes;
2700 const lifetime_changed = row.lifetimeMagnitude() != 0 and
2701 row.lifetimeMagnitude() >= options.min_memory_lifetime_delta_ns;
2702 return byte_changed or lifetime_changed;
2703 }
2704
2705 fn includePlot(row: PlotDelta, options: Options) bool {
2706 if (row.partial) return true;
2707 if (row.compatibility == .mismatch or row.compatibility == .unknown) return true;
2708 return row.status != .stable or options.include_stable;
2709 }
2710
2711 fn includeFrame(row: FrameDelta, options: Options) bool {
2712 if (row.partial) return true;
2713 if (row.status == .stable and !options.include_stable) return false;
2714 if (row.populationChanged()) return true;
2715 if (row.budgetChanged()) return true;
2716 return row.magnitude() >= options.min_frame_delta_ns;
2717 }
2718
2719 fn includeGpu(row: GpuDelta, options: Options) bool {
2720 if (!std.mem.eql(u8, row.evidence(), "complete")) return true;
2721 if (row.status == .stable and !options.include_stable) return false;
2722 return row.magnitude() >= options.min_gpu_delta_ns;
2723 }
2724
2725 fn countStatuses(rows: anytype) Counts {
2726 var counts: Counts = .{};
2727 for (rows) |row| counts.observe(row.status);
2728 return counts;
2729 }
2730
2731 fn gpuPartialCount(rows: []const GpuDelta) u64 {
2732 var count: u64 = 0;
2733 for (rows) |row| {
2734 if (!std.mem.eql(u8, row.evidence(), "complete")) count += 1;
2735 }
2736 return count;
2737 }
2738
2739 fn zonePartialCount(rows: []const ZoneDelta) u64 {
2740 var count: u64 = 0;
2741 for (rows) |row| {
2742 if (!std.mem.eql(u8, row.evidence(), "complete")) count += 1;
2743 }
2744 return count;
2745 }
2746
2747 fn memoryPartialCount(rows: []const MemoryDelta) u64 {
2748 var count: u64 = 0;
2749 for (rows) |row| {
2750 if (!std.mem.eql(u8, row.evidence(), "complete")) count += 1;
2751 }
2752 return count;
2753 }
2754
2755 fn plotPartialCount(rows: []const PlotDelta) u64 {
2756 var count: u64 = 0;
2757 for (rows) |row| count += @intFromBool(row.partial);
2758 return count;
2759 }
2760
2761 fn plotCompatibilityCount(
2762 rows: []const PlotDelta,
2763 compatibility: PlotCompatibility,
2764 ) u64 {
2765 var count: u64 = 0;
2766 for (rows) |row| count += @intFromBool(row.compatibility == compatibility);
2767 return count;
2768 }
2769
2770 fn framePartialCount(rows: []const FrameDelta) u64 {
2771 var count: u64 = 0;
2772 for (rows) |row| count += @intFromBool(row.partial);
2773 return count;
2774 }
2775
2776 fn frameCapturePartial(analyzer: *frame_mod.Analyzer) bool {
2777 if (!std.mem.eql(u8, analyzer.captureIntegrity().status, "complete")) return true;
2778 return analyzer.counters.out_of_order_marks != 0;
2779 }
2780
2781 fn plotCapturePartial(analyzer: *summary.Analyzer) bool {
2782 if (!std.mem.eql(u8, analyzer.plots.captureIntegrity().status, "complete")) return true;
2783 if (analyzer.plots.counters.ignored_missing_values != 0) return true;
2784 return analyzer.plots.counters.ignored_nonfinite != 0;
2785 }
2786
2787 fn zoneDeltaGreaterThan(_: void, left: ZoneDelta, right: ZoneDelta) bool {
2788 const left_partial = !std.mem.eql(u8, left.evidence(), "complete");
2789 const right_partial = !std.mem.eql(u8, right.evidence(), "complete");
2790 if (left_partial != right_partial) return left_partial;
2791 if (left.magnitude() != right.magnitude()) return left.magnitude() > right.magnitude();
2792 if (left.run.p99_ns != right.run.p99_ns) return left.run.p99_ns > right.run.p99_ns;
2793 if (left.run.total_ns != right.run.total_ns) return left.run.total_ns > right.run.total_ns;
2794 return std.mem.lessThan(u8, left.name, right.name);
2795 }
2796
2797 fn memoryDeltaGreaterThan(_: void, left: MemoryDelta, right: MemoryDelta) bool {
2798 const left_partial = !std.mem.eql(u8, left.evidence(), "complete");
2799 const right_partial = !std.mem.eql(u8, right.evidence(), "complete");
2800 if (left_partial != right_partial) return left_partial;
2801 if (left.byteMagnitude() != right.byteMagnitude()) {
2802 return left.byteMagnitude() > right.byteMagnitude();
2803 }
2804 if (left.lifetimeMagnitude() != right.lifetimeMagnitude()) {
2805 return left.lifetimeMagnitude() > right.lifetimeMagnitude();
2806 }
2807 if (left.run.high_water_live_bytes != right.run.high_water_live_bytes) {
2808 return left.run.high_water_live_bytes > right.run.high_water_live_bytes;
2809 }
2810 if (left.run.p99_lifetime_ns != right.run.p99_lifetime_ns) {
2811 return left.run.p99_lifetime_ns > right.run.p99_lifetime_ns;
2812 }
2813 return std.mem.lessThan(u8, left.name, right.name);
2814 }
2815
2816 fn plotDeltaGreaterThan(_: void, left: PlotDelta, right: PlotDelta) bool {
2817 const left_priority = plotPriority(left);
2818 const right_priority = plotPriority(right);
2819 if (left_priority != right_priority) return left_priority > right_priority;
2820 if (left.magnitude() != right.magnitude()) return left.magnitude() > right.magnitude();
2821 const left_samples = if (left.run) |plot| plot.count else 0;
2822 const right_samples = if (right.run) |plot| plot.count else 0;
2823 if (left_samples != right_samples) return left_samples > right_samples;
2824 return std.mem.lessThan(u8, left.name, right.name);
2825 }
2826
2827 fn plotPriority(row: PlotDelta) u8 {
2828 if (row.compatibility == .mismatch) return 3;
2829 if (row.partial) return 2;
2830 if (row.compatibility == .unknown) return 1;
2831 return 0;
2832 }
2833
2834 fn frameDeltaGreaterThan(_: void, left: FrameDelta, right: FrameDelta) bool {
2835 if (left.magnitude() != right.magnitude()) return left.magnitude() > right.magnitude();
2836 if (left.run_p99_ns != right.run_p99_ns) return left.run_p99_ns > right.run_p99_ns;
2837 return std.mem.lessThan(u8, left.name, right.name);
2838 }
2839
2840 fn gpuDeltaGreaterThan(_: void, left: GpuDelta, right: GpuDelta) bool {
2841 if (left.magnitude() != right.magnitude()) return left.magnitude() > right.magnitude();
2842 if (left.run_total_gpu_ns != right.run_total_gpu_ns) {
2843 return left.run_total_gpu_ns > right.run_total_gpu_ns;
2844 }
2845 return std.mem.lessThan(u8, left.name, right.name);
2846 }
2847
2848 fn signedDelta(base: u64, run: u64) i128 {
2849 return @as(i128, @intCast(run)) - @as(i128, @intCast(base));
2850 }
2851
2852 fn absI128(value: i128) u128 {
2853 if (value < 0) return @intCast(-value);
2854 return @intCast(value);
2855 }
2856
2857 fn absF64(value: f64) f64 {
2858 return if (value < 0) -value else value;
2859 }
2860
2861 fn plotOrEmpty(value: ?plot_mod.Summary, name: []const u8) plot_mod.Summary {
2862 return value orelse .{ .name = name };
2863 }
2864
2865 fn optionalStringEqual(left: ?[]const u8, right: ?[]const u8) bool {
2866 if (left) |left_text| {
2867 const right_text = right orelse return false;
2868 return std.mem.eql(u8, left_text, right_text);
2869 }
2870 return right == null;
2871 }
2872
2873 fn writeOptionalStringText(writer: *std.Io.Writer, value: ?[]const u8) !void {
2874 if (value) |actual| {
2875 try pretty_json.writeString(writer, actual);
2876 } else {
2877 try writer.writeAll("none");
2878 }
2879 }
2880
2881 fn writeOptionalU32Text(writer: *std.Io.Writer, value: ?u32) !void {
2882 if (value) |actual| {
2883 try writer.print("{d}", .{actual});
2884 } else {
2885 try writer.writeAll("none");
2886 }
2887 }
2888
2889 fn writeOptionalF64Text(writer: *std.Io.Writer, value: ?f64) !void {
2890 if (value) |actual| {
2891 try writer.print("{d}", .{actual});
2892 } else {
2893 try writer.writeAll("none");
2894 }
2895 }
2896
2897 fn writeOptionalU64Text(writer: *std.Io.Writer, value: ?u64) !void {
2898 if (value) |actual| {
2899 try writer.print("{d}", .{actual});
2900 } else {
2901 try writer.writeAll("none");
2902 }
2903 }
2904
2905 test "compare reports zone and memory deltas" {
2906 var baseline = Analyzer.init(std.testing.allocator);
2907 defer baseline.deinit();
2908 try addZone(&baseline, "phase", 100);
2909 try addMemory(&baseline, "arena", 64);
2910
2911 var run = Analyzer.init(std.testing.allocator);
2912 defer run.deinit();
2913 try addZoneTrace(&run, &.{
2914 .{ .name = "phase", .durations_ns = &.{150} },
2915 .{ .name = "new-phase", .durations_ns = &.{40} },
2916 }, false);
2917 try addMemory(&run, "arena", 128);
2918
2919 var out = std.Io.Writer.Allocating.init(std.testing.allocator);
2920 defer out.deinit();
2921 try writeText(std.testing.allocator, &baseline, &run, &out.writer, .{ .top = 8 });
2922 const text = out.written();
2923 try std.testing.expect(std.mem.indexOf(u8, text, "tracy compare zones") != null);
2924 try std.testing.expect(
2925 std.mem.indexOf(u8, text, "zone status=changed evidence=partial name=\"phase\"") != null,
2926 );
2927 try std.testing.expect(std.mem.indexOf(u8, text, "delta_total_ns=50") != null);
2928 try std.testing.expect(
2929 std.mem.indexOf(u8, text, "zone status=new evidence=partial name=\"new-phase\"") != null,
2930 );
2931 try std.testing.expect(std.mem.indexOf(
2932 u8,
2933 text,
2934 "memory status=changed name=\"arena\"",
2935 ) != null);
2936 try std.testing.expect(std.mem.indexOf(u8, text, "delta_high_water_live_bytes=64") != null);
2937 }
2938
2939 test "compare preserves equal-total zone tails and population changes" {
2940 var baseline = Analyzer.init(std.testing.allocator);
2941 defer baseline.deinit();
2942 try addZoneTrace(&baseline, &.{
2943 .{ .name = "tail", .durations_ns = &.{ 10, 10, 10, 10, 60 } },
2944 .{ .name = "population", .durations_ns = &.{50} },
2945 }, false);
2946 var run = Analyzer.init(std.testing.allocator);
2947 defer run.deinit();
2948 try addZoneTrace(&run, &.{
2949 .{ .name = "tail", .durations_ns = &.{ 1, 1, 1, 47, 50 } },
2950 .{ .name = "population", .durations_ns = &.{ 25, 25 } },
2951 }, false);
2952
2953 var text = std.Io.Writer.Allocating.init(std.testing.allocator);
2954 defer text.deinit();
2955 try writeText(std.testing.allocator, &baseline, &run, &text.writer, .{
2956 .min_zone_delta_ns = 20,
2957 });
2958 try expectContains(text.written(), "zones shared=2 changed=2 stable=0");
2959 try expectContains(text.written(), "partial=0 baseline_duration_evidence=complete");
2960 try expectContains(text.written(), "name=\"tail\" base_count=5 run_count=5");
2961 try expectContains(text.written(), "base_total_ns=100 run_total_ns=100 delta_total_ns=0");
2962 try expectContains(text.written(), "base_p99_ns=10 run_p99_ns=47 delta_p99_ns=37");
2963 try expectContains(text.written(), "name=\"population\" base_count=1 run_count=2");
2964
2965 var json = std.Io.Writer.Allocating.init(std.testing.allocator);
2966 defer json.deinit();
2967 try writeJsonl(std.testing.allocator, &baseline, &run, &json.writer, .{});
2968 try std.testing.expectEqual(@as(usize, 3), try validJsonLineCount(json.written()));
2969 try expectContains(json.written(), "\"zone_partial\":0");
2970 try expectContains(json.written(), "\"base_p99_ns\":10,\"run_p99_ns\":47");
2971 }
2972
2973 test "compare always surfaces partial zone duration evidence" {
2974 var baseline = Analyzer.init(std.testing.allocator);
2975 defer baseline.deinit();
2976 try addZoneTrace(&baseline, &.{
2977 .{ .name = "invalid", .durations_ns = &.{ 10, 10 } },
2978 .{ .name = "stable", .durations_ns = &.{20} },
2979 }, false);
2980 var run = Analyzer.init(std.testing.allocator);
2981 defer run.deinit();
2982 try addZoneTrace(&run, &.{
2983 .{ .name = "invalid", .durations_ns = &.{ 10, 10 }, .invalid_last = true },
2984 .{ .name = "stable", .durations_ns = &.{20} },
2985 }, true);
2986
2987 var text = std.Io.Writer.Allocating.init(std.testing.allocator);
2988 defer text.deinit();
2989 try writeText(std.testing.allocator, &baseline, &run, &text.writer, .{
2990 .min_zone_delta_ns = std.math.maxInt(u64),
2991 });
2992 try expectContains(text.written(), "partial=2 baseline_duration_evidence=complete");
2993 try expectContains(text.written(), "run_duration_evidence=partial");
2994 try expectContains(text.written(), "run_timestamp_regressions=1");
2995 try expectContains(text.written(), "status=stable evidence=partial name=\"stable\"");
2996 try expectContains(text.written(), "name=\"invalid\" base_count=2 run_count=2");
2997 try expectContains(text.written(), "base_duration_samples=2 run_duration_samples=1");
2998 }
2999
3000 test "compare preserves lifetime tails with equal high water" {
3001 var baseline = Analyzer.init(std.testing.allocator);
3002 defer baseline.deinit();
3003 try addMemoryLifetimes(&baseline, "arena", 64, &.{ 10, 20, 30, 40, 50 }, .{});
3004 var run = Analyzer.init(std.testing.allocator);
3005 defer run.deinit();
3006 try addMemoryLifetimes(&run, "arena", 64, &.{ 10, 20, 30, 40, 200 }, .{});
3007
3008 var text = std.Io.Writer.Allocating.init(std.testing.allocator);
3009 defer text.deinit();
3010 try writeText(std.testing.allocator, &baseline, &run, &text.writer, .{
3011 .min_memory_delta_bytes = 1000,
3012 });
3013 try expectContains(text.written(), "memory status=changed name=\"arena\" evidence=complete");
3014 try expectContains(text.written(), "delta_high_water_live_bytes=0");
3015 try expectContains(text.written(), "base_p99_ns=50 run_p99_ns=200 delta_p99_ns=150");
3016 try expectContains(text.written(), "partial=0 baseline_lifetime_evidence=complete");
3017
3018 var json = std.Io.Writer.Allocating.init(std.testing.allocator);
3019 defer json.deinit();
3020 try writeJsonl(std.testing.allocator, &baseline, &run, &json.writer, .{});
3021 try std.testing.expectEqual(@as(usize, 2), try validJsonLineCount(json.written()));
3022 try expectContains(json.written(), "\"memory_partial\":0");
3023 try expectContains(json.written(), "\"base_lifetime_p50_ns\":30");
3024 try expectContains(json.written(), "\"delta_lifetime_p99_ns\":150");
3025
3026 var filtered = std.Io.Writer.Allocating.init(std.testing.allocator);
3027 defer filtered.deinit();
3028 try writeText(std.testing.allocator, &baseline, &run, &filtered.writer, .{
3029 .min_memory_delta_bytes = 1000,
3030 .min_memory_lifetime_delta_ns = 151,
3031 });
3032 try std.testing.expect(std.mem.indexOf(u8, filtered.written(), "memory status=") == null);
3033 }
3034
3035 test "compare retains right censored memory evidence through thresholds" {
3036 var baseline = Analyzer.init(std.testing.allocator);
3037 defer baseline.deinit();
3038 try addMemoryLifetimes(&baseline, "arena", 64, &.{10}, .{});
3039 var run = Analyzer.init(std.testing.allocator);
3040 defer run.deinit();
3041 try addMemoryLifetimes(&run, "arena", 64, &.{10}, .{ .right_censored = true });
3042
3043 var out = std.Io.Writer.Allocating.init(std.testing.allocator);
3044 defer out.deinit();
3045 try writeText(std.testing.allocator, &baseline, &run, &out.writer, .{
3046 .min_memory_delta_bytes = std.math.maxInt(u64),
3047 .min_memory_lifetime_delta_ns = std.math.maxInt(u64),
3048 });
3049 try expectContains(out.written(), "memory status=changed name=\"arena\" evidence=partial");
3050 try expectContains(out.written(), "partial=1 baseline_lifetime_evidence=complete");
3051 try expectContains(out.written(), "run_lifetime_evidence=partial");
3052 try expectContains(out.written(), "base_right_censored=0 run_right_censored=1");
3053 }
3054
3055 test "compare retains memory anomalies through thresholds" {
3056 var baseline = Analyzer.init(std.testing.allocator);
3057 defer baseline.deinit();
3058 try addMemoryLifetimes(&baseline, "arena", 64, &.{10}, .{});
3059 var run = Analyzer.init(std.testing.allocator);
3060 defer run.deinit();
3061 try addMemoryLifetimes(&run, "arena", 64, &.{10}, .{ .size_mismatch = true });
3062
3063 var out = std.Io.Writer.Allocating.init(std.testing.allocator);
3064 defer out.deinit();
3065 try writeText(std.testing.allocator, &baseline, &run, &out.writer, .{
3066 .min_memory_delta_bytes = std.math.maxInt(u64),
3067 .min_memory_lifetime_delta_ns = std.math.maxInt(u64),
3068 });
3069 try expectContains(out.written(), "memory status=changed name=\"arena\" evidence=partial");
3070 try expectContains(out.written(), "delta_high_water_live_bytes=0");
3071 try expectContains(out.written(), "delta_p99_ns=0");
3072 try expectContains(out.written(), "base_size_mismatches=0 run_size_mismatches=1");
3073 }
3074
3075 test "memory correlation preserves new removed and stable pools" {
3076 var baseline = [_]memory_mod.Summary{
3077 .{ .name = "removed", .allocations = 1 },
3078 .{ .name = "stable", .allocated_bytes = 64 },
3079 };
3080 var run = [_]memory_mod.Summary{
3081 .{ .name = "new", .allocations = 1 },
3082 .{ .name = "stable", .allocated_bytes = 64 },
3083 };
3084 var rows = try collectMemoryDeltas(
3085 std.testing.allocator,
3086 &baseline,
3087 &run,
3088 "complete",
3089 "complete",
3090 .{ .include_stable = true },
3091 );
3092 defer rows.deinit(std.testing.allocator);
3093 try std.testing.expectEqual(@as(usize, 3), rows.items.len);
3094 try expectMemoryStatus(rows.items, "new", .new);
3095 try expectMemoryStatus(rows.items, "removed", .removed);
3096 try expectMemoryStatus(rows.items, "stable", .stable);
3097
3098 var hidden = try collectMemoryDeltas(
3099 std.testing.allocator,
3100 &baseline,
3101 &run,
3102 "complete",
3103 "complete",
3104 .{},
3105 );
3106 defer hidden.deinit(std.testing.allocator);
3107 try std.testing.expectEqual(@as(usize, 2), hidden.items.len);
3108 }
3109
3110 test "compare jsonl emits machine-readable rows" {
3111 var baseline = Analyzer.init(std.testing.allocator);
3112 defer baseline.deinit();
3113 try addZone(&baseline, "phase", 100);
3114
3115 var run = Analyzer.init(std.testing.allocator);
3116 defer run.deinit();
3117 try addZone(&run, "phase", 60);
3118
3119 var out = std.Io.Writer.Allocating.init(std.testing.allocator);
3120 defer out.deinit();
3121 try writeJsonl(std.testing.allocator, &baseline, &run, &out.writer, .{ .top = 4 });
3122 const text = out.written();
3123 try std.testing.expect(std.mem.indexOf(u8, text, "\"schema\":\"tracy.compare/v0\"") != null);
3124 try std.testing.expect(std.mem.indexOf(u8, text, "\"kind\":\"zone\"") != null);
3125 try std.testing.expect(std.mem.indexOf(u8, text, "\"delta_total_ns\":-40") != null);
3126 }
3127
3128 test "compare correlates frame sets and preserves distribution tails" {
3129 var baseline = Analyzer.init(std.testing.allocator);
3130 defer baseline.deinit();
3131 try addFrameTrace(&baseline, &.{
3132 .{ .name = "render", .durations_ns = &.{ 16, 16, 16 } },
3133 .{ .name = "removed", .durations_ns = &.{10} },
3134 .{ .name = "steady", .durations_ns = &.{5} },
3135 }, false);
3136 var run = Analyzer.init(std.testing.allocator);
3137 defer run.deinit();
3138 try addFrameTrace(&run, &.{
3139 .{ .name = "render", .durations_ns = &.{ 16, 16, 48 } },
3140 .{ .name = "new", .durations_ns = &.{10} },
3141 .{ .name = "steady", .durations_ns = &.{5} },
3142 }, false);
3143
3144 var out = std.Io.Writer.Allocating.init(std.testing.allocator);
3145 defer out.deinit();
3146 try writeText(std.testing.allocator, &baseline, &run, &out.writer, .{ .top = 8 });
3147 const text = out.written();
3148 try expectContains(
3149 text,
3150 "tracy compare frames shared=1 changed=1 stable=0 new=1 removed=1 partial=0",
3151 );
3152 try expectContains(text, "frame status=changed evidence=complete name=\"render\"");
3153 try expectContains(text, "base_p50_ns=16 run_p50_ns=16 delta_p50_ns=0");
3154 try expectContains(text, "base_p99_ns=16 run_p99_ns=48 delta_p99_ns=32");
3155 try expectContains(
3156 text,
3157 "base_discrepancy_ns=16 run_discrepancy_ns=48 delta_discrepancy_ns=32",
3158 );
3159 try expectContains(text, "frame status=new evidence=complete name=\"new\"");
3160 try expectContains(text, "frame status=removed evidence=complete name=\"removed\"");
3161 try std.testing.expect(std.mem.indexOf(u8, text, "name=\"steady\"") == null);
3162 }
3163
3164 test "compare retains frame budget changes independently of time threshold" {
3165 var baseline = Analyzer.init(std.testing.allocator);
3166 defer baseline.deinit();
3167 try addFrameTrace(
3168 &baseline,
3169 &.{.{ .name = "render", .durations_ns = &.{ 16, 16, 16 } }},
3170 false,
3171 );
3172 var run = Analyzer.init(std.testing.allocator);
3173 defer run.deinit();
3174 try addFrameTrace(
3175 &run,
3176 &.{.{ .name = "render", .durations_ns = &.{ 16, 16, 48 } }},
3177 false,
3178 );
3179
3180 var filtered = std.Io.Writer.Allocating.init(std.testing.allocator);
3181 defer filtered.deinit();
3182 try writeText(std.testing.allocator, &baseline, &run, &filtered.writer, .{
3183 .min_frame_delta_ns = 1_000,
3184 });
3185 try std.testing.expect(std.mem.indexOf(u8, filtered.written(), "frame status=") == null);
3186
3187 var budgeted = std.Io.Writer.Allocating.init(std.testing.allocator);
3188 defer budgeted.deinit();
3189 try writeText(std.testing.allocator, &baseline, &run, &budgeted.writer, .{
3190 .frame_budget_ns = 20,
3191 .min_frame_delta_ns = 1_000,
3192 });
3193 try expectContains(budgeted.written(), "frame status=changed evidence=complete");
3194 try expectContains(budgeted.written(), "delta_over_budget_per_mille=333");
3195 try expectContains(budgeted.written(), "delta_missed_intervals=2");
3196 }
3197
3198 test "compare retains frame population changes independently of time threshold" {
3199 var baseline = Analyzer.init(std.testing.allocator);
3200 defer baseline.deinit();
3201 try addFrameTrace(
3202 &baseline,
3203 &.{.{ .name = "render", .durations_ns = &.{ 16, 16 } }},
3204 false,
3205 );
3206 var run = Analyzer.init(std.testing.allocator);
3207 defer run.deinit();
3208 try addFrameTrace(
3209 &run,
3210 &.{.{ .name = "render", .durations_ns = &.{16} }},
3211 false,
3212 );
3213
3214 var out = std.Io.Writer.Allocating.init(std.testing.allocator);
3215 defer out.deinit();
3216 try writeText(std.testing.allocator, &baseline, &run, &out.writer, .{
3217 .min_frame_delta_ns = 1_000,
3218 });
3219 try expectContains(out.written(), "frame status=changed evidence=complete");
3220 try expectContains(out.written(), "base_frames=2 run_frames=1 delta_frames=-1");
3221 }
3222
3223 test "compare always surfaces partial frame evidence" {
3224 var baseline = Analyzer.init(std.testing.allocator);
3225 defer baseline.deinit();
3226 try addFrameTrace(
3227 &baseline,
3228 &.{.{ .name = "render", .durations_ns = &.{ 16, 16 } }},
3229 false,
3230 );
3231 var run = Analyzer.init(std.testing.allocator);
3232 defer run.deinit();
3233 try addFrameTrace(
3234 &run,
3235 &.{.{ .name = "render", .durations_ns = &.{ 16, 16 } }},
3236 true,
3237 );
3238
3239 var out = std.Io.Writer.Allocating.init(std.testing.allocator);
3240 defer out.deinit();
3241 try writeText(std.testing.allocator, &baseline, &run, &out.writer, .{
3242 .min_frame_delta_ns = 1_000,
3243 });
3244 try expectContains(out.written(), "run_integrity=sequence_gaps");
3245 try expectContains(out.written(), "stable=1 new=0 removed=0 partial=1");
3246 try expectContains(out.written(), "frame status=stable evidence=partial name=\"render\"");
3247 }
3248
3249 test "compare always surfaces out-of-order frame evidence" {
3250 var baseline = Analyzer.init(std.testing.allocator);
3251 defer baseline.deinit();
3252 try addOutOfOrderFrameTrace(&baseline);
3253 var run = Analyzer.init(std.testing.allocator);
3254 defer run.deinit();
3255 try addOutOfOrderFrameTrace(&run);
3256
3257 var out = std.Io.Writer.Allocating.init(std.testing.allocator);
3258 defer out.deinit();
3259 try writeText(std.testing.allocator, &baseline, &run, &out.writer, .{
3260 .min_frame_delta_ns = 1_000,
3261 });
3262 try expectContains(out.written(), "baseline_integrity=complete run_integrity=complete");
3263 try expectContains(
3264 out.written(),
3265 "partial=1 baseline_marks=3 run_marks=3 baseline_frames=1 run_frames=1 " ++
3266 "baseline_out_of_order_marks=1 run_out_of_order_marks=1",
3267 );
3268 try expectContains(out.written(), "frame status=stable evidence=partial name=\"render\"");
3269 }
3270
3271 test "compare frame JSONL retains budget units and valid rows" {
3272 var baseline = Analyzer.init(std.testing.allocator);
3273 defer baseline.deinit();
3274 try addFrameTrace(
3275 &baseline,
3276 &.{.{ .name = "render", .durations_ns = &.{ 16, 16, 16 } }},
3277 false,
3278 );
3279 var run = Analyzer.init(std.testing.allocator);
3280 defer run.deinit();
3281 try addFrameTrace(
3282 &run,
3283 &.{.{ .name = "render", .durations_ns = &.{ 16, 16, 48 } }},
3284 false,
3285 );
3286
3287 var out = std.Io.Writer.Allocating.init(std.testing.allocator);
3288 defer out.deinit();
3289 try writeJsonl(std.testing.allocator, &baseline, &run, &out.writer, .{
3290 .frame_budget_ns = 20,
3291 });
3292 const text = out.written();
3293 try expectContains(text, "\"frame_budget_ns\":20");
3294 try expectContains(text, "\"kind\":\"frame\"");
3295 try expectContains(text, "\"delta_p99_ns\":32");
3296 try expectContains(text, "\"kind\":\"frame_budget\"");
3297 try expectContains(text, "\"delta_missed_per_1000_frames\":666");
3298 try std.testing.expectEqual(3, try validJsonLineCount(text));
3299 }
3300
3301 test "compare rejects a zero frame budget" {
3302 var baseline = Analyzer.init(std.testing.allocator);
3303 defer baseline.deinit();
3304 var run = Analyzer.init(std.testing.allocator);
3305 defer run.deinit();
3306 var out = std.Io.Writer.Allocating.init(std.testing.allocator);
3307 defer out.deinit();
3308 try std.testing.expectError(
3309 error.InvalidBudget,
3310 writeText(std.testing.allocator, &baseline, &run, &out.writer, .{
3311 .frame_budget_ns = 0,
3312 }),
3313 );
3314 }
3315
3316 test "compare correlates named GPU zones and preserves completed means" {
3317 var baseline = Analyzer.init(std.testing.allocator);
3318 defer baseline.deinit();
3319 try addGpuTrace(&baseline, &.{
3320 .{ .name = "draw", .gpu_ns = 30 },
3321 .{ .name = "draw", .gpu_ns = 30 },
3322 .{ .name = "removed", .gpu_ns = 20 },
3323 .{ .name = "steady", .gpu_ns = 10 },
3324 });
3325 var run = Analyzer.init(std.testing.allocator);
3326 defer run.deinit();
3327 try addGpuTrace(&run, &.{
3328 .{ .name = "draw", .gpu_ns = 100 },
3329 .{ .name = "new", .gpu_ns = 30 },
3330 .{ .name = "steady", .gpu_ns = 10 },
3331 });
3332
3333 var out = std.Io.Writer.Allocating.init(std.testing.allocator);
3334 defer out.deinit();
3335 try writeText(std.testing.allocator, &baseline, &run, &out.writer, .{ .top = 8 });
3336 const text = out.written();
3337 try std.testing.expect(std.mem.indexOf(
3338 u8,
3339 text,
3340 "tracy compare capture baseline_integrity=complete run_integrity=complete",
3341 ) != null);
3342 try std.testing.expect(std.mem.indexOf(
3343 u8,
3344 text,
3345 "tracy compare gpu shared=1 changed=1 stable=0 new=1 removed=1 partial=0",
3346 ) != null);
3347 try expectContains(text, "gpu status=changed evidence=complete name=\"draw\"");
3348 try expectContains(
3349 text,
3350 "base_total_gpu_ns=60 run_total_gpu_ns=100 delta_total_gpu_ns=40",
3351 );
3352 try expectContains(
3353 text,
3354 "base_mean_gpu_ns=30 run_mean_gpu_ns=100 delta_mean_gpu_ns=70",
3355 );
3356 try expectContains(
3357 text,
3358 "base_count=2 run_count=1 base_completed_zones=2 run_completed_zones=1",
3359 );
3360 try expectContains(text, "gpu status=new evidence=complete name=\"new\"");
3361 try expectContains(text, "gpu status=removed evidence=complete name=\"removed\"");
3362 try std.testing.expect(std.mem.indexOf(u8, text, "name=\"steady\"") == null);
3363 const changed_index = std.mem.indexOf(u8, text, "gpu status=changed").?;
3364 const new_index = std.mem.indexOf(u8, text, "gpu status=new").?;
3365 const removed_index = std.mem.indexOf(u8, text, "gpu status=removed").?;
3366 try std.testing.expect(changed_index < new_index);
3367 try std.testing.expect(new_index < removed_index);
3368 }
3369
3370 test "compare filters complete GPU deltas independently" {
3371 var baseline = Analyzer.init(std.testing.allocator);
3372 defer baseline.deinit();
3373 try addGpuTrace(&baseline, &.{
3374 .{ .name = "draw", .gpu_ns = 60 },
3375 .{ .name = "steady", .gpu_ns = 10 },
3376 });
3377 var run = Analyzer.init(std.testing.allocator);
3378 defer run.deinit();
3379 try addGpuTrace(&run, &.{
3380 .{ .name = "draw", .gpu_ns = 100 },
3381 .{ .name = "steady", .gpu_ns = 10 },
3382 });
3383
3384 var filtered = std.Io.Writer.Allocating.init(std.testing.allocator);
3385 defer filtered.deinit();
3386 try writeText(std.testing.allocator, &baseline, &run, &filtered.writer, .{
3387 .top = 8,
3388 .min_gpu_delta_ns = 50,
3389 .include_stable = true,
3390 });
3391 try std.testing.expect(std.mem.indexOf(u8, filtered.written(), "gpu status=") == null);
3392
3393 var stable = std.Io.Writer.Allocating.init(std.testing.allocator);
3394 defer stable.deinit();
3395 try writeText(std.testing.allocator, &baseline, &run, &stable.writer, .{
3396 .top = 8,
3397 .include_stable = true,
3398 });
3399 try std.testing.expect(std.mem.indexOf(
3400 u8,
3401 stable.written(),
3402 "gpu status=stable evidence=complete name=\"steady\"",
3403 ) != null);
3404 }
3405
3406 test "compare GPU threshold observes completed-zone mean changes" {
3407 var baseline = Analyzer.init(std.testing.allocator);
3408 defer baseline.deinit();
3409 try addGpuTrace(&baseline, &.{
3410 .{ .name = "draw", .gpu_ns = 30 },
3411 .{ .name = "draw", .gpu_ns = 30 },
3412 });
3413 var run = Analyzer.init(std.testing.allocator);
3414 defer run.deinit();
3415 try addGpuTrace(&run, &.{.{ .name = "draw", .gpu_ns = 60 }});
3416
3417 var out = std.Io.Writer.Allocating.init(std.testing.allocator);
3418 defer out.deinit();
3419 try writeText(std.testing.allocator, &baseline, &run, &out.writer, .{
3420 .top = 8,
3421 .min_gpu_delta_ns = 20,
3422 });
3423 const text = out.written();
3424 try expectContains(text, "gpu status=changed evidence=complete name=\"draw\"");
3425 try expectContains(text, "delta_total_gpu_ns=0");
3426 try expectContains(text, "base_mean_gpu_ns=30 run_mean_gpu_ns=60 delta_mean_gpu_ns=30");
3427 }
3428
3429 test "compare always surfaces partial GPU evidence" {
3430 var baseline = Analyzer.init(std.testing.allocator);
3431 defer baseline.deinit();
3432 try addGpuTrace(&baseline, &.{.{ .name = "draw", .gpu_ns = 20 }});
3433 var run = Analyzer.init(std.testing.allocator);
3434 defer run.deinit();
3435 try addGpuTrace(&run, &.{.{ .name = "draw", .gpu_ns = 30, .complete = false }});
3436
3437 var out = std.Io.Writer.Allocating.init(std.testing.allocator);
3438 defer out.deinit();
3439 try writeText(std.testing.allocator, &baseline, &run, &out.writer, .{
3440 .top = 8,
3441 .min_gpu_delta_ns = 1_000,
3442 });
3443 const text = out.written();
3444 try std.testing.expect(std.mem.indexOf(u8, text, "run_integrity=unbalanced_events") != null);
3445 try expectContains(
3446 text,
3447 "partial=1 baseline_incomplete_zones=0 run_incomplete_zones=1",
3448 );
3449 try expectContains(text, "gpu status=changed evidence=partial name=\"draw\"");
3450 try expectContains(text, "base_completed_zones=1 run_completed_zones=0");
3451 }
3452
3453 test "compare GPU JSONL retains integrity and units" {
3454 var baseline = Analyzer.init(std.testing.allocator);
3455 defer baseline.deinit();
3456 try addGpuTrace(&baseline, &.{.{ .name = "draw", .gpu_ns = 60 }});
3457 var run = Analyzer.init(std.testing.allocator);
3458 defer run.deinit();
3459 try addGpuTrace(&run, &.{.{ .name = "draw", .gpu_ns = 100 }});
3460
3461 var out = std.Io.Writer.Allocating.init(std.testing.allocator);
3462 defer out.deinit();
3463 try writeJsonl(std.testing.allocator, &baseline, &run, &out.writer, .{ .top = 4 });
3464 const text = out.written();
3465 try std.testing.expect(std.mem.indexOf(u8, text, "\"gpu_changed\":1") != null);
3466 try expectContains(text, "\"baseline_capture_integrity\":\"complete\"");
3467 try std.testing.expect(std.mem.indexOf(u8, text, "\"kind\":\"gpu\"") != null);
3468 try std.testing.expect(std.mem.indexOf(u8, text, "\"delta_total_gpu_ns\":40") != null);
3469 try std.testing.expect(std.mem.indexOf(u8, text, "\"delta_mean_gpu_ns\":40") != null);
3470 try std.testing.expectEqual(2, try validJsonLineCount(text));
3471 }
3472
3473 test "compare correlates plot names and preserves distributions" {
3474 var baseline = Analyzer.init(std.testing.allocator);
3475 defer baseline.deinit();
3476 try addPlotTrace(&baseline, &.{
3477 .{ .name = "latency", .unit = "nanoseconds", .values = &.{ 10, 20, 30 } },
3478 .{ .name = "removed", .unit = "count", .values = &.{1} },
3479 .{ .name = "steady", .unit = "count", .values = &.{5} },
3480 }, false);
3481 var run = Analyzer.init(std.testing.allocator);
3482 defer run.deinit();
3483 try addPlotTrace(&run, &.{
3484 .{ .name = "latency", .unit = "nanoseconds", .values = &.{ 20, 40, 60 } },
3485 .{ .name = "new", .unit = "count", .values = &.{1} },
3486 .{ .name = "steady", .unit = "count", .values = &.{5} },
3487 }, false);
3488
3489 var out = std.Io.Writer.Allocating.init(std.testing.allocator);
3490 defer out.deinit();
3491 try writeText(std.testing.allocator, &baseline, &run, &out.writer, .{ .top = 8 });
3492 const text = out.written();
3493 try expectContains(
3494 text,
3495 "tracy compare plots shared=1 changed=1 stable=0 new=1 removed=1 partial=0",
3496 );
3497 try expectContains(
3498 text,
3499 "plot status=changed evidence=complete unit_compatibility=matching name=\"latency\"",
3500 );
3501 try expectContains(text, "base_samples=3 run_samples=3 delta_samples=0");
3502 try expectContains(text, "base_p50=20 run_p50=40 delta_p50=20");
3503 try expectContains(text, "base_p99=30 run_p99=60 delta_p99=30");
3504 try expectContains(text, "base_mean=20 run_mean=40 delta_mean=20");
3505 try expectContains(
3506 text,
3507 "plot status=new evidence=complete unit_compatibility=not_applicable name=\"new\"",
3508 );
3509 try expectContains(
3510 text,
3511 "plot status=removed evidence=complete unit_compatibility=not_applicable " ++
3512 "name=\"removed\"",
3513 );
3514 try std.testing.expect(std.mem.indexOf(u8, text, "name=\"steady\"") == null);
3515 }
3516
3517 test "compare suppresses plot value deltas when units conflict" {
3518 var baseline = Analyzer.init(std.testing.allocator);
3519 defer baseline.deinit();
3520 try addPlotTrace(
3521 &baseline,
3522 &.{.{ .name = "heap", .unit = "bytes", .values = &.{ 64, 96 } }},
3523 false,
3524 );
3525 var run = Analyzer.init(std.testing.allocator);
3526 defer run.deinit();
3527 try addPlotTrace(
3528 &run,
3529 &.{.{ .name = "heap", .unit = "nanoseconds", .values = &.{ 64, 96 } }},
3530 false,
3531 );
3532
3533 var text_out = std.Io.Writer.Allocating.init(std.testing.allocator);
3534 defer text_out.deinit();
3535 try writeText(std.testing.allocator, &baseline, &run, &text_out.writer, .{});
3536 try expectContains(text_out.written(), "unit_mismatch=1 unit_unknown=0");
3537 try expectContains(text_out.written(), "unit_compatibility=mismatch name=\"heap\"");
3538 try expectContains(text_out.written(), "base_mean=80 run_mean=80 delta_mean=none");
3539
3540 var json_out = std.Io.Writer.Allocating.init(std.testing.allocator);
3541 defer json_out.deinit();
3542 try writeJsonl(std.testing.allocator, &baseline, &run, &json_out.writer, .{});
3543 const json = json_out.written();
3544 try expectContains(json, "\"plot_unit_mismatch\":1");
3545 try expectContains(json, "\"unit_compatibility\":\"mismatch\"");
3546 try expectContains(json, "\"base_unit\":\"bytes\",\"run_unit\":\"nanoseconds\"");
3547 try expectContains(json, "\"delta_mean\":null");
3548 try std.testing.expectEqual(2, try validJsonLineCount(json));
3549 }
3550
3551 test "compare always surfaces incomplete plot evidence" {
3552 var baseline = Analyzer.init(std.testing.allocator);
3553 defer baseline.deinit();
3554 try addPlotTrace(&baseline, &.{
3555 .{
3556 .name = "heap",
3557 .unit = "bytes",
3558 .values = &.{64},
3559 .conflict = "nanoseconds",
3560 .missing_value = true,
3561 },
3562 .{ .name = "legacy", .values = &.{1} },
3563 }, false);
3564 var run = Analyzer.init(std.testing.allocator);
3565 defer run.deinit();
3566 try addPlotTrace(&run, &.{
3567 .{ .name = "heap", .unit = "bytes", .values = &.{64} },
3568 .{ .name = "legacy", .values = &.{1} },
3569 }, true);
3570
3571 var out = std.Io.Writer.Allocating.init(std.testing.allocator);
3572 defer out.deinit();
3573 try writeText(std.testing.allocator, &baseline, &run, &out.writer, .{});
3574 const text = out.written();
3575 try expectContains(text, "run_integrity=sequence_gaps");
3576 try expectContains(
3577 text,
3578 "tracy compare plots shared=2 changed=0 stable=2 new=0 removed=0 partial=2",
3579 );
3580 try expectContains(text, "unit_mismatch=0 unit_unknown=1");
3581 try expectContains(text, "unit_compatibility=matching name=\"heap\"");
3582 try expectContains(
3583 text,
3584 "base_configuration_conflicts=1 run_configuration_conflicts=0",
3585 );
3586 try expectContains(text, "baseline_ignored_missing_values=1 run_ignored_missing_values=0");
3587 try expectContains(text, "unit_compatibility=unknown name=\"legacy\"");
3588 }
3589
3590 test "compare releases assembled evidence on allocation failure" {
3591 try std.testing.checkAllAllocationFailures(
3592 std.testing.allocator,
3593 writeComparisonWithAllocator,
3594 .{},
3595 );
3596 }
3597
3598 const TestGpuZone = struct {
3599 name: []const u8,
3600 gpu_ns: u64,
3601 complete: bool = true,
3602 };
3603
3604 const TestZoneSet = struct {
3605 name: []const u8,
3606 durations_ns: []const u64,
3607 invalid_last: bool = false,
3608 };
3609
3610 const TestFrameSet = struct {
3611 name: []const u8,
3612 durations_ns: []const u64,
3613 };
3614
3615 const TestPlotSet = struct {
3616 name: []const u8,
3617 unit: ?[]const u8 = null,
3618 values: []const f64,
3619 conflict: ?[]const u8 = null,
3620 missing_value: bool = false,
3621 };
3622
3623 fn expectContains(text: []const u8, needle: []const u8) !void {
3624 try std.testing.expect(std.mem.indexOf(u8, text, needle) != null);
3625 }
3626
3627 fn expectMemoryStatus(rows: []const MemoryDelta, name: []const u8, expected: Status) !void {
3628 for (rows) |row| {
3629 if (!std.mem.eql(u8, row.name, name)) continue;
3630 try std.testing.expectEqual(expected, row.status);
3631 return;
3632 }
3633 return error.TestExpectedEqual;
3634 }
3635
3636 fn validJsonLineCount(text: []const u8) !usize {
3637 var count: usize = 0;
3638 var lines = std.mem.splitScalar(u8, text, '\n');
3639 while (lines.next()) |line| {
3640 if (line.len == 0) continue;
3641 var parsed = try std.json.parseFromSlice(
3642 std.json.Value,
3643 std.testing.allocator,
3644 line,
3645 .{},
3646 );
3647 parsed.deinit();
3648 count += 1;
3649 }
3650 return count;
3651 }
3652
3653 fn writeComparisonWithAllocator(allocator: std.mem.Allocator) !void {
3654 var baseline = Analyzer.init(std.testing.allocator);
3655 defer baseline.deinit();
3656 try addZone(&baseline, "phase", 10);
3657 try addMemory(&baseline, "arena", 16);
3658 try addGpuTrace(&baseline, &.{.{ .name = "draw", .gpu_ns = 20 }});
3659 try addPlotTrace(
3660 &baseline,
3661 &.{.{ .name = "latency", .unit = "nanoseconds", .values = &.{ 10, 20 } }},
3662 false,
3663 );
3664 try addFrameTrace(
3665 &baseline,
3666 &.{.{ .name = "render", .durations_ns = &.{ 10, 10 } }},
3667 false,
3668 );
3669 var run = Analyzer.init(std.testing.allocator);
3670 defer run.deinit();
3671 try addZone(&run, "phase", 20);
3672 try addMemory(&run, "arena", 32);
3673 try addGpuTrace(&run, &.{.{ .name = "draw", .gpu_ns = 40 }});
3674 try addPlotTrace(
3675 &run,
3676 &.{.{ .name = "latency", .unit = "nanoseconds", .values = &.{ 20, 40 } }},
3677 false,
3678 );
3679 try addFrameTrace(
3680 &run,
3681 &.{.{ .name = "render", .durations_ns = &.{ 10, 20 } }},
3682 false,
3683 );
3684
3685 var out = std.Io.Writer.Allocating.init(std.testing.allocator);
3686 defer out.deinit();
3687 try writeText(allocator, &baseline, &run, &out.writer, .{});
3688 }
3689
3690 fn addPlotTrace(
3691 analyzer: *Analyzer,
3692 sets: []const TestPlotSet,
3693 sequence_gap: bool,
3694 ) !void {
3695 var trace = std.Io.Writer.Allocating.init(std.testing.allocator);
3696 defer trace.deinit();
3697 var sequence: u64 = 1;
3698 var time_ns: u64 = 100;
3699 try (event.TraceEvent{
3700 .seq = sequence,
3701 .kind = .start,
3702 .time_ns = time_ns,
3703 .thread = 1,
3704 .name = "plots",
3705 }).writeJsonLine(&trace.writer);
3706 sequence += 1;
3707 if (sequence_gap) sequence += 1;
3708 for (sets) |set| {
3709 try writeTestPlotSet(&trace.writer, &sequence, &time_ns, set);
3710 }
3711 time_ns += 10;
3712 try (event.TraceEvent{
3713 .seq = sequence,
3714 .kind = .stop,
3715 .time_ns = time_ns,
3716 .thread = 1,
3717 }).writeJsonLine(&trace.writer);
3718 try analyzer.ingestJsonlBytes(trace.written());
3719 }
3720
3721 fn writeTestPlotSet(
3722 writer: *std.Io.Writer,
3723 sequence: *u64,
3724 time_ns: *u64,
3725 set: TestPlotSet,
3726 ) !void {
3727 if (set.unit) |unit| try writeTestPlotConfig(writer, sequence, time_ns, set.name, unit);
3728 if (set.conflict) |unit| {
3729 try writeTestPlotConfig(writer, sequence, time_ns, set.name, unit);
3730 }
3731 if (set.missing_value) {
3732 time_ns.* += 10;
3733 try (event.TraceEvent{
3734 .seq = sequence.*,
3735 .kind = .plot,
3736 .time_ns = time_ns.*,
3737 .thread = 1,
3738 .name = set.name,
3739 }).writeJsonLine(writer);
3740 sequence.* += 1;
3741 }
3742 for (set.values) |value| {
3743 time_ns.* += 10;
3744 try (event.TraceEvent{
3745 .seq = sequence.*,
3746 .kind = .plot,
3747 .time_ns = time_ns.*,
3748 .thread = 1,
3749 .name = set.name,
3750 .value_f64 = value,
3751 .plot_kind = "float",
3752 }).writeJsonLine(writer);
3753 sequence.* += 1;
3754 }
3755 }
3756
3757 fn writeTestPlotConfig(
3758 writer: *std.Io.Writer,
3759 sequence: *u64,
3760 time_ns: *u64,
3761 name: []const u8,
3762 unit: []const u8,
3763 ) !void {
3764 time_ns.* += 10;
3765 try (event.TraceEvent{
3766 .seq = sequence.*,
3767 .kind = .plot_config,
3768 .time_ns = time_ns.*,
3769 .thread = 1,
3770 .name = name,
3771 .color = 7,
3772 .plot_unit = unit,
3773 .plot_step = true,
3774 .plot_fill = true,
3775 }).writeJsonLine(writer);
3776 sequence.* += 1;
3777 }
3778
3779 fn addFrameTrace(
3780 analyzer: *Analyzer,
3781 sets: []const TestFrameSet,
3782 sequence_gap: bool,
3783 ) !void {
3784 var trace = std.Io.Writer.Allocating.init(std.testing.allocator);
3785 defer trace.deinit();
3786 var sequence: u64 = 1;
3787 try (event.TraceEvent{
3788 .seq = sequence,
3789 .kind = .start,
3790 .time_ns = 1,
3791 .thread = 1,
3792 .name = "frames",
3793 }).writeJsonLine(&trace.writer);
3794 sequence += 1;
3795 if (sequence_gap) sequence += 1;
3796 var time_ns: u64 = 100;
3797 for (sets) |set| {
3798 try writeTestFrameMark(&trace.writer, &sequence, time_ns, set.name);
3799 for (set.durations_ns) |duration_ns| {
3800 time_ns += duration_ns;
3801 try writeTestFrameMark(&trace.writer, &sequence, time_ns, set.name);
3802 }
3803 time_ns += 10;
3804 }
3805 try (event.TraceEvent{
3806 .seq = sequence,
3807 .kind = .stop,
3808 .time_ns = time_ns + 10,
3809 .thread = 1,
3810 }).writeJsonLine(&trace.writer);
3811 try analyzer.ingestJsonlBytes(trace.written());
3812 }
3813
3814 fn addOutOfOrderFrameTrace(analyzer: *Analyzer) !void {
3815 var trace = std.Io.Writer.Allocating.init(std.testing.allocator);
3816 defer trace.deinit();
3817 try (event.TraceEvent{
3818 .seq = 1,
3819 .kind = .start,
3820 .time_ns = 1,
3821 .thread = 1,
3822 .name = "frames",
3823 }).writeJsonLine(&trace.writer);
3824 try (event.TraceEvent{
3825 .seq = 2,
3826 .kind = .frame,
3827 .time_ns = 100,
3828 .thread = 1,
3829 .name = "render",
3830 }).writeJsonLine(&trace.writer);
3831 try (event.TraceEvent{
3832 .seq = 3,
3833 .kind = .frame,
3834 .time_ns = 120,
3835 .thread = 1,
3836 .name = "render",
3837 }).writeJsonLine(&trace.writer);
3838 try (event.TraceEvent{
3839 .seq = 4,
3840 .kind = .frame,
3841 .time_ns = 110,
3842 .thread = 1,
3843 .name = "render",
3844 }).writeJsonLine(&trace.writer);
3845 try (event.TraceEvent{
3846 .seq = 5,
3847 .kind = .stop,
3848 .time_ns = 140,
3849 .thread = 1,
3850 }).writeJsonLine(&trace.writer);
3851 try analyzer.ingestJsonlBytes(trace.written());
3852 }
3853
3854 fn writeTestFrameMark(
3855 writer: *std.Io.Writer,
3856 sequence: *u64,
3857 time_ns: u64,
3858 name: []const u8,
3859 ) !void {
3860 try (event.TraceEvent{
3861 .seq = sequence.*,
3862 .kind = .frame,
3863 .time_ns = time_ns,
3864 .thread = 1,
3865 .name = name,
3866 }).writeJsonLine(writer);
3867 sequence.* += 1;
3868 }
3869
3870 fn addGpuTrace(analyzer: *Analyzer, zones: []const TestGpuZone) !void {
3871 var trace = std.Io.Writer.Allocating.init(std.testing.allocator);
3872 defer trace.deinit();
3873 var sequence: u64 = 1;
3874 try (event.TraceEvent{
3875 .seq = sequence,
3876 .kind = .start,
3877 .time_ns = 900,
3878 .thread = 1,
3879 .name = "gpu",
3880 }).writeJsonLine(&trace.writer);
3881 sequence += 1;
3882 try (event.TraceEvent{
3883 .seq = sequence,
3884 .kind = .gpu_context,
3885 .time_ns = 1_000,
3886 .thread = 10,
3887 .name = "render",
3888 .gpu_context = 2,
3889 .gpu_time = 100,
3890 .gpu_period = 1,
3891 .gpu_context_type = "vulkan",
3892 }).writeJsonLine(&trace.writer);
3893 sequence += 1;
3894 var cpu_time: u64 = 1_010;
3895 var gpu_time: i64 = 110;
3896 for (zones, 0..) |zone, index| {
3897 const begin_query: u32 = @intCast(index * 2 + 1);
3898 const end_query = begin_query + 1;
3899 try writeTestGpuZone(
3900 &trace.writer,
3901 &sequence,
3902 &cpu_time,
3903 &gpu_time,
3904 begin_query,
3905 end_query,
3906 zone,
3907 );
3908 }
3909 try (event.TraceEvent{
3910 .seq = sequence,
3911 .kind = .stop,
3912 .time_ns = cpu_time + 10,
3913 .thread = 1,
3914 }).writeJsonLine(&trace.writer);
3915 try analyzer.ingestJsonlBytes(trace.written());
3916 }
3917
3918 fn writeTestGpuZone(
3919 writer: *std.Io.Writer,
3920 sequence: *u64,
3921 cpu_time: *u64,
3922 gpu_time: *i64,
3923 begin_query: u32,
3924 end_query: u32,
3925 zone: TestGpuZone,
3926 ) !void {
3927 try (event.TraceEvent{
3928 .seq = sequence.*,
3929 .kind = .gpu_zone_begin,
3930 .time_ns = cpu_time.*,
3931 .thread = 10,
3932 .name = zone.name,
3933 .gpu_context = 2,
3934 .gpu_query = begin_query,
3935 }).writeJsonLine(writer);
3936 sequence.* += 1;
3937 try (event.TraceEvent{
3938 .seq = sequence.*,
3939 .kind = .gpu_zone_end,
3940 .time_ns = cpu_time.* + 10,
3941 .thread = 10,
3942 .gpu_context = 2,
3943 .gpu_query = end_query,
3944 }).writeJsonLine(writer);
3945 sequence.* += 1;
3946 try (event.TraceEvent{
3947 .seq = sequence.*,
3948 .kind = .gpu_time,
3949 .time_ns = cpu_time.* + 11,
3950 .thread = 10,
3951 .gpu_context = 2,
3952 .gpu_query = begin_query,
3953 .gpu_time = gpu_time.*,
3954 }).writeJsonLine(writer);
3955 sequence.* += 1;
3956 if (zone.complete) {
3957 try (event.TraceEvent{
3958 .seq = sequence.*,
3959 .kind = .gpu_time,
3960 .time_ns = cpu_time.* + 12,
3961 .thread = 10,
3962 .gpu_context = 2,
3963 .gpu_query = end_query,
3964 .gpu_time = gpu_time.* + @as(i64, @intCast(zone.gpu_ns)),
3965 }).writeJsonLine(writer);
3966 sequence.* += 1;
3967 }
3968 gpu_time.* += @intCast(zone.gpu_ns + 10);
3969 cpu_time.* += 20;
3970 }
3971
3972 fn addZone(analyzer: *Analyzer, name: []const u8, duration_ns: u64) !void {
3973 const durations = [_]u64{duration_ns};
3974 return addZoneTrace(analyzer, &.{.{
3975 .name = name,
3976 .durations_ns = &durations,
3977 }}, false);
3978 }
3979
3980 fn addZoneTrace(
3981 analyzer: *Analyzer,
3982 sets: []const TestZoneSet,
3983 sequence_gap: bool,
3984 ) !void {
3985 var trace = std.Io.Writer.Allocating.init(std.testing.allocator);
3986 defer trace.deinit();
3987 var sequence: u64 = 1;
3988 var time_ns: u64 = 100;
3989 var zone_id: u64 = 1;
3990 try (event.TraceEvent{ .seq = sequence, .kind = .start, .time_ns = time_ns })
3991 .writeJsonLine(&trace.writer);
3992 sequence += 1;
3993 if (sequence_gap) sequence += 1;
3994 for (sets) |set| for (set.durations_ns, 0..) |duration_ns, duration_index| {
3995 time_ns += 10;
3996 try (event.TraceEvent{
3997 .seq = sequence,
3998 .kind = .zone_begin,
3999 .time_ns = time_ns,
4000 .thread = 1,
4001 .id = zone_id,
4002 .name = set.name,
4003 }).writeJsonLine(&trace.writer);
4004 sequence += 1;
4005 const invalid = set.invalid_last and duration_index + 1 == set.durations_ns.len;
4006 const end_ns = if (invalid) time_ns - 1 else time_ns + duration_ns;
4007 try (event.TraceEvent{
4008 .seq = sequence,
4009 .kind = .zone_end,
4010 .time_ns = end_ns,
4011 .thread = 1,
4012 .id = zone_id,
4013 }).writeJsonLine(&trace.writer);
4014 sequence += 1;
4015 zone_id += 1;
4016 if (!invalid) time_ns = end_ns;
4017 };
4018 time_ns += 10;
4019 try (event.TraceEvent{ .seq = sequence, .kind = .stop, .time_ns = time_ns })
4020 .writeJsonLine(&trace.writer);
4021 try analyzer.ingestJsonlBytes(trace.written());
4022 }
4023
4024 fn addMemory(analyzer: *Analyzer, name: []const u8, high_water_bytes: u64) !void {
4025 var trace = std.Io.Writer.Allocating.init(std.testing.allocator);
4026 defer trace.deinit();
4027 try (event.TraceEvent{
4028 .seq = 1,
4029 .kind = .alloc,
4030 .time_ns = 100,
4031 .thread = 1,
4032 .name = name,
4033 .address = 4096,
4034 .size = high_water_bytes,
4035 }).writeJsonLine(&trace.writer);
4036 try (event.TraceEvent{
4037 .seq = 2,
4038 .kind = .free,
4039 .time_ns = 110,
4040 .thread = 1,
4041 .name = name,
4042 .address = 4096,
4043 }).writeJsonLine(&trace.writer);
4044 try analyzer.ingestJsonlBytes(trace.written());
4045 }
4046
4047 const MemoryFixtureOptions = struct {
4048 right_censored: bool = false,
4049 size_mismatch: bool = false,
4050 };
4051
4052 fn addMemoryLifetimes(
4053 analyzer: *Analyzer,
4054 name: []const u8,
4055 size: u64,
4056 lifetimes_ns: []const u64,
4057 options: MemoryFixtureOptions,
4058 ) !void {
4059 std.debug.assert(lifetimes_ns.len <= 16);
4060 std.debug.assert(size < std.math.maxInt(u64));
4061 var trace = std.Io.Writer.Allocating.init(std.testing.allocator);
4062 defer trace.deinit();
4063 var sequence: u64 = 1;
4064 var time_ns: u64 = 100;
4065 var address: u64 = 4096;
4066 try (event.TraceEvent{
4067 .seq = sequence,
4068 .kind = .start,
4069 .time_ns = time_ns,
4070 .thread = 1,
4071 .name = "memory-compare",
4072 }).writeJsonLine(&trace.writer);
4073 sequence += 1;
4074 for (lifetimes_ns, 0..) |lifetime_ns, index| {
4075 time_ns += 10;
4076 try writeTestAllocation(&trace.writer, sequence, time_ns, name, address, size);
4077 sequence += 1;
4078 time_ns += lifetime_ns;
4079 const reported_size = if (options.size_mismatch and index == 0) size + 1 else size;
4080 try writeTestFree(&trace.writer, sequence, time_ns, name, address, reported_size);
4081 sequence += 1;
4082 address += 4096;
4083 }
4084 if (options.right_censored) {
4085 time_ns += 10;
4086 try writeTestAllocation(&trace.writer, sequence, time_ns, name, address, size);
4087 sequence += 1;
4088 }
4089 time_ns += 10;
4090 try (event.TraceEvent{
4091 .seq = sequence,
4092 .kind = .stop,
4093 .time_ns = time_ns,
4094 .thread = 1,
4095 .name = "memory-compare",
4096 }).writeJsonLine(&trace.writer);
4097 try analyzer.ingestJsonlBytes(trace.written());
4098 }
4099
4100 fn writeTestAllocation(
4101 writer: *std.Io.Writer,
4102 sequence: u64,
4103 time_ns: u64,
4104 name: []const u8,
4105 address: u64,
4106 size: u64,
4107 ) !void {
4108 try (event.TraceEvent{
4109 .seq = sequence,
4110 .kind = .alloc,
4111 .time_ns = time_ns,
4112 .thread = 1,
4113 .name = name,
4114 .address = address,
4115 .size = size,
4116 }).writeJsonLine(writer);
4117 }
4118
4119 fn writeTestFree(
4120 writer: *std.Io.Writer,
4121 sequence: u64,
4122 time_ns: u64,
4123 name: []const u8,
4124 address: u64,
4125 size: u64,
4126 ) !void {
4127 try (event.TraceEvent{
4128 .seq = sequence,
4129 .kind = .free,
4130 .time_ns = time_ns,
4131 .thread = 1,
4132 .name = name,
4133 .address = address,
4134 .size = size,
4135 }).writeJsonLine(writer);
4136 }