lib/tracy/src/gpu.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

   1 const std = @import("std");
   2 const pretty_json = @import("pretty").json;
   3 const capture_mod = @import("capture.zig");
   4 const report = @import("report.zig");
   5 const event = @import("event.zig");
   6 const record_mod = @import("record.zig");
   7 const transport = @import("transport.zig");
   8 
   9 pub const schema = "tracy.gpu/v1";
  10 pub const CaptureIntegrity = capture_mod.Integrity;
  11 
  12 pub const Group = enum {
  13     context,
  14     name,
  15     thread,
  16     annotation,
  17     none,
  18 
  19     pub fn fromName(text: []const u8) ?Group {
  20         if (std.mem.eql(u8, text, "context")) return .context;
  21         if (std.mem.eql(u8, text, "name")) return .name;
  22         if (std.mem.eql(u8, text, "thread")) return .thread;
  23         if (std.mem.eql(u8, text, "annotation")) return .annotation;
  24         if (std.mem.eql(u8, text, "none")) return .none;
  25         return null;
  26     }
  27 
  28     fn tag(self: Group) []const u8 {
  29         return switch (self) {
  30             .context => "context",
  31             .name => "name",
  32             .thread => "thread",
  33             .annotation => "annotation",
  34             .none => "none",
  35         };
  36     }
  37 };
  38 
  39 pub const Sort = enum {
  40     gpu,
  41     gpu_tail,
  42     cpu,
  43     cpu_tail,
  44     count,
  45     annotations,
  46     last,
  47     context,
  48     thread,
  49     label,
  50 
  51     pub fn fromName(text: []const u8) ?Sort {
  52         if (std.mem.eql(u8, text, "gpu")) return .gpu;
  53         if (std.mem.eql(u8, text, "gpu-tail")) return .gpu_tail;
  54         if (std.mem.eql(u8, text, "cpu")) return .cpu;
  55         if (std.mem.eql(u8, text, "cpu-tail")) return .cpu_tail;
  56         if (std.mem.eql(u8, text, "count")) return .count;
  57         if (std.mem.eql(u8, text, "annotations")) return .annotations;
  58         if (std.mem.eql(u8, text, "last")) return .last;
  59         if (std.mem.eql(u8, text, "context")) return .context;
  60         if (std.mem.eql(u8, text, "thread")) return .thread;
  61         if (std.mem.eql(u8, text, "label")) return .label;
  62         return null;
  63     }
  64 
  65     fn tag(self: Sort) []const u8 {
  66         return switch (self) {
  67             .gpu => "gpu",
  68             .gpu_tail => "gpu-tail",
  69             .cpu => "cpu",
  70             .cpu_tail => "cpu-tail",
  71             .count => "count",
  72             .annotations => "annotations",
  73             .last => "last",
  74             .context => "context",
  75             .thread => "thread",
  76             .label => "label",
  77         };
  78     }
  79 };
  80 
  81 pub const Options = struct {
  82     top: usize = 20,
  83     occurrences: usize = 80,
  84     group: Group = .name,
  85     sort: Sort = .gpu,
  86     context: ?u32 = null,
  87     thread: ?u64 = null,
  88     since_ns: ?u64 = null,
  89     until_ns: ?u64 = null,
  90     min_gpu_ns: u64 = 0,
  91     match: ?[]const u8 = null,
  92     ignore_case: bool = false,
  93 };
  94 
  95 pub const Counters = struct {
  96     events: u64 = 0,
  97     contexts: u64 = 0,
  98     context_names: u64 = 0,
  99     zone_begins: u64 = 0,
 100     zone_ends: u64 = 0,
 101     completed_zones: u64 = 0,
 102     gpu_times: u64 = 0,
 103     calibrations: u64 = 0,
 104     syncs: u64 = 0,
 105     annotation_names: u64 = 0,
 106     annotations: u64 = 0,
 107     unmatched_ends: u64 = 0,
 108     unmatched_times: u64 = 0,
 109     duplicate_queries: u64 = 0,
 110     duplicate_times: u64 = 0,
 111     gpu_duration_samples: u64 = 0,
 112     cpu_duration_samples: u64 = 0,
 113     gpu_timestamp_regressions: u64 = 0,
 114     cpu_timestamp_regressions: u64 = 0,
 115     filtered: u64 = 0,
 116     groups: u64 = 0,
 117     duration_ns: u64 = 0,
 118 };
 119 
 120 const QuerySide = enum {
 121     start,
 122     end,
 123 };
 124 
 125 const QueryRef = struct {
 126     zone: usize,
 127     side: QuerySide,
 128 };
 129 
 130 const StackKey = struct {
 131     context: u32,
 132     thread: u64,
 133 };
 134 
 135 const AnnotationKey = struct {
 136     context: u32,
 137     annotation_id: i64,
 138 };
 139 
 140 const StackState = struct {
 141     zones: std.ArrayListUnmanaged(usize) = .empty,
 142 
 143     fn deinit(self: *StackState, allocator: std.mem.Allocator) void {
 144         self.zones.deinit(allocator);
 145         self.* = undefined;
 146     }
 147 };
 148 
 149 const ContextState = struct {
 150     id: u32,
 151     name: ?[]u8 = null,
 152     context_type: ?[]u8 = null,
 153     period: f64 = 1.0,
 154     has_calibration: bool = false,
 155     time_diff_ns: i64 = 0,
 156     calibrated_gpu_time: f64 = 0,
 157     calibrated_cpu_time_ns: i64 = 0,
 158     calibration_mod: f64 = 1.0,
 159     zones: u64 = 0,
 160     completed_zones: u64 = 0,
 161     gpu_ns: u64 = 0,
 162     cpu_ns: u64 = 0,
 163     gpu_times: u64 = 0,
 164     calibrations: u64 = 0,
 165     syncs: u64 = 0,
 166     annotations: u64 = 0,
 167     first_ns: u64 = 0,
 168     last_ns: u64 = 0,
 169 
 170     fn deinit(self: *ContextState, allocator: std.mem.Allocator) void {
 171         if (self.name) |name| allocator.free(name);
 172         if (self.context_type) |context_type| allocator.free(context_type);
 173         self.* = undefined;
 174     }
 175 
 176     fn displayName(self: ContextState) []const u8 {
 177         return self.name orelse "";
 178     }
 179 
 180     fn scaleGpuTime(self: ContextState, gpu_time: i64) f64 {
 181         return @as(f64, @floatFromInt(gpu_time)) * self.period;
 182     }
 183 
 184     fn convertGpuTime(self: ContextState, gpu_time: i64) i64 {
 185         const scaled = self.scaleGpuTime(gpu_time);
 186         if (self.has_calibration) {
 187             return floatToI64((scaled - self.calibrated_gpu_time) * self.calibration_mod + @as(f64, @floatFromInt(self.calibrated_cpu_time_ns)));
 188         }
 189         return floatToI64(scaled) + self.time_diff_ns;
 190     }
 191 };
 192 
 193 const ZoneState = struct {
 194     context: u32,
 195     begin_query: u32,
 196     end_query: u32 = 0,
 197     thread: u64,
 198     name: []u8,
 199     file: ?[]u8 = null,
 200     function: ?[]u8 = null,
 201     line: u32 = 0,
 202     column: u32 = 0,
 203     color: ?u32 = null,
 204     cpu_start_ns: u64,
 205     cpu_end_ns: ?u64 = null,
 206     gpu_start_ns: ?i64 = null,
 207     gpu_end_ns: ?i64 = null,
 208     annotations: u64 = 0,
 209     serial: bool = false,
 210     accounted: bool = false,
 211 
 212     fn deinit(self: *ZoneState, allocator: std.mem.Allocator) void {
 213         allocator.free(self.name);
 214         if (self.file) |file| allocator.free(file);
 215         if (self.function) |function| allocator.free(function);
 216         self.* = undefined;
 217     }
 218 };
 219 
 220 const DurationSample = struct {
 221     row: usize,
 222     duration_ns: u64,
 223 };
 224 
 225 const AnnotationName = struct {
 226     name: []u8,
 227 
 228     fn deinit(self: *AnnotationName, allocator: std.mem.Allocator) void {
 229         allocator.free(self.name);
 230         self.* = undefined;
 231     }
 232 };
 233 
 234 const AnnotationState = struct {
 235     label: []u8,
 236     context: u32,
 237     id: i64,
 238     count: u64 = 0,
 239     min: f64 = 0,
 240     max: f64 = 0,
 241     last: f64 = 0,
 242     first_ns: u64 = 0,
 243     last_ns: u64 = 0,
 244 
 245     fn deinit(self: *AnnotationState, allocator: std.mem.Allocator) void {
 246         allocator.free(self.label);
 247         self.* = undefined;
 248     }
 249 };
 250 
 251 const OccurrenceKind = enum {
 252     context,
 253     context_name,
 254     zone_begin,
 255     zone_end,
 256     gpu_time,
 257     calibration,
 258     time_sync,
 259     annotation,
 260     annotation_name,
 261     zone,
 262 
 263     fn tag(self: OccurrenceKind) []const u8 {
 264         return switch (self) {
 265             .context => "context",
 266             .context_name => "context-name",
 267             .zone_begin => "zone-begin",
 268             .zone_end => "zone-end",
 269             .gpu_time => "gpu-time",
 270             .calibration => "calibration",
 271             .time_sync => "time-sync",
 272             .annotation => "annotation",
 273             .annotation_name => "annotation-name",
 274             .zone => "zone",
 275         };
 276     }
 277 };
 278 
 279 const Occurrence = struct {
 280     kind: OccurrenceKind,
 281     seq: u64 = 0,
 282     time_ns: u64 = 0,
 283     context: ?u32 = null,
 284     query: ?u32 = null,
 285     thread: u64 = 0,
 286     name: ?[]u8 = null,
 287     gpu_time: ?i64 = null,
 288     converted_gpu_ns: ?i64 = null,
 289     annotation_id: ?i64 = null,
 290     value: ?f64 = null,
 291     unmatched: bool = false,
 292     duplicate_query: bool = false,
 293     duplicate_time: bool = false,
 294     end_query: ?u32 = null,
 295     cpu_start_ns: ?u64 = null,
 296     cpu_end_ns: ?u64 = null,
 297     gpu_start_ns: ?i64 = null,
 298     gpu_end_ns: ?i64 = null,
 299     gpu_duration_ns: ?u64 = null,
 300     cpu_duration_ns: ?u64 = null,
 301 
 302     fn deinit(self: *Occurrence, allocator: std.mem.Allocator) void {
 303         if (self.name) |name| allocator.free(name);
 304         self.* = undefined;
 305     }
 306 };
 307 
 308 pub const Summary = struct {
 309     group: Group,
 310     label: []u8,
 311     count: u64 = 0,
 312     context: ?u32 = null,
 313     thread: ?u64 = null,
 314     zones: u64 = 0,
 315     completed_zones: u64 = 0,
 316     gpu_ns: u64 = 0,
 317     cpu_ns: u64 = 0,
 318     gpu_duration_samples: u64 = 0,
 319     gpu_timestamp_regressions: u64 = 0,
 320     gpu_min_ns: u64 = 0,
 321     gpu_p50_ns: u64 = 0,
 322     gpu_p90_ns: u64 = 0,
 323     gpu_p99_ns: u64 = 0,
 324     gpu_max_ns: u64 = 0,
 325     cpu_duration_samples: u64 = 0,
 326     cpu_timestamp_regressions: u64 = 0,
 327     cpu_min_ns: u64 = 0,
 328     cpu_p50_ns: u64 = 0,
 329     cpu_p90_ns: u64 = 0,
 330     cpu_p99_ns: u64 = 0,
 331     cpu_max_ns: u64 = 0,
 332     gpu_times: u64 = 0,
 333     calibrations: u64 = 0,
 334     syncs: u64 = 0,
 335     annotations: u64 = 0,
 336     annotation_id: ?i64 = null,
 337     annotation_min: ?f64 = null,
 338     annotation_max: ?f64 = null,
 339     annotation_last: ?f64 = null,
 340     first_ns: u64 = 0,
 341     last_ns: u64 = 0,
 342 
 343     pub fn deinit(self: *Summary, allocator: std.mem.Allocator) void {
 344         allocator.free(self.label);
 345         self.* = undefined;
 346     }
 347 
 348     pub fn meanGpuNs(self: Summary) u64 {
 349         if (self.gpu_duration_samples == 0) return 0;
 350         return self.gpu_ns / self.gpu_duration_samples;
 351     }
 352 
 353     pub fn meanCpuNs(self: Summary) u64 {
 354         if (self.cpu_duration_samples == 0) return 0;
 355         return self.cpu_ns / self.cpu_duration_samples;
 356     }
 357 };
 358 
 359 pub const Analyzer = struct {
 360     allocator: std.mem.Allocator,
 361     capture: capture_mod.Tracker = .{},
 362     contexts: std.AutoHashMapUnmanaged(u32, ContextState) = .{},
 363     stacks: std.AutoHashMapUnmanaged(StackKey, StackState) = .{},
 364     query_refs: std.AutoHashMapUnmanaged(u64, QueryRef) = .{},
 365     annotation_names: std.AutoHashMapUnmanaged(AnnotationKey, AnnotationName) = .{},
 366     annotations: std.AutoHashMapUnmanaged(AnnotationKey, AnnotationState) = .{},
 367     zones: std.ArrayListUnmanaged(ZoneState) = .empty,
 368     occurrences: std.ArrayListUnmanaged(Occurrence) = .empty,
 369     counters: Counters = .{},
 370     start_ns: ?u64 = null,
 371     end_ns: ?u64 = null,
 372 
 373     pub fn init(allocator: std.mem.Allocator) Analyzer {
 374         return .{ .allocator = allocator };
 375     }
 376 
 377     pub fn deinit(self: *Analyzer) void {
 378         var context_iter = self.contexts.valueIterator();
 379         while (context_iter.next()) |context| context.deinit(self.allocator);
 380         self.contexts.deinit(self.allocator);
 381         var stack_iter = self.stacks.valueIterator();
 382         while (stack_iter.next()) |stack| stack.deinit(self.allocator);
 383         self.stacks.deinit(self.allocator);
 384         self.query_refs.deinit(self.allocator);
 385         var annotation_name_iter = self.annotation_names.valueIterator();
 386         while (annotation_name_iter.next()) |annotation_name| annotation_name.deinit(self.allocator);
 387         self.annotation_names.deinit(self.allocator);
 388         var annotation_iter = self.annotations.valueIterator();
 389         while (annotation_iter.next()) |annotation| annotation.deinit(self.allocator);
 390         self.annotations.deinit(self.allocator);
 391         for (self.zones.items) |*zone| zone.deinit(self.allocator);
 392         self.zones.deinit(self.allocator);
 393         for (self.occurrences.items) |*occurrence| occurrence.deinit(self.allocator);
 394         self.occurrences.deinit(self.allocator);
 395         self.* = undefined;
 396     }
 397 
 398     pub fn ingestJsonlBytes(self: *Analyzer, bytes: []const u8) !void {
 399         var lines = std.mem.splitScalar(u8, bytes, '\n');
 400         while (lines.next()) |line| try self.ingestJsonLine(line);
 401     }
 402 
 403     pub fn ingestJsonLine(self: *Analyzer, line: []const u8) !void {
 404         const text = std.mem.trim(u8, line, " \t\r\n");
 405         if (text.len == 0) return;
 406         var parsed = try record_mod.parseLine(self.allocator, text);
 407         defer parsed.deinit();
 408         switch (parsed) {
 409             .event => |value| try self.ingest(value),
 410             .flight => |report_value| self.recordFlightReport(report_value),
 411         }
 412     }
 413 
 414     pub fn ingest(self: *Analyzer, parsed: event.Parsed) !void {
 415         self.capture.record(parsed);
 416         self.counters.events += 1;
 417         if (self.start_ns == null and parsed.time_ns != 0) self.start_ns = parsed.time_ns;
 418         if (parsed.time_ns != 0) self.end_ns = parsed.time_ns;
 419         switch (parsed.kind) {
 420             .start => {
 421                 if (parsed.time_ns != 0) self.start_ns = parsed.time_ns;
 422             },
 423             .stop => {
 424                 if (parsed.time_ns != 0) self.end_ns = parsed.time_ns;
 425             },
 426             .gpu_context => try self.recordContext(parsed),
 427             .gpu_context_name => try self.recordContextName(parsed),
 428             .gpu_zone_begin => try self.recordZoneBegin(parsed),
 429             .gpu_zone_end => try self.recordZoneEnd(parsed),
 430             .gpu_time => try self.recordGpuTime(parsed),
 431             .gpu_calibration => try self.recordCalibration(parsed),
 432             .gpu_time_sync => try self.recordTimeSync(parsed),
 433             .gpu_annotation_name => try self.recordAnnotationName(parsed),
 434             .gpu_annotation => try self.recordAnnotation(parsed),
 435             else => {},
 436         }
 437     }
 438 
 439     pub fn collectSummaries(self: *Analyzer, allocator: std.mem.Allocator, options: Options) !std.ArrayListUnmanaged(Summary) {
 440         var summaries: std.ArrayListUnmanaged(Summary) = .empty;
 441         errdefer deinitSummaries(allocator, &summaries);
 442         self.counters.filtered = 0;
 443         switch (options.group) {
 444             .context => {
 445                 var iter = self.contexts.valueIterator();
 446                 while (iter.next()) |context| {
 447                     if (!contextMatches(context.*, options)) {
 448                         self.counters.filtered += 1;
 449                         continue;
 450                     }
 451                     try summaries.append(allocator, try contextSummary(allocator, context.*));
 452                 }
 453             },
 454             .name => try self.collectZoneGroups(allocator, &summaries, options, .name),
 455             .thread => try self.collectZoneGroups(allocator, &summaries, options, .thread),
 456             .annotation => {
 457                 var iter = self.annotations.valueIterator();
 458                 while (iter.next()) |annotation| {
 459                     if (!annotationMatches(annotation.*, options)) {
 460                         self.counters.filtered += 1;
 461                         continue;
 462                     }
 463                     try summaries.append(allocator, try annotationSummary(allocator, annotation.*));
 464                 }
 465             },
 466             .none => {
 467                 for (self.zones.items) |zone| {
 468                     if (!zoneMatches(zone, options)) {
 469                         self.counters.filtered += 1;
 470                         continue;
 471                     }
 472                     try summaries.append(allocator, try zoneSummary(allocator, zone));
 473                 }
 474             },
 475         }
 476         if (options.group != .annotation) {
 477             try self.applyDurationDistributions(allocator, summaries.items, options);
 478         }
 479         self.counters.groups = @intCast(summaries.items.len);
 480         self.counters.duration_ns = self.durationNs();
 481         sortSummaries(summaries.items, options.sort);
 482         return summaries;
 483     }
 484 
 485     pub fn collectOccurrences(self: *Analyzer, allocator: std.mem.Allocator, options: Options) !std.ArrayListUnmanaged(Occurrence) {
 486         var occurrences: std.ArrayListUnmanaged(Occurrence) = .empty;
 487         errdefer occurrences.deinit(allocator);
 488         if (options.sort == .gpu_tail or options.sort == .cpu_tail) {
 489             try occurrences.ensureTotalCapacity(allocator, self.zones.items.len);
 490             for (self.zones.items) |zone| {
 491                 if (!zone.accounted or !zoneMatches(zone, options)) continue;
 492                 occurrences.appendAssumeCapacity(zoneOccurrence(zone));
 493             }
 494             sortOccurrences(occurrences.items, options.sort);
 495             return occurrences;
 496         }
 497         try occurrences.ensureTotalCapacity(allocator, self.occurrences.items.len);
 498         for (self.occurrences.items) |occurrence| {
 499             if (!occurrenceMatches(occurrence, options)) continue;
 500             occurrences.appendAssumeCapacity(occurrence);
 501         }
 502         sortOccurrences(occurrences.items, options.sort);
 503         return occurrences;
 504     }
 505 
 506     pub fn durationNs(self: Analyzer) u64 {
 507         const start_ns = self.start_ns orelse return 0;
 508         const end_ns = self.end_ns orelse return 0;
 509         if (end_ns <= start_ns) return 0;
 510         return end_ns - start_ns;
 511     }
 512 
 513     pub fn gpuNs(self: Analyzer) u64 {
 514         var total: u64 = 0;
 515         var iter = self.contexts.valueIterator();
 516         while (iter.next()) |context| total +|= context.gpu_ns;
 517         return total;
 518     }
 519 
 520     pub fn cpuNs(self: Analyzer) u64 {
 521         var total: u64 = 0;
 522         var iter = self.contexts.valueIterator();
 523         while (iter.next()) |context| total +|= context.cpu_ns;
 524         return total;
 525     }
 526 
 527     pub fn incompleteZoneCount(self: Analyzer) u64 {
 528         return self.counters.zone_begins -| self.counters.completed_zones;
 529     }
 530 
 531     pub fn captureIntegrity(self: Analyzer) CaptureIntegrity {
 532         var unbalanced = self.incompleteZoneCount();
 533         unbalanced +|= self.counters.unmatched_ends;
 534         unbalanced +|= self.counters.unmatched_times;
 535         return self.capture.integrity(unbalanced);
 536     }
 537 
 538     pub fn durationPairsComplete(self: Analyzer) bool {
 539         if (self.incompleteZoneCount() != 0) return false;
 540         if (self.counters.unmatched_ends != 0) return false;
 541         if (self.counters.unmatched_times != 0) return false;
 542         if (self.counters.duplicate_queries != 0) return false;
 543         if (self.counters.duplicate_times != 0) return false;
 544         if (self.counters.gpu_timestamp_regressions != 0) return false;
 545         return self.counters.cpu_timestamp_regressions == 0;
 546     }
 547 
 548     pub fn durationEvidence(self: Analyzer) []const u8 {
 549         if (!std.mem.eql(u8, self.captureIntegrity().status, "complete")) return "partial";
 550         return if (self.durationPairsComplete()) "complete" else "partial";
 551     }
 552 
 553     pub fn recordFlightReport(self: *Analyzer, report_value: transport.Report) void {
 554         self.capture.recordFlightReport(report_value);
 555     }
 556 
 557     fn recordContext(self: *Analyzer, parsed: event.Parsed) !void {
 558         const context_id = parsed.gpu_context orelse 0;
 559         const context = try self.contextState(context_id);
 560         try replaceOptional(self.allocator, &context.name, parsed.name);
 561         try replaceOptional(self.allocator, &context.context_type, parsed.gpu_context_type);
 562         if (parsed.gpu_period) |period| context.period = period;
 563         context.has_calibration = parsed.gpu_has_calibration;
 564         const raw_gpu = parsed.gpu_time orelse 0;
 565         const scaled_gpu = context.scaleGpuTime(raw_gpu);
 566         context.time_diff_ns = @as(i64, @intCast(parsed.time_ns)) - floatToI64(scaled_gpu);
 567         context.calibrated_gpu_time = scaled_gpu;
 568         context.calibrated_cpu_time_ns = @intCast(parsed.time_ns);
 569         context.calibration_mod = 1.0;
 570         noteRange(&context.first_ns, &context.last_ns, parsed.time_ns);
 571         self.counters.contexts += 1;
 572         try self.recordOccurrence(.{
 573             .kind = .context,
 574             .seq = parsed.seq,
 575             .time_ns = parsed.time_ns,
 576             .context = context_id,
 577             .thread = parsed.thread,
 578             .name = try dupeOptional(self.allocator, parsed.name),
 579             .gpu_time = parsed.gpu_time,
 580         });
 581     }
 582 
 583     fn recordContextName(self: *Analyzer, parsed: event.Parsed) !void {
 584         const context_id = parsed.gpu_context orelse 0;
 585         const context = try self.contextState(context_id);
 586         try replaceOptional(self.allocator, &context.name, parsed.name);
 587         noteRange(&context.first_ns, &context.last_ns, parsed.time_ns);
 588         self.counters.context_names += 1;
 589         try self.recordOccurrence(.{
 590             .kind = .context_name,
 591             .seq = parsed.seq,
 592             .time_ns = parsed.time_ns,
 593             .context = context_id,
 594             .thread = parsed.thread,
 595             .name = try dupeOptional(self.allocator, parsed.name),
 596         });
 597     }
 598 
 599     fn recordZoneBegin(self: *Analyzer, parsed: event.Parsed) !void {
 600         const context_id = parsed.gpu_context orelse 0;
 601         const query = parsed.gpu_query orelse 0;
 602         const context = try self.contextState(context_id);
 603         context.zones += 1;
 604         noteRange(&context.first_ns, &context.last_ns, parsed.time_ns);
 605         const name = parsed.name orelse "<gpu>";
 606         const index = self.zones.items.len;
 607         const zone = try initZoneState(self.allocator, parsed, context_id, query, name);
 608         self.zones.append(self.allocator, zone) catch |err| {
 609             var owned = zone;
 610             owned.deinit(self.allocator);
 611             return err;
 612         };
 613         const duplicate_query = if (query == 0)
 614             false
 615         else
 616             try self.bindQuery(context_id, query, .{ .zone = index, .side = .start });
 617         const stack = try self.stackState(context_id, parsed.thread);
 618         try stack.zones.append(self.allocator, index);
 619         self.counters.zone_begins += 1;
 620         try self.recordOccurrence(.{
 621             .kind = .zone_begin,
 622             .seq = parsed.seq,
 623             .time_ns = parsed.time_ns,
 624             .context = context_id,
 625             .query = query,
 626             .thread = parsed.thread,
 627             .name = try self.allocator.dupe(u8, name),
 628             .duplicate_query = duplicate_query,
 629         });
 630     }
 631 
 632     fn recordZoneEnd(self: *Analyzer, parsed: event.Parsed) !void {
 633         const context_id = parsed.gpu_context orelse 0;
 634         const query = parsed.gpu_query orelse 0;
 635         const stack = try self.stackState(context_id, parsed.thread);
 636         const index = stack.zones.pop() orelse {
 637             self.counters.unmatched_ends += 1;
 638             try self.recordOccurrence(.{
 639                 .kind = .zone_end,
 640                 .seq = parsed.seq,
 641                 .time_ns = parsed.time_ns,
 642                 .context = context_id,
 643                 .query = query,
 644                 .thread = parsed.thread,
 645                 .unmatched = true,
 646             });
 647             return;
 648         };
 649         const zone = &self.zones.items[index];
 650         zone.cpu_end_ns = parsed.time_ns;
 651         zone.end_query = query;
 652         zone.serial = zone.serial or parsed.gpu_serial;
 653         const duplicate_query = if (query == 0)
 654             false
 655         else
 656             try self.bindQuery(context_id, query, .{ .zone = index, .side = .end });
 657         const context = try self.contextState(context_id);
 658         noteRange(&context.first_ns, &context.last_ns, parsed.time_ns);
 659         self.counters.zone_ends += 1;
 660         try self.accountZoneIfComplete(index);
 661         try self.recordOccurrence(.{
 662             .kind = .zone_end,
 663             .seq = parsed.seq,
 664             .time_ns = parsed.time_ns,
 665             .context = context_id,
 666             .query = query,
 667             .thread = parsed.thread,
 668             .duplicate_query = duplicate_query,
 669         });
 670     }
 671 
 672     fn recordGpuTime(self: *Analyzer, parsed: event.Parsed) !void {
 673         const context_id = parsed.gpu_context orelse 0;
 674         const query = parsed.gpu_query orelse 0;
 675         const raw_gpu = parsed.gpu_time orelse return;
 676         const context = try self.contextState(context_id);
 677         context.gpu_times += 1;
 678         self.counters.gpu_times += 1;
 679         const converted = context.convertGpuTime(raw_gpu);
 680         const query_ref = self.query_refs.get(queryKey(context_id, query));
 681         var duplicate_time = false;
 682         if (query_ref) |reference| {
 683             const zone = &self.zones.items[reference.zone];
 684             const slot = switch (reference.side) {
 685                 .start => &zone.gpu_start_ns,
 686                 .end => &zone.gpu_end_ns,
 687             };
 688             if (slot.* == null) {
 689                 slot.* = converted;
 690                 try self.accountZoneIfComplete(reference.zone);
 691             } else {
 692                 duplicate_time = true;
 693                 self.counters.duplicate_times +|= 1;
 694             }
 695         } else {
 696             self.counters.unmatched_times +|= 1;
 697         }
 698         noteRange(&context.first_ns, &context.last_ns, parsed.time_ns);
 699         try self.recordOccurrence(.{
 700             .kind = .gpu_time,
 701             .seq = parsed.seq,
 702             .time_ns = parsed.time_ns,
 703             .context = context_id,
 704             .query = query,
 705             .thread = parsed.thread,
 706             .gpu_time = raw_gpu,
 707             .converted_gpu_ns = converted,
 708             .unmatched = query_ref == null,
 709             .duplicate_time = duplicate_time,
 710         });
 711     }
 712 
 713     fn recordCalibration(self: *Analyzer, parsed: event.Parsed) !void {
 714         const context_id = parsed.gpu_context orelse 0;
 715         const raw_gpu = parsed.gpu_time orelse return;
 716         const context = try self.contextState(context_id);
 717         const scaled_gpu = context.scaleGpuTime(raw_gpu);
 718         const gpu_delta = scaled_gpu - context.calibrated_gpu_time;
 719         if (gpu_delta != 0) {
 720             const cpu_delta = parsed.gpu_cpu_delta_ns orelse 0;
 721             context.calibration_mod = @as(f64, @floatFromInt(cpu_delta)) / gpu_delta;
 722             context.calibrated_gpu_time = scaled_gpu;
 723             context.calibrated_cpu_time_ns = @intCast(parsed.time_ns);
 724         }
 725         context.calibrations += 1;
 726         self.counters.calibrations += 1;
 727         noteRange(&context.first_ns, &context.last_ns, parsed.time_ns);
 728         try self.recordOccurrence(.{
 729             .kind = .calibration,
 730             .seq = parsed.seq,
 731             .time_ns = parsed.time_ns,
 732             .context = context_id,
 733             .thread = parsed.thread,
 734             .gpu_time = raw_gpu,
 735         });
 736     }
 737 
 738     fn recordTimeSync(self: *Analyzer, parsed: event.Parsed) !void {
 739         const context_id = parsed.gpu_context orelse 0;
 740         const raw_gpu = parsed.gpu_time orelse return;
 741         const context = try self.contextState(context_id);
 742         const scaled_gpu = context.scaleGpuTime(raw_gpu);
 743         context.time_diff_ns = @as(i64, @intCast(parsed.time_ns)) - floatToI64(scaled_gpu);
 744         context.syncs += 1;
 745         self.counters.syncs += 1;
 746         noteRange(&context.first_ns, &context.last_ns, parsed.time_ns);
 747         try self.recordOccurrence(.{
 748             .kind = .time_sync,
 749             .seq = parsed.seq,
 750             .time_ns = parsed.time_ns,
 751             .context = context_id,
 752             .thread = parsed.thread,
 753             .gpu_time = raw_gpu,
 754         });
 755     }
 756 
 757     fn recordAnnotationName(self: *Analyzer, parsed: event.Parsed) !void {
 758         const context_id = parsed.gpu_context orelse 0;
 759         const annotation_id = parsed.gpu_annotation_id orelse return;
 760         const name = parsed.name orelse return;
 761         const key = AnnotationKey{ .context = context_id, .annotation_id = annotation_id };
 762         const owned_name = try self.allocator.dupe(u8, name);
 763         const entry = self.annotation_names.getOrPut(self.allocator, key) catch |err| {
 764             self.allocator.free(owned_name);
 765             return err;
 766         };
 767         if (entry.found_existing) entry.value_ptr.deinit(self.allocator);
 768         entry.value_ptr.* = .{ .name = owned_name };
 769         self.counters.annotation_names += 1;
 770         try self.recordOccurrence(.{
 771             .kind = .annotation_name,
 772             .seq = parsed.seq,
 773             .time_ns = parsed.time_ns,
 774             .context = context_id,
 775             .thread = parsed.thread,
 776             .name = try self.allocator.dupe(u8, name),
 777             .annotation_id = annotation_id,
 778         });
 779     }
 780 
 781     fn recordAnnotation(self: *Analyzer, parsed: event.Parsed) !void {
 782         const context_id = parsed.gpu_context orelse 0;
 783         const annotation_id = parsed.gpu_annotation_id orelse return;
 784         const value = parsed.value_f64 orelse return;
 785         const key = AnnotationKey{ .context = context_id, .annotation_id = annotation_id };
 786         const label = if (self.annotation_names.get(key)) |named| named.name else null;
 787         const annotation = self.annotations.getPtr(key) orelse create: {
 788             const owned_label = try self.annotationLabel(context_id, annotation_id, label);
 789             errdefer self.allocator.free(owned_label);
 790             const entry = try self.annotations.getOrPut(self.allocator, key);
 791             std.debug.assert(!entry.found_existing);
 792             entry.value_ptr.* = .{
 793                 .label = owned_label,
 794                 .context = context_id,
 795                 .id = annotation_id,
 796             };
 797             break :create entry.value_ptr;
 798         };
 799         if (annotation.count == 0) {
 800             annotation.min = value;
 801             annotation.max = value;
 802         } else {
 803             annotation.min = @min(annotation.min, value);
 804             annotation.max = @max(annotation.max, value);
 805         }
 806         annotation.count += 1;
 807         annotation.last = value;
 808         noteRange(&annotation.first_ns, &annotation.last_ns, parsed.time_ns);
 809         const context = try self.contextState(context_id);
 810         context.annotations += 1;
 811         self.counters.annotations += 1;
 812         if (parsed.gpu_query) |query| {
 813             if (self.query_refs.get(queryKey(context_id, query))) |query_ref| {
 814                 self.zones.items[query_ref.zone].annotations += 1;
 815             }
 816         }
 817         try self.recordOccurrence(.{
 818             .kind = .annotation,
 819             .seq = parsed.seq,
 820             .time_ns = parsed.time_ns,
 821             .context = context_id,
 822             .query = parsed.gpu_query,
 823             .thread = parsed.thread,
 824             .name = try dupeOptional(self.allocator, label),
 825             .annotation_id = annotation_id,
 826             .value = value,
 827         });
 828     }
 829 
 830     fn collectZoneGroups(
 831         self: *Analyzer,
 832         allocator: std.mem.Allocator,
 833         summaries: *std.ArrayListUnmanaged(Summary),
 834         options: Options,
 835         group: Group,
 836     ) !void {
 837         var groups: std.StringHashMapUnmanaged(usize) = .{};
 838         defer groups.deinit(allocator);
 839         const zone_capacity = std.math.cast(u32, self.zones.items.len) orelse
 840             return error.TooManyGpuZones;
 841         try groups.ensureTotalCapacity(allocator, zone_capacity);
 842         for (self.zones.items) |zone| {
 843             if (!zoneMatches(zone, options)) {
 844                 self.counters.filtered += 1;
 845                 continue;
 846             }
 847             var thread_buffer: [32]u8 = undefined;
 848             const key = switch (group) {
 849                 .name => zone.name,
 850                 .thread => try std.fmt.bufPrint(&thread_buffer, "thread {d}", .{zone.thread}),
 851                 else => unreachable,
 852             };
 853             const entry = groups.getOrPutAssumeCapacity(key);
 854             var summary_index: usize = undefined;
 855             if (!entry.found_existing) {
 856                 const label = try allocator.dupe(u8, key);
 857                 errdefer allocator.free(label);
 858                 summary_index = summaries.items.len;
 859                 try summaries.append(allocator, .{
 860                     .group = group,
 861                     .label = label,
 862                     .thread = if (group == .thread) zone.thread else null,
 863                 });
 864                 entry.key_ptr.* = label;
 865                 entry.value_ptr.* = summary_index;
 866             } else {
 867                 summary_index = entry.value_ptr.*;
 868             }
 869             addZoneToSummary(&summaries.items[summary_index], zone);
 870         }
 871     }
 872 
 873     fn applyDurationDistributions(
 874         self: *Analyzer,
 875         allocator: std.mem.Allocator,
 876         summaries: []Summary,
 877         options: Options,
 878     ) !void {
 879         var names: std.StringHashMapUnmanaged(usize) = .{};
 880         defer names.deinit(allocator);
 881         var ids: std.AutoHashMapUnmanaged(u64, usize) = .{};
 882         defer ids.deinit(allocator);
 883         const group_capacity = std.math.cast(u32, summaries.len) orelse
 884             return error.TooManyGpuGroups;
 885         switch (options.group) {
 886             .context, .thread => try ids.ensureTotalCapacity(allocator, group_capacity),
 887             .name => try names.ensureTotalCapacity(allocator, group_capacity),
 888             .none, .annotation => {},
 889         }
 890         for (summaries, 0..) |summary, index| switch (options.group) {
 891             .context => ids.putAssumeCapacity(summary.context.?, index),
 892             .name => names.putAssumeCapacity(summary.label, index),
 893             .thread => ids.putAssumeCapacity(summary.thread.?, index),
 894             .none => {},
 895             .annotation => unreachable,
 896         };
 897 
 898         var gpu_samples: std.ArrayListUnmanaged(DurationSample) = .empty;
 899         defer gpu_samples.deinit(allocator);
 900         var cpu_samples: std.ArrayListUnmanaged(DurationSample) = .empty;
 901         defer cpu_samples.deinit(allocator);
 902         try gpu_samples.ensureTotalCapacity(allocator, self.zones.items.len);
 903         try cpu_samples.ensureTotalCapacity(allocator, self.zones.items.len);
 904         var none_index: usize = 0;
 905         for (self.zones.items) |zone| {
 906             const row = durationRow(
 907                 zone,
 908                 options,
 909                 summaries.len,
 910                 &none_index,
 911                 &names,
 912                 &ids,
 913             ) orelse continue;
 914             if (!zone.accounted) continue;
 915             if (zoneGpuDuration(zone)) |duration_ns| {
 916                 gpu_samples.appendAssumeCapacity(.{ .row = row, .duration_ns = duration_ns });
 917             } else {
 918                 summaries[row].gpu_timestamp_regressions +|= 1;
 919             }
 920             if (zoneCpuDuration(zone)) |duration_ns| {
 921                 cpu_samples.appendAssumeCapacity(.{ .row = row, .duration_ns = duration_ns });
 922             } else {
 923                 summaries[row].cpu_timestamp_regressions +|= 1;
 924             }
 925         }
 926         applyDurationSamples(summaries, gpu_samples.items, .gpu);
 927         applyDurationSamples(summaries, cpu_samples.items, .cpu);
 928     }
 929 
 930     fn contextState(self: *Analyzer, id: u32) !*ContextState {
 931         const entry = try self.contexts.getOrPut(self.allocator, id);
 932         if (!entry.found_existing) entry.value_ptr.* = .{ .id = id };
 933         return entry.value_ptr;
 934     }
 935 
 936     fn stackState(self: *Analyzer, context: u32, thread: u64) !*StackState {
 937         const entry = try self.stacks.getOrPut(self.allocator, .{ .context = context, .thread = thread });
 938         if (!entry.found_existing) entry.value_ptr.* = .{};
 939         return entry.value_ptr;
 940     }
 941 
 942     fn annotationLabel(self: *Analyzer, context: u32, id: i64, name: ?[]const u8) ![]u8 {
 943         if (name) |actual| return try self.allocator.dupe(u8, actual);
 944         return try std.fmt.allocPrint(self.allocator, "context {d} annotation {d}", .{ context, id });
 945     }
 946 
 947     fn accountZoneIfComplete(self: *Analyzer, index: usize) !void {
 948         const zone = &self.zones.items[index];
 949         if (zone.accounted) return;
 950         if (zone.cpu_end_ns == null or zone.gpu_start_ns == null or zone.gpu_end_ns == null) return;
 951         zone.accounted = true;
 952         const context = try self.contextState(zone.context);
 953         context.completed_zones +|= 1;
 954         self.counters.completed_zones +|= 1;
 955         if (zoneCpuDuration(zone.*)) |duration_ns| {
 956             context.cpu_ns +|= duration_ns;
 957             self.counters.cpu_duration_samples +|= 1;
 958         } else {
 959             self.counters.cpu_timestamp_regressions +|= 1;
 960         }
 961         if (zoneGpuDuration(zone.*)) |duration_ns| {
 962             context.gpu_ns +|= duration_ns;
 963             self.counters.gpu_duration_samples +|= 1;
 964         } else {
 965             self.counters.gpu_timestamp_regressions +|= 1;
 966         }
 967     }
 968 
 969     fn bindQuery(self: *Analyzer, context: u32, query: u32, query_ref: QueryRef) !bool {
 970         const entry = try self.query_refs.getOrPut(self.allocator, queryKey(context, query));
 971         if (entry.found_existing) {
 972             self.counters.duplicate_queries +|= 1;
 973             return true;
 974         }
 975         entry.value_ptr.* = query_ref;
 976         return false;
 977     }
 978 
 979     fn recordOccurrence(self: *Analyzer, occurrence: Occurrence) !void {
 980         self.occurrences.append(self.allocator, occurrence) catch |err| {
 981             var owned = occurrence;
 982             owned.deinit(self.allocator);
 983             return err;
 984         };
 985     }
 986 };
 987 
 988 pub fn deinitSummaries(allocator: std.mem.Allocator, summaries: *std.ArrayListUnmanaged(Summary)) void {
 989     for (summaries.items) |*summary| summary.deinit(allocator);
 990     summaries.deinit(allocator);
 991 }
 992 
 993 pub fn writeTextFromJsonlPath(
 994     allocator: std.mem.Allocator,
 995     path: []const u8,
 996     writer: *std.Io.Writer,
 997     options: Options,
 998 ) !void {
 999     return report.writeFromJsonlPath(Analyzer, writeText, allocator, path, writer, options);
1000 }
1001 
1002 pub fn writeJsonlFromJsonlPath(
1003     allocator: std.mem.Allocator,
1004     path: []const u8,
1005     writer: *std.Io.Writer,
1006     options: Options,
1007 ) !void {
1008     return report.writeFromJsonlPath(Analyzer, writeJsonl, allocator, path, writer, options);
1009 }
1010 
1011 pub fn ingestPath(analyzer: *Analyzer, path: []const u8) !void {
1012     return report.ingestJsonlPath(analyzer, path);
1013 }
1014 
1015 fn writeText(
1016     allocator: std.mem.Allocator,
1017     analyzer: *Analyzer,
1018     writer: *std.Io.Writer,
1019     options: Options,
1020 ) !void {
1021     var summaries = try analyzer.collectSummaries(allocator, options);
1022     defer deinitSummaries(allocator, &summaries);
1023     var occurrences = try analyzer.collectOccurrences(allocator, options);
1024     defer occurrences.deinit(allocator);
1025     try writeTextHeader(writer, analyzer, options, summaries.items.len);
1026     try capture_mod.writeText(writer, analyzer.captureIntegrity());
1027     const summary_limit = @min(options.top, summaries.items.len);
1028     for (summaries.items[0..summary_limit]) |summary| {
1029         try writer.print("gpu group={s} label=", .{summary.group.tag()});
1030         try pretty_json.writeString(writer, summary.label);
1031         try writer.print(" count={d}", .{summary.count});
1032         try writeSummaryFieldsText(writer, summary);
1033         try writer.writeByte('\n');
1034     }
1035 
1036     const occurrence_limit = @min(options.occurrences, occurrences.items.len);
1037     for (occurrences.items[0..occurrence_limit]) |occurrence| {
1038         try writer.print("gpu-occurrence kind={s} time_ns={d}", .{ occurrence.kind.tag(), occurrence.time_ns });
1039         try writeOccurrenceFieldsText(writer, occurrence);
1040         try writer.writeByte('\n');
1041     }
1042 }
1043 
1044 fn writeJsonl(
1045     allocator: std.mem.Allocator,
1046     analyzer: *Analyzer,
1047     writer: *std.Io.Writer,
1048     options: Options,
1049 ) !void {
1050     var summaries = try analyzer.collectSummaries(allocator, options);
1051     defer deinitSummaries(allocator, &summaries);
1052     var occurrences = try analyzer.collectOccurrences(allocator, options);
1053     defer occurrences.deinit(allocator);
1054 
1055     try writeJsonHeader(writer, analyzer, options, summaries.items.len);
1056 
1057     const summary_limit = @min(options.top, summaries.items.len);
1058     for (summaries.items[0..summary_limit]) |summary| {
1059         var stream = pretty_json.Writer.init(writer, .minified);
1060         const object = try stream.object();
1061         try object.field("schema", schema);
1062         try object.field("kind", "group");
1063         try object.field("group", summary.group.tag());
1064         try object.field("label", summary.label);
1065         try object.field("count", summary.count);
1066         try writeSummaryFields(object, summary);
1067         try object.endLine();
1068     }
1069 
1070     const occurrence_limit = @min(options.occurrences, occurrences.items.len);
1071     for (occurrences.items[0..occurrence_limit]) |occurrence| {
1072         var stream = pretty_json.Writer.init(writer, .minified);
1073         const object = try stream.object();
1074         try object.field("schema", schema);
1075         try object.field("kind", occurrence.kind.tag());
1076         try object.field("time_ns", occurrence.time_ns);
1077         try writeOccurrenceFields(object, occurrence);
1078         try object.endLine();
1079     }
1080 }
1081 
1082 fn writeTextHeader(
1083     writer: *std.Io.Writer,
1084     analyzer: *Analyzer,
1085     options: Options,
1086     groups: usize,
1087 ) !void {
1088     const counters = analyzer.counters;
1089     try writer.print(
1090         "tracy gpu groups={d} contexts={d} zones={d} completed_zones={d} " ++
1091             "gpu_duration_samples={d} cpu_duration_samples={d} gpu_times={d} " ++
1092             "annotations={d} calibrations={d} syncs={d} gpu_ns={d} cpu_ns={d} " ++
1093             "unmatched_ends={d} unmatched_times={d} duplicate_queries={d} " ++
1094             "duplicate_times={d} gpu_timestamp_regressions={d} " ++
1095             "cpu_timestamp_regressions={d} filtered={d} duration_ns={d} " ++
1096             "duration_population=completed_valid_pairs duration_evidence={s} " ++
1097             "group={s} sort={s}\n",
1098         .{
1099             groups,                             analyzer.contexts.count(),
1100             analyzer.counters.zone_begins,      analyzer.counters.completed_zones,
1101             counters.gpu_duration_samples,      counters.cpu_duration_samples,
1102             counters.gpu_times,                 counters.annotations,
1103             counters.calibrations,              counters.syncs,
1104             analyzer.gpuNs(),                   analyzer.cpuNs(),
1105             counters.unmatched_ends,            counters.unmatched_times,
1106             counters.duplicate_queries,         counters.duplicate_times,
1107             counters.gpu_timestamp_regressions, counters.cpu_timestamp_regressions,
1108             counters.filtered,                  analyzer.durationNs(),
1109             analyzer.durationEvidence(),        options.group.tag(),
1110             options.sort.tag(),
1111         },
1112     );
1113 }
1114 
1115 fn writeJsonHeader(
1116     writer: *std.Io.Writer,
1117     analyzer: *Analyzer,
1118     options: Options,
1119     groups: usize,
1120 ) !void {
1121     const counters = analyzer.counters;
1122     var stream = pretty_json.Writer.init(writer, .minified);
1123     const object = try stream.object();
1124     try object.field("schema", schema);
1125     try object.field("kind", "summary");
1126     try object.field("groups", groups);
1127     try object.field("contexts", analyzer.contexts.count());
1128     try object.field("zones", analyzer.counters.zone_begins);
1129     try object.field("completed_zones", analyzer.counters.completed_zones);
1130     try object.field("gpu_duration_samples", counters.gpu_duration_samples);
1131     try object.field("cpu_duration_samples", counters.cpu_duration_samples);
1132     try object.field("gpu_times", counters.gpu_times);
1133     try object.field("annotations", counters.annotations);
1134     try object.field("calibrations", counters.calibrations);
1135     try object.field("syncs", counters.syncs);
1136     try object.field("gpu_ns", analyzer.gpuNs());
1137     try object.field("cpu_ns", analyzer.cpuNs());
1138     try object.field("unmatched_ends", counters.unmatched_ends);
1139     try object.field("unmatched_times", counters.unmatched_times);
1140     try object.field("duplicate_queries", counters.duplicate_queries);
1141     try object.field("duplicate_times", counters.duplicate_times);
1142     try object.field("gpu_timestamp_regressions", counters.gpu_timestamp_regressions);
1143     try object.field("cpu_timestamp_regressions", counters.cpu_timestamp_regressions);
1144     try object.field("filtered", counters.filtered);
1145     try object.field("duration_ns", analyzer.durationNs());
1146     try object.field("duration_population", "completed_valid_pairs");
1147     try object.field("duration_evidence", analyzer.durationEvidence());
1148     try object.field("group", options.group.tag());
1149     try object.field("sort", options.sort.tag());
1150     try capture_mod.writeFields(object, analyzer.captureIntegrity());
1151     try object.endLine();
1152 }
1153 
1154 fn contextSummary(allocator: std.mem.Allocator, context: ContextState) !Summary {
1155     var label = std.Io.Writer.Allocating.init(allocator);
1156     defer label.deinit();
1157     try label.writer.print("context {d}", .{context.id});
1158     if (context.name) |name| {
1159         try label.writer.writeByte(' ');
1160         try label.writer.writeAll(name);
1161     }
1162     return .{
1163         .group = .context,
1164         .label = try allocator.dupe(u8, label.written()),
1165         .count = context.zones,
1166         .context = context.id,
1167         .zones = context.zones,
1168         .completed_zones = context.completed_zones,
1169         .gpu_ns = context.gpu_ns,
1170         .cpu_ns = context.cpu_ns,
1171         .gpu_times = context.gpu_times,
1172         .calibrations = context.calibrations,
1173         .syncs = context.syncs,
1174         .annotations = context.annotations,
1175         .first_ns = context.first_ns,
1176         .last_ns = context.last_ns,
1177     };
1178 }
1179 
1180 fn zoneSummary(allocator: std.mem.Allocator, zone: ZoneState) !Summary {
1181     return .{
1182         .group = .none,
1183         .label = try allocator.dupe(u8, zone.name),
1184         .count = 1,
1185         .context = zone.context,
1186         .thread = zone.thread,
1187         .zones = 1,
1188         .completed_zones = if (zone.accounted) 1 else 0,
1189         .gpu_ns = if (zone.accounted) zoneGpuNs(zone) else 0,
1190         .cpu_ns = if (zone.accounted) zoneCpuNs(zone) else 0,
1191         .annotations = zone.annotations,
1192         .first_ns = zone.cpu_start_ns,
1193         .last_ns = zone.cpu_end_ns orelse zone.cpu_start_ns,
1194     };
1195 }
1196 
1197 fn annotationSummary(allocator: std.mem.Allocator, annotation: AnnotationState) !Summary {
1198     return .{
1199         .group = .annotation,
1200         .label = try allocator.dupe(u8, annotation.label),
1201         .count = annotation.count,
1202         .context = annotation.context,
1203         .annotations = annotation.count,
1204         .annotation_id = annotation.id,
1205         .annotation_min = annotation.min,
1206         .annotation_max = annotation.max,
1207         .annotation_last = annotation.last,
1208         .first_ns = annotation.first_ns,
1209         .last_ns = annotation.last_ns,
1210     };
1211 }
1212 
1213 fn addZoneToSummary(summary: *Summary, zone: ZoneState) void {
1214     summary.count += 1;
1215     summary.zones += 1;
1216     summary.completed_zones += if (zone.accounted) 1 else 0;
1217     if (zone.accounted) {
1218         summary.gpu_ns +|= zoneGpuNs(zone);
1219         summary.cpu_ns +|= zoneCpuNs(zone);
1220     }
1221     summary.annotations +|= zone.annotations;
1222     if (summary.context == null) summary.context = zone.context;
1223     if (summary.thread == null) summary.thread = zone.thread;
1224     noteRange(&summary.first_ns, &summary.last_ns, zone.cpu_start_ns);
1225     if (zone.cpu_end_ns) |end_ns| noteRange(&summary.first_ns, &summary.last_ns, end_ns);
1226 }
1227 
1228 fn durationRow(
1229     zone: ZoneState,
1230     options: Options,
1231     row_count: usize,
1232     none_index: *usize,
1233     names: *const std.StringHashMapUnmanaged(usize),
1234     ids: *const std.AutoHashMapUnmanaged(u64, usize),
1235 ) ?usize {
1236     return switch (options.group) {
1237         .context => ids.get(zone.context),
1238         .name => if (zoneMatches(zone, options)) names.get(zone.name) else null,
1239         .thread => if (zoneMatches(zone, options)) ids.get(zone.thread) else null,
1240         .none => if (zoneMatches(zone, options)) nextDurationRow(row_count, none_index) else null,
1241         .annotation => null,
1242     };
1243 }
1244 
1245 fn nextDurationRow(row_count: usize, index: *usize) ?usize {
1246     if (index.* >= row_count) return null;
1247     const row = index.*;
1248     index.* += 1;
1249     return row;
1250 }
1251 
1252 const DurationKind = enum {
1253     gpu,
1254     cpu,
1255 };
1256 
1257 fn applyDurationSamples(rows: []Summary, samples: []DurationSample, kind: DurationKind) void {
1258     std.mem.sort(DurationSample, samples, {}, durationSampleLessThan);
1259     var start: usize = 0;
1260     while (start < samples.len) {
1261         var end = start + 1;
1262         while (end < samples.len and samples[end].row == samples[start].row) : (end += 1) {}
1263         applyDurationDistribution(&rows[samples[start].row], samples[start..end], kind);
1264         start = end;
1265     }
1266 }
1267 
1268 fn applyDurationDistribution(
1269     summary: *Summary,
1270     samples: []const DurationSample,
1271     kind: DurationKind,
1272 ) void {
1273     std.debug.assert(samples.len > 0);
1274     const count: u64 = @intCast(samples.len);
1275     switch (kind) {
1276         .gpu => {
1277             summary.gpu_duration_samples = count;
1278             summary.gpu_min_ns = durationPercentile(samples, 0);
1279             summary.gpu_p50_ns = durationPercentile(samples, 50);
1280             summary.gpu_p90_ns = durationPercentile(samples, 90);
1281             summary.gpu_p99_ns = durationPercentile(samples, 99);
1282             summary.gpu_max_ns = durationPercentile(samples, 100);
1283         },
1284         .cpu => {
1285             summary.cpu_duration_samples = count;
1286             summary.cpu_min_ns = durationPercentile(samples, 0);
1287             summary.cpu_p50_ns = durationPercentile(samples, 50);
1288             summary.cpu_p90_ns = durationPercentile(samples, 90);
1289             summary.cpu_p99_ns = durationPercentile(samples, 99);
1290             summary.cpu_max_ns = durationPercentile(samples, 100);
1291         },
1292     }
1293 }
1294 
1295 fn durationPercentile(samples: []const DurationSample, percent: u64) u64 {
1296     std.debug.assert(samples.len > 0);
1297     const rank: usize = @intCast((@as(u128, @min(percent, 100)) * samples.len + 99) / 100);
1298     const index = @min(@max(rank, 1) - 1, samples.len - 1);
1299     return samples[index].duration_ns;
1300 }
1301 
1302 fn durationSampleLessThan(_: void, left: DurationSample, right: DurationSample) bool {
1303     if (left.row != right.row) return left.row < right.row;
1304     return left.duration_ns < right.duration_ns;
1305 }
1306 
1307 fn zoneOccurrence(zone: ZoneState) Occurrence {
1308     return .{
1309         .kind = .zone,
1310         .time_ns = zone.cpu_end_ns orelse zone.cpu_start_ns,
1311         .context = zone.context,
1312         .query = zone.begin_query,
1313         .end_query = zone.end_query,
1314         .thread = zone.thread,
1315         .name = zone.name,
1316         .cpu_start_ns = zone.cpu_start_ns,
1317         .cpu_end_ns = zone.cpu_end_ns,
1318         .gpu_start_ns = zone.gpu_start_ns,
1319         .gpu_end_ns = zone.gpu_end_ns,
1320         .gpu_duration_ns = zoneGpuDuration(zone),
1321         .cpu_duration_ns = zoneCpuDuration(zone),
1322     };
1323 }
1324 
1325 fn initZoneState(
1326     allocator: std.mem.Allocator,
1327     parsed: event.Parsed,
1328     context: u32,
1329     query: u32,
1330     name: []const u8,
1331 ) !ZoneState {
1332     var zone = ZoneState{
1333         .context = context,
1334         .begin_query = query,
1335         .thread = parsed.thread,
1336         .name = try allocator.dupe(u8, name),
1337         .line = parsed.line,
1338         .column = parsed.column,
1339         .color = parsed.color,
1340         .cpu_start_ns = parsed.time_ns,
1341         .serial = parsed.gpu_serial,
1342     };
1343     errdefer zone.deinit(allocator);
1344     zone.file = try dupeOptional(allocator, parsed.file);
1345     zone.function = try dupeOptional(allocator, parsed.function);
1346     return zone;
1347 }
1348 
1349 fn zoneCpuDuration(zone: ZoneState) ?u64 {
1350     const end_ns = zone.cpu_end_ns orelse return null;
1351     if (end_ns < zone.cpu_start_ns) return null;
1352     return end_ns - zone.cpu_start_ns;
1353 }
1354 
1355 fn zoneGpuDuration(zone: ZoneState) ?u64 {
1356     const start_ns = zone.gpu_start_ns orelse return null;
1357     const end_ns = zone.gpu_end_ns orelse return null;
1358     return gpuTimestampDuration(start_ns, end_ns);
1359 }
1360 
1361 fn gpuTimestampDuration(start_ns: i64, end_ns: i64) ?u64 {
1362     if (end_ns < start_ns) return null;
1363     return @intCast(@as(i128, end_ns) - @as(i128, start_ns));
1364 }
1365 
1366 fn zoneCpuNs(zone: ZoneState) u64 {
1367     return zoneCpuDuration(zone) orelse 0;
1368 }
1369 
1370 fn zoneGpuNs(zone: ZoneState) u64 {
1371     return zoneGpuDuration(zone) orelse 0;
1372 }
1373 
1374 fn contextMatches(context: ContextState, options: Options) bool {
1375     if (options.context) |context_filter| if (context.id != context_filter) return false;
1376     if (!rangeMatches(context.first_ns, context.last_ns, options)) return false;
1377     if (options.match) |needle| {
1378         if (contains(context.displayName(), needle, options.ignore_case)) return true;
1379         if (context.context_type) |context_type| if (contains(context_type, needle, options.ignore_case)) return true;
1380         var buffer: [32]u8 = undefined;
1381         const context_text = std.fmt.bufPrint(&buffer, "{d}", .{context.id}) catch "";
1382         if (contains(context_text, needle, options.ignore_case)) return true;
1383         return false;
1384     }
1385     return true;
1386 }
1387 
1388 fn zoneMatches(zone: ZoneState, options: Options) bool {
1389     if (options.context) |context_filter| if (zone.context != context_filter) return false;
1390     if (options.thread) |thread_filter| if (zone.thread != thread_filter) return false;
1391     if (options.min_gpu_ns != 0 and zoneGpuNs(zone) < options.min_gpu_ns) return false;
1392     const last_ns = zone.cpu_end_ns orelse zone.cpu_start_ns;
1393     if (!rangeMatches(zone.cpu_start_ns, last_ns, options)) return false;
1394     if (options.match) |needle| {
1395         if (contains(zone.name, needle, options.ignore_case)) return true;
1396         if (zone.file) |file| if (contains(file, needle, options.ignore_case)) return true;
1397         if (zone.function) |function| if (contains(function, needle, options.ignore_case)) return true;
1398         return false;
1399     }
1400     return true;
1401 }
1402 
1403 fn annotationMatches(annotation: AnnotationState, options: Options) bool {
1404     if (options.context) |context_filter| if (annotation.context != context_filter) return false;
1405     if (!rangeMatches(annotation.first_ns, annotation.last_ns, options)) return false;
1406     if (options.match) |needle| return contains(annotation.label, needle, options.ignore_case);
1407     return true;
1408 }
1409 
1410 fn occurrenceMatches(occurrence: Occurrence, options: Options) bool {
1411     if (options.context) |context_filter| {
1412         const context = occurrence.context orelse return false;
1413         if (context != context_filter) return false;
1414     }
1415     if (options.thread) |thread_filter| if (occurrence.thread != thread_filter) return false;
1416     if (options.since_ns) |since_ns| if (occurrence.time_ns < since_ns) return false;
1417     if (options.until_ns) |until_ns| if (occurrence.time_ns > until_ns) return false;
1418     if (options.match) |needle| {
1419         if (contains(occurrence.kind.tag(), needle, options.ignore_case)) return true;
1420         if (occurrence.name) |name| if (contains(name, needle, options.ignore_case)) return true;
1421         return false;
1422     }
1423     return true;
1424 }
1425 
1426 fn rangeMatches(first_ns: u64, last_ns: u64, options: Options) bool {
1427     if (options.since_ns) |since_ns| if (last_ns != 0 and last_ns < since_ns) return false;
1428     if (options.until_ns) |until_ns| if (first_ns != 0 and first_ns > until_ns) return false;
1429     return true;
1430 }
1431 
1432 fn writeSummaryFieldsText(writer: *std.Io.Writer, summary: Summary) !void {
1433     if (summary.context) |context| try writer.print(" context={d}", .{context});
1434     if (summary.thread) |thread| try writer.print(" thread={d}", .{thread});
1435     if (summary.zones != 0) try writer.print(" zones={d}", .{summary.zones});
1436     if (summary.completed_zones != 0) try writer.print(" completed_zones={d}", .{summary.completed_zones});
1437     if (summary.gpu_ns != 0) try writer.print(" gpu_ns={d}", .{summary.gpu_ns});
1438     if (summary.cpu_ns != 0) try writer.print(" cpu_ns={d}", .{summary.cpu_ns});
1439     if (summary.zones != 0) try writeDurationFieldsText(writer, summary);
1440     if (summary.gpu_times != 0) try writer.print(" gpu_times={d}", .{summary.gpu_times});
1441     if (summary.calibrations != 0) try writer.print(" calibrations={d}", .{summary.calibrations});
1442     if (summary.syncs != 0) try writer.print(" syncs={d}", .{summary.syncs});
1443     if (summary.annotations != 0) try writer.print(" annotations={d}", .{summary.annotations});
1444     if (summary.annotation_id) |annotation_id| try writer.print(" annotation_id={d}", .{annotation_id});
1445     if (summary.annotation_min) |value| try writer.print(" annotation_min={d}", .{value});
1446     if (summary.annotation_max) |value| try writer.print(" annotation_max={d}", .{value});
1447     if (summary.annotation_last) |value| try writer.print(" annotation_last={d}", .{value});
1448     if (summary.first_ns != 0) try writer.print(" first_ns={d}", .{summary.first_ns});
1449     if (summary.last_ns != 0) try writer.print(" last_ns={d}", .{summary.last_ns});
1450 }
1451 
1452 fn writeSummaryFields(object: pretty_json.Object, summary: Summary) !void {
1453     if (summary.context) |context| try object.field("context", context);
1454     if (summary.thread) |thread| try object.field("thread", thread);
1455     if (summary.zones != 0) try object.field("zones", summary.zones);
1456     if (summary.completed_zones != 0) try object.field("completed_zones", summary.completed_zones);
1457     if (summary.gpu_ns != 0) try object.field("gpu_ns", summary.gpu_ns);
1458     if (summary.cpu_ns != 0) try object.field("cpu_ns", summary.cpu_ns);
1459     if (summary.zones != 0) try writeDurationFields(object, summary);
1460     if (summary.gpu_times != 0) try object.field("gpu_times", summary.gpu_times);
1461     if (summary.calibrations != 0) try object.field("calibrations", summary.calibrations);
1462     if (summary.syncs != 0) try object.field("syncs", summary.syncs);
1463     if (summary.annotations != 0) try object.field("annotations", summary.annotations);
1464     if (summary.annotation_id) |annotation_id| try object.field("annotation_id", annotation_id);
1465     if (summary.annotation_min) |value| try object.field("annotation_min", value);
1466     if (summary.annotation_max) |value| try object.field("annotation_max", value);
1467     if (summary.annotation_last) |value| try object.field("annotation_last", value);
1468     if (summary.first_ns != 0) try object.field("first_ns", summary.first_ns);
1469     if (summary.last_ns != 0) try object.field("last_ns", summary.last_ns);
1470 }
1471 
1472 fn writeOccurrenceFieldsText(writer: *std.Io.Writer, occurrence: Occurrence) !void {
1473     if (occurrence.context) |context| try writer.print(" context={d}", .{context});
1474     if (occurrence.query) |query| try writer.print(" query={d}", .{query});
1475     if (occurrence.thread != 0) try writer.print(" thread={d}", .{occurrence.thread});
1476     if (occurrence.name) |name| {
1477         try writer.writeAll(" name=");
1478         try pretty_json.writeString(writer, name);
1479     }
1480     if (occurrence.gpu_time) |gpu_time| try writer.print(" gpu_time={d}", .{gpu_time});
1481     if (occurrence.converted_gpu_ns) |converted| try writer.print(" gpu_ns={d}", .{converted});
1482     if (occurrence.annotation_id) |annotation_id| try writer.print(" annotation_id={d}", .{annotation_id});
1483     if (occurrence.value) |value| try writer.print(" value={d}", .{value});
1484     if (occurrence.unmatched) try writer.writeAll(" unmatched=true");
1485     if (occurrence.duplicate_query) try writer.writeAll(" duplicate_query=true");
1486     if (occurrence.duplicate_time) try writer.writeAll(" duplicate_time=true");
1487     if (occurrence.kind == .zone) try writeZoneOccurrenceText(writer, occurrence);
1488 }
1489 
1490 fn writeOccurrenceFields(object: pretty_json.Object, occurrence: Occurrence) !void {
1491     if (occurrence.context) |context| try object.field("context", context);
1492     if (occurrence.query) |query| try object.field("query", query);
1493     if (occurrence.thread != 0) try object.field("thread", occurrence.thread);
1494     if (occurrence.name) |name| try object.field("name", name);
1495     if (occurrence.gpu_time) |gpu_time| try object.field("gpu_time", gpu_time);
1496     if (occurrence.converted_gpu_ns) |converted| try object.field("gpu_ns", converted);
1497     if (occurrence.annotation_id) |annotation_id| try object.field("annotation_id", annotation_id);
1498     if (occurrence.value) |value| try object.field("value", value);
1499     if (occurrence.unmatched) try object.field("unmatched", true);
1500     if (occurrence.duplicate_query) try object.field("duplicate_query", true);
1501     if (occurrence.duplicate_time) try object.field("duplicate_time", true);
1502     if (occurrence.kind == .zone) try writeZoneOccurrenceFields(object, occurrence);
1503 }
1504 
1505 pub fn writeDurationFieldsText(writer: *std.Io.Writer, summary: Summary) !void {
1506     try writer.print(
1507         " gpu_duration_samples={d} gpu_mean_ns={d} gpu_min_ns={d} " ++
1508             "gpu_p50_ns={d} gpu_p90_ns={d} gpu_p99_ns={d} gpu_max_ns={d} " ++
1509             "gpu_timestamp_regressions={d}",
1510         .{
1511             summary.gpu_duration_samples,
1512             summary.meanGpuNs(),
1513             summary.gpu_min_ns,
1514             summary.gpu_p50_ns,
1515             summary.gpu_p90_ns,
1516             summary.gpu_p99_ns,
1517             summary.gpu_max_ns,
1518             summary.gpu_timestamp_regressions,
1519         },
1520     );
1521     try writer.print(
1522         " cpu_duration_samples={d} cpu_mean_ns={d} cpu_min_ns={d} " ++
1523             "cpu_p50_ns={d} cpu_p90_ns={d} cpu_p99_ns={d} cpu_max_ns={d} " ++
1524             "cpu_timestamp_regressions={d}",
1525         .{
1526             summary.cpu_duration_samples,
1527             summary.meanCpuNs(),
1528             summary.cpu_min_ns,
1529             summary.cpu_p50_ns,
1530             summary.cpu_p90_ns,
1531             summary.cpu_p99_ns,
1532             summary.cpu_max_ns,
1533             summary.cpu_timestamp_regressions,
1534         },
1535     );
1536 }
1537 
1538 pub fn writeDurationFields(object: pretty_json.Object, summary: Summary) !void {
1539     try object.field("gpu_duration_samples", summary.gpu_duration_samples);
1540     try object.field("gpu_mean_ns", summary.meanGpuNs());
1541     try object.field("gpu_min_ns", summary.gpu_min_ns);
1542     try object.field("gpu_p50_ns", summary.gpu_p50_ns);
1543     try object.field("gpu_p90_ns", summary.gpu_p90_ns);
1544     try object.field("gpu_p99_ns", summary.gpu_p99_ns);
1545     try object.field("gpu_max_ns", summary.gpu_max_ns);
1546     try object.field("gpu_timestamp_regressions", summary.gpu_timestamp_regressions);
1547     try object.field("cpu_duration_samples", summary.cpu_duration_samples);
1548     try object.field("cpu_mean_ns", summary.meanCpuNs());
1549     try object.field("cpu_min_ns", summary.cpu_min_ns);
1550     try object.field("cpu_p50_ns", summary.cpu_p50_ns);
1551     try object.field("cpu_p90_ns", summary.cpu_p90_ns);
1552     try object.field("cpu_p99_ns", summary.cpu_p99_ns);
1553     try object.field("cpu_max_ns", summary.cpu_max_ns);
1554     try object.field("cpu_timestamp_regressions", summary.cpu_timestamp_regressions);
1555 }
1556 
1557 fn writeZoneOccurrenceText(writer: *std.Io.Writer, occurrence: Occurrence) !void {
1558     if (occurrence.end_query) |query| try writer.print(" end_query={d}", .{query});
1559     if (occurrence.cpu_start_ns) |time_ns| try writer.print(" cpu_start_ns={d}", .{time_ns});
1560     if (occurrence.cpu_end_ns) |time_ns| try writer.print(" cpu_end_ns={d}", .{time_ns});
1561     if (occurrence.gpu_start_ns) |time_ns| try writer.print(" gpu_start_ns={d}", .{time_ns});
1562     if (occurrence.gpu_end_ns) |time_ns| try writer.print(" gpu_end_ns={d}", .{time_ns});
1563     if (occurrence.gpu_duration_ns) |duration_ns| {
1564         try writer.print(" gpu_duration_ns={d}", .{duration_ns});
1565     }
1566     if (occurrence.cpu_duration_ns) |duration_ns| {
1567         try writer.print(" cpu_duration_ns={d}", .{duration_ns});
1568     }
1569     try writer.print(
1570         " gpu_duration_valid={} cpu_duration_valid={}",
1571         .{ occurrence.gpu_duration_ns != null, occurrence.cpu_duration_ns != null },
1572     );
1573 }
1574 
1575 fn writeZoneOccurrenceFields(object: pretty_json.Object, occurrence: Occurrence) !void {
1576     if (occurrence.end_query) |query| try object.field("end_query", query);
1577     if (occurrence.cpu_start_ns) |time_ns| try object.field("cpu_start_ns", time_ns);
1578     if (occurrence.cpu_end_ns) |time_ns| try object.field("cpu_end_ns", time_ns);
1579     if (occurrence.gpu_start_ns) |time_ns| try object.field("gpu_start_ns", time_ns);
1580     if (occurrence.gpu_end_ns) |time_ns| try object.field("gpu_end_ns", time_ns);
1581     if (occurrence.gpu_duration_ns) |duration_ns| try object.field("gpu_duration_ns", duration_ns);
1582     if (occurrence.cpu_duration_ns) |duration_ns| try object.field("cpu_duration_ns", duration_ns);
1583     try object.field("gpu_duration_valid", occurrence.gpu_duration_ns != null);
1584     try object.field("cpu_duration_valid", occurrence.cpu_duration_ns != null);
1585 }
1586 
1587 fn sortSummaries(items: []Summary, sort: Sort) void {
1588     std.mem.sort(Summary, items, sort, summaryLessThan);
1589 }
1590 
1591 fn summaryLessThan(sort: Sort, left: Summary, right: Summary) bool {
1592     return switch (sort) {
1593         .gpu => summaryGpuGreaterThan({}, left, right),
1594         .gpu_tail => summaryGpuTailGreaterThan({}, left, right),
1595         .cpu => summaryCpuGreaterThan({}, left, right),
1596         .cpu_tail => summaryCpuTailGreaterThan({}, left, right),
1597         .count => summaryCountGreaterThan({}, left, right),
1598         .annotations => summaryAnnotationsGreaterThan({}, left, right),
1599         .last => summaryLastGreaterThan({}, left, right),
1600         .context => summaryContextLessThan({}, left, right),
1601         .thread => summaryThreadLessThan({}, left, right),
1602         .label => summaryLabelLessThan({}, left, right),
1603     };
1604 }
1605 
1606 fn summaryGpuTailGreaterThan(_: void, left: Summary, right: Summary) bool {
1607     if (left.gpu_p99_ns != right.gpu_p99_ns) return left.gpu_p99_ns > right.gpu_p99_ns;
1608     if (left.gpu_max_ns != right.gpu_max_ns) return left.gpu_max_ns > right.gpu_max_ns;
1609     return summaryGpuGreaterThan({}, left, right);
1610 }
1611 
1612 fn summaryCpuTailGreaterThan(_: void, left: Summary, right: Summary) bool {
1613     if (left.cpu_p99_ns != right.cpu_p99_ns) return left.cpu_p99_ns > right.cpu_p99_ns;
1614     if (left.cpu_max_ns != right.cpu_max_ns) return left.cpu_max_ns > right.cpu_max_ns;
1615     return summaryCpuGreaterThan({}, left, right);
1616 }
1617 
1618 fn summaryGpuGreaterThan(_: void, left: Summary, right: Summary) bool {
1619     if (left.gpu_ns != right.gpu_ns) return left.gpu_ns > right.gpu_ns;
1620     return summaryCountGreaterThan({}, left, right);
1621 }
1622 
1623 fn summaryCpuGreaterThan(_: void, left: Summary, right: Summary) bool {
1624     if (left.cpu_ns != right.cpu_ns) return left.cpu_ns > right.cpu_ns;
1625     return summaryCountGreaterThan({}, left, right);
1626 }
1627 
1628 fn summaryCountGreaterThan(_: void, left: Summary, right: Summary) bool {
1629     if (left.count != right.count) return left.count > right.count;
1630     return std.mem.lessThan(u8, left.label, right.label);
1631 }
1632 
1633 fn summaryAnnotationsGreaterThan(_: void, left: Summary, right: Summary) bool {
1634     if (left.annotations != right.annotations) return left.annotations > right.annotations;
1635     return summaryCountGreaterThan({}, left, right);
1636 }
1637 
1638 fn summaryLastGreaterThan(_: void, left: Summary, right: Summary) bool {
1639     if (left.last_ns != right.last_ns) return left.last_ns > right.last_ns;
1640     return summaryCountGreaterThan({}, left, right);
1641 }
1642 
1643 fn summaryContextLessThan(_: void, left: Summary, right: Summary) bool {
1644     const left_context = left.context orelse 0;
1645     const right_context = right.context orelse 0;
1646     if (left_context != right_context) return left_context < right_context;
1647     return summaryCountGreaterThan({}, left, right);
1648 }
1649 
1650 fn summaryThreadLessThan(_: void, left: Summary, right: Summary) bool {
1651     const left_thread = left.thread orelse 0;
1652     const right_thread = right.thread orelse 0;
1653     if (left_thread != right_thread) return left_thread < right_thread;
1654     return summaryCountGreaterThan({}, left, right);
1655 }
1656 
1657 fn summaryLabelLessThan(_: void, left: Summary, right: Summary) bool {
1658     return std.mem.lessThan(u8, left.label, right.label);
1659 }
1660 
1661 fn sortOccurrences(items: []Occurrence, sort: Sort) void {
1662     switch (sort) {
1663         .gpu_tail => std.mem.sort(Occurrence, items, {}, occurrenceGpuGreaterThan),
1664         .cpu_tail => std.mem.sort(Occurrence, items, {}, occurrenceCpuGreaterThan),
1665         .last => std.mem.sort(Occurrence, items, {}, occurrenceTimeGreaterThan),
1666         else => std.mem.sort(Occurrence, items, {}, occurrenceTimeLessThan),
1667     }
1668 }
1669 
1670 fn occurrenceGpuGreaterThan(_: void, left: Occurrence, right: Occurrence) bool {
1671     if ((left.gpu_duration_ns != null) != (right.gpu_duration_ns != null)) {
1672         return left.gpu_duration_ns != null;
1673     }
1674     const left_ns = left.gpu_duration_ns orelse 0;
1675     const right_ns = right.gpu_duration_ns orelse 0;
1676     if (left_ns != right_ns) return left_ns > right_ns;
1677     return occurrenceTimeLessThan({}, left, right);
1678 }
1679 
1680 fn occurrenceCpuGreaterThan(_: void, left: Occurrence, right: Occurrence) bool {
1681     if ((left.cpu_duration_ns != null) != (right.cpu_duration_ns != null)) {
1682         return left.cpu_duration_ns != null;
1683     }
1684     const left_ns = left.cpu_duration_ns orelse 0;
1685     const right_ns = right.cpu_duration_ns orelse 0;
1686     if (left_ns != right_ns) return left_ns > right_ns;
1687     return occurrenceTimeLessThan({}, left, right);
1688 }
1689 
1690 fn occurrenceTimeLessThan(_: void, left: Occurrence, right: Occurrence) bool {
1691     if (left.time_ns != right.time_ns) return left.time_ns < right.time_ns;
1692     return left.seq < right.seq;
1693 }
1694 
1695 fn occurrenceTimeGreaterThan(_: void, left: Occurrence, right: Occurrence) bool {
1696     if (left.time_ns != right.time_ns) return left.time_ns > right.time_ns;
1697     return left.seq > right.seq;
1698 }
1699 
1700 fn queryKey(context: u32, query: u32) u64 {
1701     return (@as(u64, context) << 32) | query;
1702 }
1703 
1704 fn noteRange(first_ns: *u64, last_ns: *u64, time_ns: u64) void {
1705     if (time_ns == 0) return;
1706     if (first_ns.* == 0 or time_ns < first_ns.*) first_ns.* = time_ns;
1707     last_ns.* = @max(last_ns.*, time_ns);
1708 }
1709 
1710 fn floatToI64(value: f64) i64 {
1711     if (!std.math.isFinite(value)) return 0;
1712     if (value <= @as(f64, @floatFromInt(std.math.minInt(i64)))) return std.math.minInt(i64);
1713     if (value >= @as(f64, @floatFromInt(std.math.maxInt(i64)))) return std.math.maxInt(i64);
1714     return @intFromFloat(value);
1715 }
1716 
1717 fn dupeOptional(allocator: std.mem.Allocator, text: ?[]const u8) !?[]u8 {
1718     const actual = text orelse return null;
1719     return try allocator.dupe(u8, actual);
1720 }
1721 
1722 fn replaceOptional(allocator: std.mem.Allocator, slot: *?[]u8, text: ?[]const u8) !void {
1723     const replacement = try dupeOptional(allocator, text);
1724     if (slot.*) |old| allocator.free(old);
1725     slot.* = replacement;
1726 }
1727 
1728 fn contains(haystack: []const u8, needle: []const u8, ignore_case: bool) bool {
1729     if (!ignore_case) return std.mem.indexOf(u8, haystack, needle) != null;
1730     if (needle.len == 0) return true;
1731     if (needle.len > haystack.len) return false;
1732     var index: usize = 0;
1733     while (index + needle.len <= haystack.len) : (index += 1) {
1734         if (asciiEqlIgnoreCase(haystack[index .. index + needle.len], needle)) return true;
1735     }
1736     return false;
1737 }
1738 
1739 fn asciiEqlIgnoreCase(left: []const u8, right: []const u8) bool {
1740     if (left.len != right.len) return false;
1741     for (left, right) |a, b| {
1742         if (std.ascii.toLower(a) != std.ascii.toLower(b)) return false;
1743     }
1744     return true;
1745 }
1746 
1747 test "gpu analyzer converts timestamp pairs into completed zones" {
1748     var trace = std.Io.Writer.Allocating.init(std.testing.allocator);
1749     defer trace.deinit();
1750     try (event.TraceEvent{ .seq = 1, .kind = .start, .time_ns = 900, .thread = 1, .name = "gpu" }).writeJsonLine(&trace.writer);
1751     try (event.TraceEvent{ .seq = 2, .kind = .gpu_context, .time_ns = 1000, .thread = 10, .name = "render", .gpu_context = 2, .gpu_time = 100, .gpu_period = 2.0, .gpu_context_type = "vulkan" }).writeJsonLine(&trace.writer);
1752     try (event.TraceEvent{ .seq = 3, .kind = .gpu_annotation_name, .time_ns = 1005, .thread = 10, .gpu_context = 2, .gpu_annotation_id = 5, .name = "occupancy" }).writeJsonLine(&trace.writer);
1753     try (event.TraceEvent{ .seq = 4, .kind = .gpu_zone_begin, .time_ns = 1010, .thread = 10, .gpu_context = 2, .gpu_query = 11, .name = "draw", .file = "draw.zig", .line = 7 }).writeJsonLine(&trace.writer);
1754     try (event.TraceEvent{ .seq = 5, .kind = .gpu_annotation, .time_ns = 1015, .thread = 10, .gpu_context = 2, .gpu_query = 11, .gpu_annotation_id = 5, .value_f64 = 0.75 }).writeJsonLine(&trace.writer);
1755     try (event.TraceEvent{ .seq = 6, .kind = .gpu_zone_end, .time_ns = 1060, .thread = 10, .gpu_context = 2, .gpu_query = 12 }).writeJsonLine(&trace.writer);
1756     try (event.TraceEvent{ .seq = 7, .kind = .gpu_time, .time_ns = 1070, .thread = 10, .gpu_context = 2, .gpu_query = 11, .gpu_time = 110 }).writeJsonLine(&trace.writer);
1757     try (event.TraceEvent{ .seq = 8, .kind = .gpu_time, .time_ns = 1080, .thread = 10, .gpu_context = 2, .gpu_query = 12, .gpu_time = 140 }).writeJsonLine(&trace.writer);
1758     try (event.TraceEvent{ .seq = 9, .kind = .stop, .time_ns = 1100, .thread = 1 }).writeJsonLine(&trace.writer);
1759 
1760     var analyzer = Analyzer.init(std.testing.allocator);
1761     defer analyzer.deinit();
1762     try analyzer.ingestJsonlBytes(trace.written());
1763     try std.testing.expectEqual(@as(u64, 1), analyzer.counters.completed_zones);
1764     try std.testing.expectEqual(@as(u64, 60), analyzer.gpuNs());
1765     try std.testing.expectEqual(@as(u64, 50), analyzer.cpuNs());
1766     try std.testing.expectEqual(@as(u64, 1), analyzer.counters.annotations);
1767 
1768     var out = std.Io.Writer.Allocating.init(std.testing.allocator);
1769     defer out.deinit();
1770     try writeText(std.testing.allocator, &analyzer, &out.writer, .{ .group = .name, .sort = .gpu, .top = 4, .occurrences = 8 });
1771     const text = out.written();
1772     try expectGpuContains(
1773         text,
1774         "tracy gpu groups=1 contexts=1 zones=1 completed_zones=1 " ++
1775             "gpu_duration_samples=1 cpu_duration_samples=1",
1776     );
1777     try expectGpuContains(
1778         text,
1779         "gpu_times=2 annotations=1 calibrations=0 syncs=0 gpu_ns=60 cpu_ns=50",
1780     );
1781     try expectGpuContains(
1782         text,
1783         "gpu group=name label=\"draw\" count=1 context=2 thread=10 zones=1 " ++
1784             "completed_zones=1 gpu_ns=60 cpu_ns=50 gpu_duration_samples=1 " ++
1785             "gpu_mean_ns=60 gpu_min_ns=60 gpu_p50_ns=60 gpu_p90_ns=60 " ++
1786             "gpu_p99_ns=60 gpu_max_ns=60",
1787     );
1788     try std.testing.expect(std.mem.indexOf(u8, text, "gpu-occurrence kind=gpu-time time_ns=1070 context=2 query=11 thread=10 gpu_time=110 gpu_ns=1020") != null);
1789 }
1790 
1791 test "gpu jsonl filters annotations by context and match" {
1792     var trace = std.Io.Writer.Allocating.init(std.testing.allocator);
1793     defer trace.deinit();
1794     try (event.TraceEvent{ .seq = 1, .kind = .gpu_context, .time_ns = 100, .thread = 1, .gpu_context = 1, .name = "render" }).writeJsonLine(&trace.writer);
1795     try (event.TraceEvent{ .seq = 2, .kind = .gpu_annotation_name, .time_ns = 110, .thread = 1, .gpu_context = 1, .gpu_annotation_id = 3, .name = "wave occupancy" }).writeJsonLine(&trace.writer);
1796     try (event.TraceEvent{ .seq = 3, .kind = .gpu_annotation, .time_ns = 120, .thread = 1, .gpu_context = 1, .gpu_annotation_id = 3, .value_f64 = 0.5 }).writeJsonLine(&trace.writer);
1797     try (event.TraceEvent{ .seq = 4, .kind = .gpu_annotation, .time_ns = 130, .thread = 1, .gpu_context = 1, .gpu_annotation_id = 3, .value_f64 = 0.75 }).writeJsonLine(&trace.writer);
1798 
1799     var analyzer = Analyzer.init(std.testing.allocator);
1800     defer analyzer.deinit();
1801     try analyzer.ingestJsonlBytes(trace.written());
1802 
1803     var out = std.Io.Writer.Allocating.init(std.testing.allocator);
1804     defer out.deinit();
1805     try writeJsonl(std.testing.allocator, &analyzer, &out.writer, .{ .group = .annotation, .context = 1, .match = "OCCUPANCY", .ignore_case = true });
1806     const text = out.written();
1807     try expectGpuContains(text, "\"schema\":\"tracy.gpu/v1\"");
1808     try std.testing.expect(std.mem.indexOf(u8, text, "\"kind\":\"summary\",\"groups\":1") != null);
1809     try std.testing.expect(std.mem.indexOf(u8, text, "\"label\":\"wave occupancy\"") != null);
1810     try std.testing.expect(std.mem.indexOf(u8, text, "\"annotation_min\":0.5") != null);
1811     try std.testing.expect(std.mem.indexOf(u8, text, "\"annotation_max\":0.75") != null);
1812 }
1813 
1814 test "gpu duration distributions retain tails and worst zones" {
1815     var trace = std.Io.Writer.Allocating.init(std.testing.allocator);
1816     defer trace.deinit();
1817     var seq: u64 = 1;
1818     try appendGpuTestEvent(&trace.writer, &seq, .start, 1);
1819     for (1..11) |index| {
1820         try appendGpuTestZone(
1821             &trace.writer,
1822             &seq,
1823             "draw",
1824             1_000 + index * 100,
1825             index,
1826             @intCast(index * 10),
1827         );
1828     }
1829     try appendGpuTestEvent(&trace.writer, &seq, .stop, 3_000);
1830 
1831     var analyzer = Analyzer.init(std.testing.allocator);
1832     defer analyzer.deinit();
1833     try analyzer.ingestJsonlBytes(trace.written());
1834     var rows = try analyzer.collectSummaries(
1835         std.testing.allocator,
1836         .{ .group = .name, .sort = .gpu_tail },
1837     );
1838     defer deinitSummaries(std.testing.allocator, &rows);
1839     const draw = rows.items[0];
1840     try std.testing.expectEqual(@as(u64, 10), draw.gpu_duration_samples);
1841     try std.testing.expectEqual(@as(u64, 550), draw.gpu_ns);
1842     try std.testing.expectEqual(@as(u64, 55), draw.meanGpuNs());
1843     try std.testing.expectEqual(@as(u64, 10), draw.gpu_min_ns);
1844     try std.testing.expectEqual(@as(u64, 50), draw.gpu_p50_ns);
1845     try std.testing.expectEqual(@as(u64, 90), draw.gpu_p90_ns);
1846     try std.testing.expectEqual(@as(u64, 100), draw.gpu_p99_ns);
1847     try std.testing.expectEqual(@as(u64, 100), draw.gpu_max_ns);
1848 
1849     var out = std.Io.Writer.Allocating.init(std.testing.allocator);
1850     defer out.deinit();
1851     try writeText(std.testing.allocator, &analyzer, &out.writer, .{
1852         .group = .name,
1853         .sort = .gpu_tail,
1854         .occurrences = 2,
1855     });
1856     try expectGpuContains(out.written(), "duration_evidence=complete");
1857     const worst = std.mem.indexOf(u8, out.written(), "gpu_duration_ns=100").?;
1858     const next = std.mem.indexOfPos(u8, out.written(), worst + 1, "gpu_duration_ns=90").?;
1859     try std.testing.expect(worst < next);
1860 }
1861 
1862 test "gpu duration spans the signed timestamp domain" {
1863     const duration_ns = gpuTimestampDuration(std.math.minInt(i64), std.math.maxInt(i64));
1864     try std.testing.expectEqual(@as(?u64, std.math.maxInt(u64)), duration_ns);
1865 }
1866 
1867 test "gpu excludes malformed duration pairs and preserves first timestamps" {
1868     var trace = std.Io.Writer.Allocating.init(std.testing.allocator);
1869     defer trace.deinit();
1870     var seq: u64 = 1;
1871     try appendGpuTestEvent(&trace.writer, &seq, .start, 1);
1872     try appendGpuTestZone(&trace.writer, &seq, "valid", 100, 5, 10);
1873     try writeGpuTime(&trace.writer, &seq, 2, 999, 120);
1874     try writeGpuBegin(&trace.writer, &seq, "reused", 200, 2);
1875     try writeGpuEnd(&trace.writer, &seq, 210, 100);
1876     try appendGpuTestZoneRaw(&trace.writer, &seq, "gpu-reversed", 300, 305, 200, 201, 50, 40);
1877     try appendGpuTestZoneRaw(&trace.writer, &seq, "cpu-reversed", 400, 390, 300, 301, 60, 70);
1878     try appendGpuTestEvent(&trace.writer, &seq, .stop, 500);
1879 
1880     var analyzer = Analyzer.init(std.testing.allocator);
1881     defer analyzer.deinit();
1882     try analyzer.ingestJsonlBytes(trace.written());
1883     try std.testing.expectEqual(@as(u64, 3), analyzer.counters.completed_zones);
1884     try std.testing.expectEqual(@as(u64, 2), analyzer.counters.gpu_duration_samples);
1885     try std.testing.expectEqual(@as(u64, 2), analyzer.counters.cpu_duration_samples);
1886     try std.testing.expectEqual(@as(u64, 1), analyzer.counters.duplicate_queries);
1887     try std.testing.expectEqual(@as(u64, 1), analyzer.counters.duplicate_times);
1888     try std.testing.expectEqual(@as(u64, 1), analyzer.counters.gpu_timestamp_regressions);
1889     try std.testing.expectEqual(@as(u64, 1), analyzer.counters.cpu_timestamp_regressions);
1890     try std.testing.expectEqual(@as(?i64, 1_000), analyzer.zones.items[0].gpu_start_ns);
1891     try std.testing.expectEqualStrings("partial", analyzer.durationEvidence());
1892 
1893     var out = std.Io.Writer.Allocating.init(std.testing.allocator);
1894     defer out.deinit();
1895     try writeJsonl(std.testing.allocator, &analyzer, &out.writer, .{
1896         .group = .name,
1897         .sort = .gpu_tail,
1898         .occurrences = 8,
1899     });
1900     try expectGpuContains(out.written(), "\"duration_evidence\":\"partial\"");
1901     try expectGpuContains(out.written(), "\"gpu_duration_valid\":false");
1902     try expectGpuContains(out.written(), "\"cpu_duration_valid\":false");
1903 }
1904 
1905 test "gpu retains flight reports and sequence gaps" {
1906     var trace = std.Io.Writer.Allocating.init(std.testing.allocator);
1907     defer trace.deinit();
1908     try (event.TraceEvent{ .seq = 1, .kind = .start, .time_ns = 1 })
1909         .writeJsonLine(&trace.writer);
1910     try (event.TraceEvent{ .seq = 3, .kind = .gpu_context, .time_ns = 2 })
1911         .writeJsonLine(&trace.writer);
1912     try (event.TraceEvent{ .seq = 4, .kind = .stop, .time_ns = 3 })
1913         .writeJsonLine(&trace.writer);
1914     const flight_report = gpuTestFlightReport();
1915     try flight_report.writeJsonl(&trace.writer);
1916     var analyzer = Analyzer.init(std.testing.allocator);
1917     defer analyzer.deinit();
1918     try analyzer.ingestJsonlBytes(trace.written());
1919     try std.testing.expectEqualStrings("sequence_gaps", analyzer.captureIntegrity().status);
1920     try std.testing.expectEqualDeep(flight_report, analyzer.captureIntegrity().flight_report.?);
1921     try std.testing.expectEqualStrings("partial", analyzer.durationEvidence());
1922 }
1923 
1924 test "gpu releases duration evidence on allocation failure" {
1925     try std.testing.checkAllAllocationFailures(
1926         std.testing.allocator,
1927         analyzeGpuDurationDistributions,
1928         .{},
1929     );
1930 }
1931 
1932 fn analyzeGpuDurationDistributions(allocator: std.mem.Allocator) !void {
1933     var trace = std.Io.Writer.Allocating.init(std.testing.allocator);
1934     defer trace.deinit();
1935     var seq: u64 = 1;
1936     try appendGpuTestEvent(&trace.writer, &seq, .start, 1);
1937     try appendGpuTestZone(&trace.writer, &seq, "draw", 100, 5, 10);
1938     try appendGpuTestZone(&trace.writer, &seq, "draw", 200, 8, 40);
1939     try appendGpuTestEvent(&trace.writer, &seq, .stop, 300);
1940     var analyzer = Analyzer.init(allocator);
1941     defer analyzer.deinit();
1942     try analyzer.ingestJsonlBytes(trace.written());
1943     var out = std.Io.Writer.Allocating.init(std.testing.allocator);
1944     defer out.deinit();
1945     try writeJsonl(allocator, &analyzer, &out.writer, .{ .sort = .gpu_tail });
1946 }
1947 
1948 fn appendGpuTestEvent(
1949     writer: *std.Io.Writer,
1950     seq: *u64,
1951     kind: event.Kind,
1952     time_ns: u64,
1953 ) !void {
1954     try (event.TraceEvent{ .seq = seq.*, .kind = kind, .time_ns = time_ns })
1955         .writeJsonLine(writer);
1956     seq.* += 1;
1957 }
1958 
1959 fn appendGpuTestZone(
1960     writer: *std.Io.Writer,
1961     seq: *u64,
1962     name: []const u8,
1963     cpu_start_ns: u64,
1964     cpu_duration_ns: u64,
1965     gpu_duration_ns: i64,
1966 ) !void {
1967     const start_query: u32 = @intCast(seq.*);
1968     const end_query = start_query + 1;
1969     const gpu_start: i64 = @intCast(cpu_start_ns * 10);
1970     try appendGpuTestZoneRaw(
1971         writer,
1972         seq,
1973         name,
1974         cpu_start_ns,
1975         cpu_start_ns + cpu_duration_ns,
1976         start_query,
1977         end_query,
1978         gpu_start,
1979         gpu_start + gpu_duration_ns,
1980     );
1981 }
1982 
1983 fn appendGpuTestZoneRaw(
1984     writer: *std.Io.Writer,
1985     seq: *u64,
1986     name: []const u8,
1987     cpu_start_ns: u64,
1988     cpu_end_ns: u64,
1989     start_query: u32,
1990     end_query: u32,
1991     gpu_start: i64,
1992     gpu_end: i64,
1993 ) !void {
1994     try writeGpuBegin(writer, seq, name, cpu_start_ns, start_query);
1995     try writeGpuEnd(writer, seq, cpu_end_ns, end_query);
1996     try writeGpuTime(writer, seq, start_query, gpu_start, cpu_end_ns +| 1);
1997     try writeGpuTime(writer, seq, end_query, gpu_end, cpu_end_ns +| 2);
1998 }
1999 
2000 fn writeGpuBegin(
2001     writer: *std.Io.Writer,
2002     seq: *u64,
2003     name: []const u8,
2004     time_ns: u64,
2005     query: u32,
2006 ) !void {
2007     try (event.TraceEvent{
2008         .seq = seq.*,
2009         .kind = .gpu_zone_begin,
2010         .time_ns = time_ns,
2011         .thread = 7,
2012         .name = name,
2013         .gpu_context = 1,
2014         .gpu_query = query,
2015     }).writeJsonLine(writer);
2016     seq.* += 1;
2017 }
2018 
2019 fn writeGpuEnd(writer: *std.Io.Writer, seq: *u64, time_ns: u64, query: u32) !void {
2020     try (event.TraceEvent{
2021         .seq = seq.*,
2022         .kind = .gpu_zone_end,
2023         .time_ns = time_ns,
2024         .thread = 7,
2025         .gpu_context = 1,
2026         .gpu_query = query,
2027     }).writeJsonLine(writer);
2028     seq.* += 1;
2029 }
2030 
2031 fn writeGpuTime(
2032     writer: *std.Io.Writer,
2033     seq: *u64,
2034     query: u32,
2035     gpu_time: i64,
2036     time_ns: u64,
2037 ) !void {
2038     try (event.TraceEvent{
2039         .seq = seq.*,
2040         .kind = .gpu_time,
2041         .time_ns = time_ns,
2042         .thread = 7,
2043         .gpu_context = 1,
2044         .gpu_query = query,
2045         .gpu_time = gpu_time,
2046     }).writeJsonLine(writer);
2047     seq.* += 1;
2048 }
2049 
2050 fn gpuTestFlightReport() transport.Report {
2051     return .{
2052         .policy = .overwrite_oldest,
2053         .state = .accepting,
2054         .capacity_bytes = 64,
2055         .retained_bytes = 32,
2056         .event_capacity_bytes = 16,
2057         .writer_capacity_bytes = 8,
2058         .observed_events = 5,
2059         .stored_events = 5,
2060         .retained_events = 4,
2061         .overwritten_events = 1,
2062         .dropped_events = 0,
2063         .oversized_events = 0,
2064         .partial_event_bytes = 0,
2065         .discarding_oversized_event = false,
2066     };
2067 }
2068 
2069 fn expectGpuContains(haystack: []const u8, needle: []const u8) !void {
2070     try std.testing.expect(std.mem.indexOf(u8, haystack, needle) != null);
2071 }