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 }