lib/tracy/src/context.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.context/v0";
10 pub const CaptureIntegrity = capture_mod.Integrity;
11
12 pub const Group = enum {
13 thread,
14 cpu,
15 reason,
16 state,
17 transition,
18 none,
19
20 pub fn fromName(text: []const u8) ?Group {
21 if (std.mem.eql(u8, text, "thread")) return .thread;
22 if (std.mem.eql(u8, text, "cpu")) return .cpu;
23 if (std.mem.eql(u8, text, "reason")) return .reason;
24 if (std.mem.eql(u8, text, "state")) return .state;
25 if (std.mem.eql(u8, text, "transition")) return .transition;
26 if (std.mem.eql(u8, text, "none")) return .none;
27 return null;
28 }
29
30 fn tag(self: Group) []const u8 {
31 return switch (self) {
32 .thread => "thread",
33 .cpu => "cpu",
34 .reason => "reason",
35 .state => "state",
36 .transition => "transition",
37 .none => "none",
38 };
39 }
40 };
41
42 pub const Sort = enum {
43 running,
44 blocked,
45 ready,
46 latency,
47 switches,
48 wakeups,
49 migrations,
50 last,
51 thread,
52 cpu,
53 label,
54
55 pub fn fromName(text: []const u8) ?Sort {
56 if (std.mem.eql(u8, text, "running")) return .running;
57 if (std.mem.eql(u8, text, "blocked")) return .blocked;
58 if (std.mem.eql(u8, text, "ready")) return .ready;
59 if (std.mem.eql(u8, text, "latency")) return .latency;
60 if (std.mem.eql(u8, text, "switches")) return .switches;
61 if (std.mem.eql(u8, text, "wakeups")) return .wakeups;
62 if (std.mem.eql(u8, text, "migrations")) return .migrations;
63 if (std.mem.eql(u8, text, "last")) return .last;
64 if (std.mem.eql(u8, text, "thread")) return .thread;
65 if (std.mem.eql(u8, text, "cpu")) return .cpu;
66 if (std.mem.eql(u8, text, "label")) return .label;
67 return null;
68 }
69
70 fn tag(self: Sort) []const u8 {
71 return switch (self) {
72 .running => "running",
73 .blocked => "blocked",
74 .ready => "ready",
75 .latency => "latency",
76 .switches => "switches",
77 .wakeups => "wakeups",
78 .migrations => "migrations",
79 .last => "last",
80 .thread => "thread",
81 .cpu => "cpu",
82 .label => "label",
83 };
84 }
85 };
86
87 pub const Options = struct {
88 top: usize = 20,
89 occurrences: usize = 80,
90 group: Group = .thread,
91 sort: Sort = .running,
92 thread: ?u64 = null,
93 cpu: ?u32 = null,
94 since_ns: ?u64 = null,
95 until_ns: ?u64 = null,
96 match: ?[]const u8 = null,
97 ignore_case: bool = false,
98 };
99
100 pub const Counters = struct {
101 events: u64 = 0,
102 switches: u64 = 0,
103 wakeups: u64 = 0,
104 thread_names: u64 = 0,
105 ready_latency_samples: u64 = 0,
106 redundant_wakeups: u64 = 0,
107 ready_time_regressions: u64 = 0,
108 filtered: u64 = 0,
109 groups: u64 = 0,
110 duration_ns: u64 = 0,
111 };
112
113 const ReasonState = struct {
114 label: []const u8,
115 threads: std.AutoHashMapUnmanaged(u64, void) = .{},
116 cpus: std.AutoHashMapUnmanaged(u32, void) = .{},
117 count: u64 = 0,
118 blocked_ns: u64 = 0,
119 ready_ns: u64 = 0,
120 unknown_off_cpu_ns: u64 = 0,
121 first_ns: u64 = 0,
122 last_ns: u64 = 0,
123
124 fn deinit(self: *ReasonState, allocator: std.mem.Allocator) void {
125 self.threads.deinit(allocator);
126 self.cpus.deinit(allocator);
127 self.* = undefined;
128 }
129 };
130
131 const TransitionState = struct {
132 label: []const u8,
133 prev_thread: u64 = 0,
134 next_thread: u64 = 0,
135 count: u64 = 0,
136 first_ns: u64 = 0,
137 last_ns: u64 = 0,
138
139 fn deinit(self: *TransitionState, _: std.mem.Allocator) void {
140 self.* = undefined;
141 }
142 };
143
144 const CpuState = struct {
145 id: u32,
146 switches: u64 = 0,
147 running_ns: u64 = 0,
148 idle_ns: u64 = 0,
149 current_thread: u64 = 0,
150 current_since_ns: ?u64 = null,
151 first_ns: u64 = 0,
152 last_ns: u64 = 0,
153 };
154
155 const ThreadState = struct {
156 id: u64,
157 name: ?[]u8 = null,
158 switch_ins: u64 = 0,
159 switch_outs: u64 = 0,
160 wakeups: u64 = 0,
161 running_ns: u64 = 0,
162 blocked_ns: u64 = 0,
163 ready_ns: u64 = 0,
164 ready_latencies: std.ArrayListUnmanaged(u64) = .empty,
165 unknown_off_cpu_ns: u64 = 0,
166 migrations: u64 = 0,
167 running_since_ns: ?u64 = null,
168 off_cpu_since_ns: ?u64 = null,
169 ready_since_ns: ?u64 = null,
170 last_cpu: ?u32 = null,
171 off_cpu_reason: ?[]u8 = null,
172 off_cpu_state: ?[]u8 = null,
173 first_ns: u64 = 0,
174 last_ns: u64 = 0,
175
176 fn deinit(self: *ThreadState, allocator: std.mem.Allocator) void {
177 if (self.name) |name| allocator.free(name);
178 if (self.off_cpu_reason) |reason| allocator.free(reason);
179 if (self.off_cpu_state) |state| allocator.free(state);
180 self.ready_latencies.deinit(allocator);
181 self.* = undefined;
182 }
183
184 fn displayName(self: ThreadState) []const u8 {
185 return self.name orelse "";
186 }
187 };
188
189 const OccurrenceKind = enum {
190 context_switch,
191 wakeup,
192
193 fn tag(self: OccurrenceKind) []const u8 {
194 return switch (self) {
195 .context_switch => "switch",
196 .wakeup => "wakeup",
197 };
198 }
199 };
200
201 const Occurrence = struct {
202 kind: OccurrenceKind,
203 seq: u64 = 0,
204 time_ns: u64 = 0,
205 source_thread: u64 = 0,
206 cpu: ?u32 = null,
207 prev_thread: u64 = 0,
208 next_thread: u64 = 0,
209 target_thread: u64 = 0,
210 reason: ?[]u8 = null,
211 state: ?[]u8 = null,
212 previous_cstate: ?u32 = null,
213 prev_priority: ?i64 = null,
214 next_priority: ?i64 = null,
215 priority_increment: ?i64 = null,
216 ready_latency_ns: ?u64 = null,
217
218 fn deinit(self: *Occurrence, allocator: std.mem.Allocator) void {
219 if (self.reason) |reason| allocator.free(reason);
220 if (self.state) |state| allocator.free(state);
221 self.* = undefined;
222 }
223 };
224
225 pub const Summary = struct {
226 group: Group,
227 label: []u8,
228 count: u64 = 0,
229 thread: ?u64 = null,
230 cpu: ?u32 = null,
231 prev_thread: u64 = 0,
232 next_thread: u64 = 0,
233 switch_ins: u64 = 0,
234 switch_outs: u64 = 0,
235 wakeups: u64 = 0,
236 running_ns: u64 = 0,
237 blocked_ns: u64 = 0,
238 ready_ns: u64 = 0,
239 ready_latency_samples: u64 = 0,
240 ready_latency_mean_ns: u64 = 0,
241 ready_latency_min_ns: u64 = 0,
242 ready_latency_p50_ns: u64 = 0,
243 ready_latency_p90_ns: u64 = 0,
244 ready_latency_p99_ns: u64 = 0,
245 ready_latency_max_ns: u64 = 0,
246 unknown_off_cpu_ns: u64 = 0,
247 idle_ns: u64 = 0,
248 migrations: u64 = 0,
249 first_ns: u64 = 0,
250 last_ns: u64 = 0,
251
252 pub fn deinit(self: *Summary, allocator: std.mem.Allocator) void {
253 allocator.free(self.label);
254 self.* = undefined;
255 }
256 };
257
258 pub const Analyzer = struct {
259 allocator: std.mem.Allocator,
260 capture: capture_mod.Tracker = .{},
261 threads: std.AutoHashMapUnmanaged(u64, ThreadState) = .{},
262 cpus: std.AutoHashMapUnmanaged(u32, CpuState) = .{},
263 reasons: std.StringHashMapUnmanaged(ReasonState) = .{},
264 states: std.StringHashMapUnmanaged(ReasonState) = .{},
265 transitions: std.StringHashMapUnmanaged(TransitionState) = .{},
266 occurrences: std.ArrayListUnmanaged(Occurrence) = .empty,
267 counters: Counters = .{},
268 start_ns: ?u64 = null,
269 end_ns: ?u64 = null,
270
271 pub fn init(allocator: std.mem.Allocator) Analyzer {
272 return .{ .allocator = allocator };
273 }
274
275 pub fn deinit(self: *Analyzer) void {
276 var thread_iter = self.threads.valueIterator();
277 while (thread_iter.next()) |thread_state| thread_state.deinit(self.allocator);
278 self.threads.deinit(self.allocator);
279 self.cpus.deinit(self.allocator);
280 var reason_iter = self.reasons.iterator();
281 while (reason_iter.next()) |entry| {
282 self.allocator.free(entry.key_ptr.*);
283 entry.value_ptr.deinit(self.allocator);
284 }
285 self.reasons.deinit(self.allocator);
286 var state_iter = self.states.iterator();
287 while (state_iter.next()) |entry| {
288 self.allocator.free(entry.key_ptr.*);
289 entry.value_ptr.deinit(self.allocator);
290 }
291 self.states.deinit(self.allocator);
292 var transition_iter = self.transitions.iterator();
293 while (transition_iter.next()) |entry| {
294 self.allocator.free(entry.key_ptr.*);
295 entry.value_ptr.deinit(self.allocator);
296 }
297 self.transitions.deinit(self.allocator);
298 for (self.occurrences.items) |*occurrence| occurrence.deinit(self.allocator);
299 self.occurrences.deinit(self.allocator);
300 self.* = undefined;
301 }
302
303 pub fn ingestJsonlBytes(self: *Analyzer, bytes: []const u8) !void {
304 var lines = std.mem.splitScalar(u8, bytes, '\n');
305 while (lines.next()) |line| try self.ingestJsonLine(line);
306 }
307
308 pub fn ingestJsonLine(self: *Analyzer, line: []const u8) !void {
309 const text = std.mem.trim(u8, line, " \t\r\n");
310 if (text.len == 0) return;
311 var parsed = try record_mod.parseLine(self.allocator, text);
312 defer parsed.deinit();
313 switch (parsed) {
314 .event => |value| try self.ingest(value),
315 .flight => |report_value| self.recordFlightReport(report_value),
316 }
317 }
318
319 pub fn ingest(self: *Analyzer, parsed: event.Parsed) !void {
320 self.capture.record(parsed);
321 self.counters.events += 1;
322 if (self.start_ns == null and parsed.time_ns != 0) self.start_ns = parsed.time_ns;
323 if (parsed.time_ns != 0) self.end_ns = parsed.time_ns;
324 switch (parsed.kind) {
325 .start => {
326 if (parsed.time_ns != 0) self.start_ns = parsed.time_ns;
327 },
328 .stop => {
329 if (parsed.time_ns != 0) self.end_ns = parsed.time_ns;
330 },
331 .thread_name => try self.recordThreadName(parsed),
332 .context_switch => try self.recordSwitch(parsed),
333 .thread_wakeup => try self.recordWakeup(parsed),
334 else => {},
335 }
336 }
337
338 pub fn collectSummaries(self: *Analyzer, allocator: std.mem.Allocator, options: Options) !std.ArrayListUnmanaged(Summary) {
339 var summaries: std.ArrayListUnmanaged(Summary) = .empty;
340 errdefer deinitSummaries(allocator, &summaries);
341 self.counters.filtered = 0;
342 switch (options.group) {
343 .thread => {
344 var iter = self.threads.valueIterator();
345 while (iter.next()) |thread_state| {
346 if (!self.threadMatches(thread_state.*, options)) {
347 self.counters.filtered += 1;
348 continue;
349 }
350 try appendSummary(
351 allocator,
352 &summaries,
353 try threadSummary(allocator, thread_state.*),
354 );
355 }
356 },
357 .cpu => {
358 var iter = self.cpus.valueIterator();
359 while (iter.next()) |cpu| {
360 if (options.cpu) |filter_cpu| if (cpu.id != filter_cpu) {
361 self.counters.filtered += 1;
362 continue;
363 };
364 try appendSummary(allocator, &summaries, try cpuSummary(allocator, cpu.*));
365 }
366 },
367 .reason => try self.collectReasonState(allocator, &summaries, self.reasons, .reason, options),
368 .state => try self.collectReasonState(allocator, &summaries, self.states, .state, options),
369 .transition => {
370 var iter = self.transitions.valueIterator();
371 while (iter.next()) |transition| {
372 if (!transitionMatches(transition.*, options)) {
373 self.counters.filtered += 1;
374 continue;
375 }
376 try appendSummary(
377 allocator,
378 &summaries,
379 try transitionSummary(allocator, transition.*),
380 );
381 }
382 },
383 .none => {
384 for (self.occurrences.items) |occurrence| {
385 if (!occurrenceMatches(occurrence, options)) {
386 self.counters.filtered += 1;
387 continue;
388 }
389 try appendSummary(
390 allocator,
391 &summaries,
392 try occurrenceSummary(allocator, occurrence),
393 );
394 }
395 },
396 }
397 self.counters.groups = @intCast(summaries.items.len);
398 self.counters.duration_ns = self.durationNs();
399 sortSummaries(summaries.items, options.sort);
400 return summaries;
401 }
402
403 pub fn collectOccurrences(self: *Analyzer, allocator: std.mem.Allocator, options: Options) !std.ArrayListUnmanaged(Occurrence) {
404 var occurrences: std.ArrayListUnmanaged(Occurrence) = .empty;
405 for (self.occurrences.items) |occurrence| {
406 if (!occurrenceMatches(occurrence, options)) continue;
407 try occurrences.append(allocator, occurrence);
408 }
409 sortOccurrences(occurrences.items, options.sort);
410 return occurrences;
411 }
412
413 pub fn durationNs(self: Analyzer) u64 {
414 const start_ns = self.start_ns orelse return 0;
415 const end_ns = self.end_ns orelse return 0;
416 if (end_ns <= start_ns) return 0;
417 return end_ns - start_ns;
418 }
419
420 pub fn runningNs(self: Analyzer) u64 {
421 var total: u64 = 0;
422 var iter = self.threads.valueIterator();
423 while (iter.next()) |thread_state| total +|= thread_state.running_ns;
424 return total;
425 }
426
427 pub fn blockedNs(self: Analyzer) u64 {
428 var total: u64 = 0;
429 var iter = self.threads.valueIterator();
430 while (iter.next()) |thread_state| total +|= thread_state.blocked_ns;
431 return total;
432 }
433
434 pub fn readyNs(self: Analyzer) u64 {
435 var total: u64 = 0;
436 var iter = self.threads.valueIterator();
437 while (iter.next()) |thread_state| total +|= thread_state.ready_ns;
438 return total;
439 }
440
441 pub fn unknownOffCpuNs(self: Analyzer) u64 {
442 var total: u64 = 0;
443 var iter = self.threads.valueIterator();
444 while (iter.next()) |thread_state| total +|= thread_state.unknown_off_cpu_ns;
445 return total;
446 }
447
448 pub fn pendingReadyIntervals(self: Analyzer) u64 {
449 var total: u64 = 0;
450 var iter = self.threads.valueIterator();
451 while (iter.next()) |thread_state| {
452 if (thread_state.ready_since_ns != null) total +|= 1;
453 }
454 return total;
455 }
456
457 pub fn captureIntegrity(self: Analyzer) CaptureIntegrity {
458 return self.capture.integrity(0);
459 }
460
461 pub fn readyLatencyEvidence(self: Analyzer) []const u8 {
462 if (!std.mem.eql(u8, self.captureIntegrity().status, "complete")) return "partial";
463 if (self.counters.ready_time_regressions != 0) return "partial";
464 if (self.pendingReadyIntervals() != 0) return "partial";
465 return "complete";
466 }
467
468 pub fn recordFlightReport(self: *Analyzer, report_value: transport.Report) void {
469 self.capture.recordFlightReport(report_value);
470 }
471
472 fn recordThreadName(self: *Analyzer, parsed: event.Parsed) !void {
473 const name = parsed.name orelse return;
474 const thread_state = try self.threadState(parsed.thread);
475 if (thread_state.name) |old| self.allocator.free(old);
476 thread_state.name = try self.allocator.dupe(u8, name);
477 self.counters.thread_names += 1;
478 }
479
480 fn recordSwitch(self: *Analyzer, parsed: event.Parsed) !void {
481 self.counters.switches += 1;
482 const occurrence_index = self.occurrences.items.len;
483 var occurrence: Occurrence = .{
484 .kind = .context_switch,
485 .seq = parsed.seq,
486 .time_ns = parsed.time_ns,
487 .source_thread = parsed.thread,
488 .cpu = parsed.cpu,
489 .prev_thread = parsed.prev_thread,
490 .next_thread = parsed.next_thread,
491 .previous_cstate = parsed.previous_cstate,
492 .prev_priority = parsed.prev_priority,
493 .next_priority = parsed.next_priority,
494 };
495 errdefer occurrence.deinit(self.allocator);
496 occurrence.reason = try dupeOptional(self.allocator, parsed.context_reason);
497 occurrence.state = try dupeOptional(self.allocator, parsed.context_state);
498 try self.recordOccurrence(&occurrence);
499 try self.recordTransition(parsed);
500 if (parsed.cpu) |cpu_id| try self.recordCpuSwitch(cpu_id, parsed);
501 if (parsed.prev_thread != 0) try self.recordSwitchOut(parsed);
502 if (parsed.next_thread != 0) {
503 self.occurrences.items[occurrence_index].ready_latency_ns =
504 try self.recordSwitchIn(parsed);
505 }
506 }
507
508 fn recordWakeup(self: *Analyzer, parsed: event.Parsed) !void {
509 self.counters.wakeups += 1;
510 const target_thread = if (parsed.target_thread != 0) parsed.target_thread else parsed.thread;
511 var occurrence: Occurrence = .{
512 .kind = .wakeup,
513 .seq = parsed.seq,
514 .time_ns = parsed.time_ns,
515 .source_thread = parsed.thread,
516 .cpu = parsed.cpu,
517 .target_thread = target_thread,
518 .priority_increment = parsed.value_i64,
519 };
520 errdefer occurrence.deinit(self.allocator);
521 occurrence.reason = try dupeOptional(self.allocator, parsed.context_reason);
522 try self.recordOccurrence(&occurrence);
523 const target = try self.threadState(target_thread);
524 noteTime(target, parsed.time_ns);
525 target.wakeups += 1;
526 if (parsed.cpu) |cpu| target.last_cpu = cpu;
527 if (target.running_since_ns != null or target.ready_since_ns != null) {
528 self.counters.redundant_wakeups +|= 1;
529 return;
530 }
531 if (target.off_cpu_since_ns) |off_cpu_since| {
532 if (parsed.time_ns >= off_cpu_since) {
533 const duration = parsed.time_ns - off_cpu_since;
534 target.blocked_ns +|= duration;
535 try self.addReasonDuration(&self.reasons, target.off_cpu_reason orelse parsed.context_reason, duration, 0, 0, target_thread, parsed.cpu, parsed.time_ns);
536 try self.addReasonDuration(&self.states, target.off_cpu_state, duration, 0, 0, target_thread, parsed.cpu, parsed.time_ns);
537 }
538 target.off_cpu_since_ns = null;
539 }
540 target.ready_since_ns = parsed.time_ns;
541 }
542
543 fn recordSwitchOut(self: *Analyzer, parsed: event.Parsed) !void {
544 const thread_state = try self.threadState(parsed.prev_thread);
545 noteTime(thread_state, parsed.time_ns);
546 thread_state.switch_outs += 1;
547 if (parsed.cpu) |cpu| thread_state.last_cpu = cpu;
548 if (thread_state.running_since_ns) |running_since| {
549 if (parsed.time_ns >= running_since) thread_state.running_ns +|= parsed.time_ns - running_since;
550 }
551 thread_state.running_since_ns = null;
552 if (contextStateReady(parsed.context_state)) {
553 thread_state.off_cpu_since_ns = null;
554 thread_state.ready_since_ns = parsed.time_ns;
555 } else {
556 thread_state.off_cpu_since_ns = parsed.time_ns;
557 thread_state.ready_since_ns = null;
558 }
559 try replaceOptional(self.allocator, &thread_state.off_cpu_reason, parsed.context_reason);
560 try replaceOptional(self.allocator, &thread_state.off_cpu_state, parsed.context_state);
561 try self.addReasonDuration(&self.reasons, parsed.context_reason, 0, 0, 1, parsed.prev_thread, parsed.cpu, parsed.time_ns);
562 try self.addReasonDuration(&self.states, parsed.context_state, 0, 0, 1, parsed.prev_thread, parsed.cpu, parsed.time_ns);
563 }
564
565 fn recordSwitchIn(self: *Analyzer, parsed: event.Parsed) !?u64 {
566 const thread_state = try self.threadState(parsed.next_thread);
567 var ready_latency_ns: ?u64 = null;
568 noteTime(thread_state, parsed.time_ns);
569 thread_state.switch_ins += 1;
570 if (parsed.cpu) |cpu| {
571 if (thread_state.last_cpu) |last_cpu| {
572 if (last_cpu != cpu) thread_state.migrations += 1;
573 }
574 thread_state.last_cpu = cpu;
575 }
576 if (thread_state.ready_since_ns) |ready_since| {
577 if (parsed.time_ns >= ready_since) {
578 const duration = parsed.time_ns - ready_since;
579 try thread_state.ready_latencies.append(self.allocator, duration);
580 thread_state.ready_ns +|= duration;
581 self.counters.ready_latency_samples +|= 1;
582 ready_latency_ns = duration;
583 try self.addReasonDuration(&self.reasons, thread_state.off_cpu_reason, 0, duration, 0, parsed.next_thread, parsed.cpu, parsed.time_ns);
584 try self.addReasonDuration(&self.states, thread_state.off_cpu_state, 0, duration, 0, parsed.next_thread, parsed.cpu, parsed.time_ns);
585 } else {
586 self.counters.ready_time_regressions +|= 1;
587 }
588 } else if (thread_state.off_cpu_since_ns) |off_cpu_since| {
589 if (parsed.time_ns >= off_cpu_since) {
590 const duration = parsed.time_ns - off_cpu_since;
591 thread_state.unknown_off_cpu_ns +|= duration;
592 try self.addUnknownDuration(&self.reasons, thread_state.off_cpu_reason, duration, parsed.next_thread, parsed.cpu, parsed.time_ns);
593 try self.addUnknownDuration(&self.states, thread_state.off_cpu_state, duration, parsed.next_thread, parsed.cpu, parsed.time_ns);
594 }
595 }
596 thread_state.running_since_ns = parsed.time_ns;
597 thread_state.off_cpu_since_ns = null;
598 thread_state.ready_since_ns = null;
599 clearOptional(self.allocator, &thread_state.off_cpu_reason);
600 clearOptional(self.allocator, &thread_state.off_cpu_state);
601 return ready_latency_ns;
602 }
603
604 fn recordCpuSwitch(self: *Analyzer, cpu_id: u32, parsed: event.Parsed) !void {
605 const entry = try self.cpus.getOrPut(self.allocator, cpu_id);
606 if (!entry.found_existing) entry.value_ptr.* = .{ .id = cpu_id };
607 const cpu_state = entry.value_ptr;
608 noteCpuTime(cpu_state, parsed.time_ns);
609 if (cpu_state.current_since_ns) |current_since| {
610 if (parsed.time_ns >= current_since) {
611 const duration = parsed.time_ns - current_since;
612 if (cpu_state.current_thread == 0) {
613 cpu_state.idle_ns +|= duration;
614 } else {
615 cpu_state.running_ns +|= duration;
616 }
617 }
618 }
619 cpu_state.switches += 1;
620 cpu_state.current_thread = parsed.next_thread;
621 cpu_state.current_since_ns = parsed.time_ns;
622 }
623
624 fn recordTransition(self: *Analyzer, parsed: event.Parsed) !void {
625 var label_buffer: [48]u8 = undefined;
626 const label = std.fmt.bufPrint(
627 &label_buffer,
628 "{d}->{d}",
629 .{ parsed.prev_thread, parsed.next_thread },
630 ) catch unreachable;
631 if (self.transitions.getPtr(label)) |transition| {
632 transition.count += 1;
633 noteRange(&transition.first_ns, &transition.last_ns, parsed.time_ns);
634 return;
635 }
636 const owned_label = try self.allocator.dupe(u8, label);
637 errdefer self.allocator.free(owned_label);
638 const entry = try self.transitions.getOrPut(self.allocator, owned_label);
639 std.debug.assert(!entry.found_existing);
640 entry.key_ptr.* = owned_label;
641 entry.value_ptr.* = .{
642 .label = owned_label,
643 .prev_thread = parsed.prev_thread,
644 .next_thread = parsed.next_thread,
645 .count = 1,
646 };
647 noteRange(&entry.value_ptr.first_ns, &entry.value_ptr.last_ns, parsed.time_ns);
648 }
649
650 fn recordOccurrence(self: *Analyzer, occurrence: *Occurrence) !void {
651 try self.occurrences.append(self.allocator, occurrence.*);
652 occurrence.reason = null;
653 occurrence.state = null;
654 }
655
656 fn addReasonDuration(
657 self: *Analyzer,
658 map: *std.StringHashMapUnmanaged(ReasonState),
659 label_text: ?[]const u8,
660 blocked_ns: u64,
661 ready_ns: u64,
662 count: u64,
663 thread_id: u64,
664 cpu_id: ?u32,
665 time_ns: u64,
666 ) !void {
667 const row = try self.reasonState(map, label_text);
668 row.count += count;
669 row.blocked_ns +|= blocked_ns;
670 row.ready_ns +|= ready_ns;
671 try noteReasonThreadCpu(self.allocator, row, thread_id, cpu_id);
672 noteRange(&row.first_ns, &row.last_ns, time_ns);
673 }
674
675 fn addUnknownDuration(
676 self: *Analyzer,
677 map: *std.StringHashMapUnmanaged(ReasonState),
678 label_text: ?[]const u8,
679 duration: u64,
680 thread_id: u64,
681 cpu_id: ?u32,
682 time_ns: u64,
683 ) !void {
684 const row = try self.reasonState(map, label_text);
685 row.unknown_off_cpu_ns +|= duration;
686 try noteReasonThreadCpu(self.allocator, row, thread_id, cpu_id);
687 noteRange(&row.first_ns, &row.last_ns, time_ns);
688 }
689
690 fn reasonState(
691 self: *Analyzer,
692 map: *std.StringHashMapUnmanaged(ReasonState),
693 label_text: ?[]const u8,
694 ) !*ReasonState {
695 const label = label_text orelse "<unknown>";
696 if (map.getPtr(label)) |row| return row;
697 const owned_label = try self.allocator.dupe(u8, label);
698 errdefer self.allocator.free(owned_label);
699 const entry = try map.getOrPut(self.allocator, owned_label);
700 std.debug.assert(!entry.found_existing);
701 entry.key_ptr.* = owned_label;
702 entry.value_ptr.* = .{ .label = owned_label };
703 return entry.value_ptr;
704 }
705
706 fn threadState(self: *Analyzer, id: u64) !*ThreadState {
707 const entry = try self.threads.getOrPut(self.allocator, id);
708 if (!entry.found_existing) entry.value_ptr.* = .{ .id = id };
709 return entry.value_ptr;
710 }
711
712 fn collectReasonState(
713 self: *Analyzer,
714 allocator: std.mem.Allocator,
715 summaries: *std.ArrayListUnmanaged(Summary),
716 map: std.StringHashMapUnmanaged(ReasonState),
717 group: Group,
718 options: Options,
719 ) !void {
720 var iter = map.valueIterator();
721 while (iter.next()) |row| {
722 if (!reasonStateMatches(row.*, options)) {
723 self.counters.filtered += 1;
724 continue;
725 }
726 try appendSummary(
727 allocator,
728 summaries,
729 try reasonStateSummary(allocator, row.*, group),
730 );
731 }
732 }
733
734 fn threadMatches(self: Analyzer, thread_state: ThreadState, options: Options) bool {
735 if (options.thread) |thread_filter| if (thread_state.id != thread_filter) return false;
736 if (options.cpu) |cpu_filter| {
737 const last_cpu = thread_state.last_cpu orelse return false;
738 if (last_cpu != cpu_filter) return false;
739 }
740 if (options.match) |needle| {
741 if (contains(thread_state.displayName(), needle, options.ignore_case)) return true;
742 var buffer: [32]u8 = undefined;
743 const thread_text = std.fmt.bufPrint(&buffer, "{d}", .{thread_state.id}) catch "";
744 if (contains(thread_text, needle, options.ignore_case)) return true;
745 return false;
746 }
747 _ = self;
748 return true;
749 }
750 };
751
752 pub fn deinitSummaries(allocator: std.mem.Allocator, summaries: *std.ArrayListUnmanaged(Summary)) void {
753 for (summaries.items) |*summary| summary.deinit(allocator);
754 summaries.deinit(allocator);
755 }
756
757 fn appendSummary(
758 allocator: std.mem.Allocator,
759 summaries: *std.ArrayListUnmanaged(Summary),
760 summary_value: Summary,
761 ) !void {
762 var summary = summary_value;
763 errdefer summary.deinit(allocator);
764 try summaries.append(allocator, summary);
765 }
766
767 pub fn writeTextFromJsonlPath(
768 allocator: std.mem.Allocator,
769 path: []const u8,
770 writer: *std.Io.Writer,
771 options: Options,
772 ) !void {
773 return report.writeFromJsonlPath(Analyzer, writeText, allocator, path, writer, options);
774 }
775
776 pub fn writeJsonlFromJsonlPath(
777 allocator: std.mem.Allocator,
778 path: []const u8,
779 writer: *std.Io.Writer,
780 options: Options,
781 ) !void {
782 return report.writeFromJsonlPath(Analyzer, writeJsonl, allocator, path, writer, options);
783 }
784
785 pub fn ingestPath(analyzer: *Analyzer, path: []const u8) !void {
786 return report.ingestJsonlPath(analyzer, path);
787 }
788
789 fn writeText(
790 allocator: std.mem.Allocator,
791 analyzer: *Analyzer,
792 writer: *std.Io.Writer,
793 options: Options,
794 ) !void {
795 var summaries = try analyzer.collectSummaries(allocator, options);
796 defer deinitSummaries(allocator, &summaries);
797 var occurrences = try analyzer.collectOccurrences(allocator, options);
798 defer occurrences.deinit(allocator);
799
800 try writer.print(
801 "tracy context groups={d} switches={d} wakeups={d} threads={d} cpus={d} " ++
802 "running_ns={d} blocked_ns={d} ready_ns={d} unknown_off_cpu_ns={d} " ++
803 "ready_latency_samples={d} redundant_wakeups={d} " ++
804 "ready_time_regressions={d} pending_ready_intervals={d} " ++
805 "ready_latency_evidence={s} filtered={d} duration_ns={d} group={s} sort={s}\n",
806 .{
807 summaries.items.len,
808 analyzer.counters.switches,
809 analyzer.counters.wakeups,
810 analyzer.threads.count(),
811 analyzer.cpus.count(),
812 analyzer.runningNs(),
813 analyzer.blockedNs(),
814 analyzer.readyNs(),
815 analyzer.unknownOffCpuNs(),
816 analyzer.counters.ready_latency_samples,
817 analyzer.counters.redundant_wakeups,
818 analyzer.counters.ready_time_regressions,
819 analyzer.pendingReadyIntervals(),
820 analyzer.readyLatencyEvidence(),
821 analyzer.counters.filtered,
822 analyzer.durationNs(),
823 options.group.tag(),
824 options.sort.tag(),
825 },
826 );
827 try capture_mod.writeText(writer, analyzer.captureIntegrity());
828 const summary_limit = @min(options.top, summaries.items.len);
829 for (summaries.items[0..summary_limit]) |summary| {
830 try writer.print("context group={s} label=", .{summary.group.tag()});
831 try pretty_json.writeString(writer, summary.label);
832 try writer.print(" count={d}", .{summary.count});
833 try writeSummaryFieldsText(writer, summary);
834 try writer.writeByte('\n');
835 }
836
837 const occurrence_limit = @min(options.occurrences, occurrences.items.len);
838 for (occurrences.items[0..occurrence_limit]) |occurrence| {
839 try writer.print("context-occurrence kind={s} time_ns={d} source_thread={d}", .{ occurrence.kind.tag(), occurrence.time_ns, occurrence.source_thread });
840 try writeOccurrenceFieldsText(writer, occurrence);
841 try writer.writeByte('\n');
842 }
843 }
844
845 fn writeJsonl(
846 allocator: std.mem.Allocator,
847 analyzer: *Analyzer,
848 writer: *std.Io.Writer,
849 options: Options,
850 ) !void {
851 var summaries = try analyzer.collectSummaries(allocator, options);
852 defer deinitSummaries(allocator, &summaries);
853 var occurrences = try analyzer.collectOccurrences(allocator, options);
854 defer occurrences.deinit(allocator);
855
856 var summary_stream = pretty_json.Writer.init(writer, .minified);
857 const summary_record = try summary_stream.object();
858 try summary_record.field("schema", schema);
859 try summary_record.field("kind", "summary");
860 try summary_record.field("groups", summaries.items.len);
861 try summary_record.field("switches", analyzer.counters.switches);
862 try summary_record.field("wakeups", analyzer.counters.wakeups);
863 try summary_record.field("threads", analyzer.threads.count());
864 try summary_record.field("cpus", analyzer.cpus.count());
865 try summary_record.field("running_ns", analyzer.runningNs());
866 try summary_record.field("blocked_ns", analyzer.blockedNs());
867 try summary_record.field("ready_ns", analyzer.readyNs());
868 try summary_record.field("unknown_off_cpu_ns", analyzer.unknownOffCpuNs());
869 try summary_record.field("ready_latency_samples", analyzer.counters.ready_latency_samples);
870 try summary_record.field("redundant_wakeups", analyzer.counters.redundant_wakeups);
871 try summary_record.field("ready_time_regressions", analyzer.counters.ready_time_regressions);
872 try summary_record.field("pending_ready_intervals", analyzer.pendingReadyIntervals());
873 try summary_record.field("filtered", analyzer.counters.filtered);
874 try summary_record.field("duration_ns", analyzer.durationNs());
875 try summary_record.field("group", options.group.tag());
876 try summary_record.field("sort", options.sort.tag());
877 try summary_record.field("ready_latency_evidence", analyzer.readyLatencyEvidence());
878 try capture_mod.writeFields(summary_record, analyzer.captureIntegrity());
879 try summary_record.endLine();
880
881 const summary_limit = @min(options.top, summaries.items.len);
882 for (summaries.items[0..summary_limit]) |summary| {
883 var stream = pretty_json.Writer.init(writer, .minified);
884 const object = try stream.object();
885 try object.field("schema", schema);
886 try object.field("kind", "group");
887 try object.field("group", summary.group.tag());
888 try object.field("label", summary.label);
889 try object.field("count", summary.count);
890 try writeSummaryFields(object, summary);
891 try object.endLine();
892 }
893
894 const occurrence_limit = @min(options.occurrences, occurrences.items.len);
895 for (occurrences.items[0..occurrence_limit]) |occurrence| {
896 var stream = pretty_json.Writer.init(writer, .minified);
897 const object = try stream.object();
898 try object.field("schema", schema);
899 try object.field("kind", occurrence.kind.tag());
900 try object.field("time_ns", occurrence.time_ns);
901 try object.field("source_thread", occurrence.source_thread);
902 try writeOccurrenceFields(object, occurrence);
903 try object.endLine();
904 }
905 }
906
907 fn threadSummary(allocator: std.mem.Allocator, thread_state: ThreadState) !Summary {
908 std.mem.sort(u64, thread_state.ready_latencies.items, {}, std.sort.asc(u64));
909 const latency_samples: u64 = @intCast(thread_state.ready_latencies.items.len);
910 var id_buffer: [20]u8 = undefined;
911 const id = std.fmt.bufPrint(&id_buffer, "{d}", .{thread_state.id}) catch unreachable;
912 const name = thread_state.name orelse "";
913 const separator = if (name.len == 0) "" else " ";
914 const label = try std.mem.concat(allocator, u8, &.{ "thread ", id, separator, name });
915 return .{
916 .group = .thread,
917 .label = label,
918 .count = thread_state.switch_ins + thread_state.switch_outs + thread_state.wakeups,
919 .thread = thread_state.id,
920 .cpu = thread_state.last_cpu,
921 .switch_ins = thread_state.switch_ins,
922 .switch_outs = thread_state.switch_outs,
923 .wakeups = thread_state.wakeups,
924 .running_ns = thread_state.running_ns,
925 .blocked_ns = thread_state.blocked_ns,
926 .ready_ns = thread_state.ready_ns,
927 .ready_latency_samples = latency_samples,
928 .ready_latency_mean_ns = if (latency_samples == 0)
929 0
930 else
931 thread_state.ready_ns / latency_samples,
932 .ready_latency_min_ns = readyLatencyPercentile(thread_state.ready_latencies.items, 0),
933 .ready_latency_p50_ns = readyLatencyPercentile(thread_state.ready_latencies.items, 50),
934 .ready_latency_p90_ns = readyLatencyPercentile(thread_state.ready_latencies.items, 90),
935 .ready_latency_p99_ns = readyLatencyPercentile(thread_state.ready_latencies.items, 99),
936 .ready_latency_max_ns = readyLatencyPercentile(thread_state.ready_latencies.items, 100),
937 .unknown_off_cpu_ns = thread_state.unknown_off_cpu_ns,
938 .migrations = thread_state.migrations,
939 .first_ns = thread_state.first_ns,
940 .last_ns = thread_state.last_ns,
941 };
942 }
943
944 fn cpuSummary(allocator: std.mem.Allocator, cpu_state: CpuState) !Summary {
945 const label = try std.fmt.allocPrint(allocator, "cpu {d}", .{cpu_state.id});
946 return .{
947 .group = .cpu,
948 .label = label,
949 .count = cpu_state.switches,
950 .cpu = cpu_state.id,
951 .running_ns = cpu_state.running_ns,
952 .idle_ns = cpu_state.idle_ns,
953 .first_ns = cpu_state.first_ns,
954 .last_ns = cpu_state.last_ns,
955 };
956 }
957
958 fn reasonStateSummary(allocator: std.mem.Allocator, row: ReasonState, group: Group) !Summary {
959 return .{
960 .group = group,
961 .label = try allocator.dupe(u8, row.label),
962 .count = row.count,
963 .blocked_ns = row.blocked_ns,
964 .ready_ns = row.ready_ns,
965 .unknown_off_cpu_ns = row.unknown_off_cpu_ns,
966 .first_ns = row.first_ns,
967 .last_ns = row.last_ns,
968 };
969 }
970
971 fn transitionSummary(allocator: std.mem.Allocator, transition: TransitionState) !Summary {
972 return .{
973 .group = .transition,
974 .label = try allocator.dupe(u8, transition.label),
975 .count = transition.count,
976 .prev_thread = transition.prev_thread,
977 .next_thread = transition.next_thread,
978 .first_ns = transition.first_ns,
979 .last_ns = transition.last_ns,
980 };
981 }
982
983 fn occurrenceSummary(allocator: std.mem.Allocator, occurrence: Occurrence) !Summary {
984 var label = std.Io.Writer.Allocating.init(allocator);
985 defer label.deinit();
986 try label.writer.print("{s} {d}", .{ occurrence.kind.tag(), occurrence.time_ns });
987 var summary: Summary = .{
988 .group = .none,
989 .label = try allocator.dupe(u8, label.written()),
990 .count = 1,
991 .thread = occurrence.target_thread,
992 .cpu = occurrence.cpu,
993 .prev_thread = occurrence.prev_thread,
994 .next_thread = occurrence.next_thread,
995 .first_ns = occurrence.time_ns,
996 .last_ns = occurrence.time_ns,
997 };
998 if (occurrence.ready_latency_ns) |latency_ns| {
999 summary.ready_ns = latency_ns;
1000 summary.ready_latency_samples = 1;
1001 summary.ready_latency_mean_ns = latency_ns;
1002 summary.ready_latency_min_ns = latency_ns;
1003 summary.ready_latency_p50_ns = latency_ns;
1004 summary.ready_latency_p90_ns = latency_ns;
1005 summary.ready_latency_p99_ns = latency_ns;
1006 summary.ready_latency_max_ns = latency_ns;
1007 }
1008 return summary;
1009 }
1010
1011 fn transitionMatches(transition: TransitionState, options: Options) bool {
1012 if (options.thread) |thread| {
1013 if (transition.prev_thread != thread and transition.next_thread != thread) return false;
1014 }
1015 if (!labelMatches(transition.label, options)) return false;
1016 return true;
1017 }
1018
1019 fn occurrenceMatches(occurrence: Occurrence, options: Options) bool {
1020 if (options.thread) |thread| {
1021 if (occurrence.source_thread != thread and occurrence.prev_thread != thread and occurrence.next_thread != thread and occurrence.target_thread != thread) return false;
1022 }
1023 if (options.cpu) |cpu_filter| {
1024 const cpu = occurrence.cpu orelse return false;
1025 if (cpu != cpu_filter) return false;
1026 }
1027 if (options.since_ns) |since_ns| if (occurrence.time_ns < since_ns) return false;
1028 if (options.until_ns) |until_ns| if (occurrence.time_ns > until_ns) return false;
1029 if (options.match) |needle| {
1030 if (contains(occurrence.kind.tag(), needle, options.ignore_case)) return true;
1031 if (occurrence.reason) |reason| if (contains(reason, needle, options.ignore_case)) return true;
1032 if (occurrence.state) |state| if (contains(state, needle, options.ignore_case)) return true;
1033 return false;
1034 }
1035 return true;
1036 }
1037
1038 fn labelMatches(label: []const u8, options: Options) bool {
1039 if (options.match) |needle| return contains(label, needle, options.ignore_case);
1040 return true;
1041 }
1042
1043 fn reasonStateMatches(row: ReasonState, options: Options) bool {
1044 if (options.thread) |thread| {
1045 if (!row.threads.contains(thread)) return false;
1046 }
1047 if (options.cpu) |cpu| {
1048 if (!row.cpus.contains(cpu)) return false;
1049 }
1050 return labelMatches(row.label, options);
1051 }
1052
1053 fn writeSummaryFieldsText(writer: *std.Io.Writer, summary: Summary) !void {
1054 if (summary.thread) |thread| try writer.print(" thread={d}", .{thread});
1055 if (summary.cpu) |cpu| try writer.print(" cpu={d}", .{cpu});
1056 if (summary.prev_thread != 0 or summary.next_thread != 0) try writer.print(" prev_thread={d} next_thread={d}", .{ summary.prev_thread, summary.next_thread });
1057 if (summary.switch_ins != 0) try writer.print(" switches_in={d}", .{summary.switch_ins});
1058 if (summary.switch_outs != 0) try writer.print(" switches_out={d}", .{summary.switch_outs});
1059 if (summary.wakeups != 0) try writer.print(" wakeups={d}", .{summary.wakeups});
1060 if (summary.running_ns != 0) try writer.print(" running_ns={d}", .{summary.running_ns});
1061 if (summary.blocked_ns != 0) try writer.print(" blocked_ns={d}", .{summary.blocked_ns});
1062 if (summary.ready_ns != 0) try writer.print(" ready_ns={d}", .{summary.ready_ns});
1063 try writeReadyLatencyText(writer, summary);
1064 if (summary.unknown_off_cpu_ns != 0) try writer.print(" unknown_off_cpu_ns={d}", .{summary.unknown_off_cpu_ns});
1065 if (summary.idle_ns != 0) try writer.print(" idle_ns={d}", .{summary.idle_ns});
1066 if (summary.migrations != 0) try writer.print(" migrations={d}", .{summary.migrations});
1067 if (summary.first_ns != 0) try writer.print(" first_ns={d}", .{summary.first_ns});
1068 if (summary.last_ns != 0) try writer.print(" last_ns={d}", .{summary.last_ns});
1069 }
1070
1071 fn writeSummaryFields(object: pretty_json.Object, summary: Summary) !void {
1072 if (summary.thread) |thread| try object.field("thread", thread);
1073 if (summary.cpu) |cpu| try object.field("cpu", cpu);
1074 if (summary.prev_thread != 0 or summary.next_thread != 0) {
1075 try object.field("prev_thread", summary.prev_thread);
1076 try object.field("next_thread", summary.next_thread);
1077 }
1078 if (summary.switch_ins != 0) try object.field("switches_in", summary.switch_ins);
1079 if (summary.switch_outs != 0) try object.field("switches_out", summary.switch_outs);
1080 if (summary.wakeups != 0) try object.field("wakeups", summary.wakeups);
1081 if (summary.running_ns != 0) try object.field("running_ns", summary.running_ns);
1082 if (summary.blocked_ns != 0) try object.field("blocked_ns", summary.blocked_ns);
1083 if (summary.ready_ns != 0) try object.field("ready_ns", summary.ready_ns);
1084 try writeReadyLatencyFields(object, summary);
1085 if (summary.unknown_off_cpu_ns != 0) try object.field("unknown_off_cpu_ns", summary.unknown_off_cpu_ns);
1086 if (summary.idle_ns != 0) try object.field("idle_ns", summary.idle_ns);
1087 if (summary.migrations != 0) try object.field("migrations", summary.migrations);
1088 if (summary.first_ns != 0) try object.field("first_ns", summary.first_ns);
1089 if (summary.last_ns != 0) try object.field("last_ns", summary.last_ns);
1090 }
1091
1092 fn writeReadyLatencyText(writer: *std.Io.Writer, summary: Summary) !void {
1093 if (summary.ready_latency_samples == 0) return;
1094 try writer.print(
1095 " ready_latency_samples={d} ready_latency_mean_ns={d} " ++
1096 "ready_latency_min_ns={d} ready_latency_p50_ns={d} " ++
1097 "ready_latency_p90_ns={d} ready_latency_p99_ns={d} " ++
1098 "ready_latency_max_ns={d}",
1099 .{
1100 summary.ready_latency_samples,
1101 summary.ready_latency_mean_ns,
1102 summary.ready_latency_min_ns,
1103 summary.ready_latency_p50_ns,
1104 summary.ready_latency_p90_ns,
1105 summary.ready_latency_p99_ns,
1106 summary.ready_latency_max_ns,
1107 },
1108 );
1109 }
1110
1111 fn writeReadyLatencyFields(object: pretty_json.Object, summary: Summary) !void {
1112 if (summary.ready_latency_samples == 0) return;
1113 try object.field("ready_latency_samples", summary.ready_latency_samples);
1114 try object.field("ready_latency_mean_ns", summary.ready_latency_mean_ns);
1115 try object.field("ready_latency_min_ns", summary.ready_latency_min_ns);
1116 try object.field("ready_latency_p50_ns", summary.ready_latency_p50_ns);
1117 try object.field("ready_latency_p90_ns", summary.ready_latency_p90_ns);
1118 try object.field("ready_latency_p99_ns", summary.ready_latency_p99_ns);
1119 try object.field("ready_latency_max_ns", summary.ready_latency_max_ns);
1120 }
1121
1122 fn writeOccurrenceFieldsText(writer: *std.Io.Writer, occurrence: Occurrence) !void {
1123 if (occurrence.cpu) |cpu| try writer.print(" cpu={d}", .{cpu});
1124 if (occurrence.prev_thread != 0) try writer.print(" prev_thread={d}", .{occurrence.prev_thread});
1125 if (occurrence.next_thread != 0) try writer.print(" next_thread={d}", .{occurrence.next_thread});
1126 if (occurrence.target_thread != 0) try writer.print(" target_thread={d}", .{occurrence.target_thread});
1127 if (occurrence.reason) |reason| {
1128 try writer.writeAll(" reason=");
1129 try pretty_json.writeString(writer, reason);
1130 }
1131 if (occurrence.state) |state| {
1132 try writer.writeAll(" state=");
1133 try pretty_json.writeString(writer, state);
1134 }
1135 if (occurrence.previous_cstate) |previous_cstate| try writer.print(" previous_cstate={d}", .{previous_cstate});
1136 if (occurrence.prev_priority) |prev_priority| try writer.print(" prev_priority={d}", .{prev_priority});
1137 if (occurrence.next_priority) |next_priority| try writer.print(" next_priority={d}", .{next_priority});
1138 if (occurrence.priority_increment) |priority_increment| try writer.print(" priority_increment={d}", .{priority_increment});
1139 if (occurrence.ready_latency_ns) |latency_ns| {
1140 try writer.print(" ready_latency_ns={d}", .{latency_ns});
1141 }
1142 }
1143
1144 fn writeOccurrenceFields(object: pretty_json.Object, occurrence: Occurrence) !void {
1145 if (occurrence.cpu) |cpu| try object.field("cpu", cpu);
1146 if (occurrence.prev_thread != 0) try object.field("prev_thread", occurrence.prev_thread);
1147 if (occurrence.next_thread != 0) try object.field("next_thread", occurrence.next_thread);
1148 if (occurrence.target_thread != 0) try object.field("target_thread", occurrence.target_thread);
1149 if (occurrence.reason) |reason| try object.field("context_reason", reason);
1150 if (occurrence.state) |state| try object.field("context_state", state);
1151 if (occurrence.previous_cstate) |previous_cstate| try object.field("previous_cstate", previous_cstate);
1152 if (occurrence.prev_priority) |prev_priority| try object.field("prev_priority", prev_priority);
1153 if (occurrence.next_priority) |next_priority| try object.field("next_priority", next_priority);
1154 if (occurrence.priority_increment) |priority_increment| try object.field("priority_increment", priority_increment);
1155 if (occurrence.ready_latency_ns) |latency_ns| try object.field("ready_latency_ns", latency_ns);
1156 }
1157
1158 fn noteTime(thread: *ThreadState, time_ns: u64) void {
1159 noteRange(&thread.first_ns, &thread.last_ns, time_ns);
1160 }
1161
1162 fn noteCpuTime(cpu: *CpuState, time_ns: u64) void {
1163 noteRange(&cpu.first_ns, &cpu.last_ns, time_ns);
1164 }
1165
1166 fn noteRange(first_ns: *u64, last_ns: *u64, time_ns: u64) void {
1167 if (time_ns == 0) return;
1168 if (first_ns.* == 0 or time_ns < first_ns.*) first_ns.* = time_ns;
1169 last_ns.* = @max(last_ns.*, time_ns);
1170 }
1171
1172 fn noteReasonThreadCpu(allocator: std.mem.Allocator, row: *ReasonState, thread_id: u64, cpu_id: ?u32) !void {
1173 if (thread_id != 0) try row.threads.put(allocator, thread_id, {});
1174 if (cpu_id) |cpu| try row.cpus.put(allocator, cpu, {});
1175 }
1176
1177 fn dupeOptional(allocator: std.mem.Allocator, text: ?[]const u8) !?[]u8 {
1178 const actual = text orelse return null;
1179 return try allocator.dupe(u8, actual);
1180 }
1181
1182 fn replaceOptional(allocator: std.mem.Allocator, slot: *?[]u8, text: ?[]const u8) !void {
1183 clearOptional(allocator, slot);
1184 slot.* = try dupeOptional(allocator, text);
1185 }
1186
1187 fn clearOptional(allocator: std.mem.Allocator, slot: *?[]u8) void {
1188 if (slot.*) |text| allocator.free(text);
1189 slot.* = null;
1190 }
1191
1192 fn sortSummaries(items: []Summary, sort: Sort) void {
1193 std.mem.sort(Summary, items, sort, summaryLessThan);
1194 }
1195
1196 fn summaryLessThan(sort: Sort, left: Summary, right: Summary) bool {
1197 return switch (sort) {
1198 .running => summaryRunningGreaterThan({}, left, right),
1199 .blocked => summaryBlockedGreaterThan({}, left, right),
1200 .ready => summaryReadyGreaterThan({}, left, right),
1201 .latency => summaryLatencyGreaterThan({}, left, right),
1202 .switches => summarySwitchesGreaterThan({}, left, right),
1203 .wakeups => summaryWakeupsGreaterThan({}, left, right),
1204 .migrations => summaryMigrationsGreaterThan({}, left, right),
1205 .last => summaryLastGreaterThan({}, left, right),
1206 .thread => summaryThreadLessThan({}, left, right),
1207 .cpu => summaryCpuLessThan({}, left, right),
1208 .label => summaryLabelLessThan({}, left, right),
1209 };
1210 }
1211
1212 fn summaryRunningGreaterThan(_: void, left: Summary, right: Summary) bool {
1213 if (left.running_ns != right.running_ns) return left.running_ns > right.running_ns;
1214 return summarySwitchesGreaterThan({}, left, right);
1215 }
1216
1217 fn summaryBlockedGreaterThan(_: void, left: Summary, right: Summary) bool {
1218 if (left.blocked_ns != right.blocked_ns) return left.blocked_ns > right.blocked_ns;
1219 return summarySwitchesGreaterThan({}, left, right);
1220 }
1221
1222 fn summaryReadyGreaterThan(_: void, left: Summary, right: Summary) bool {
1223 if (left.ready_ns != right.ready_ns) return left.ready_ns > right.ready_ns;
1224 return summarySwitchesGreaterThan({}, left, right);
1225 }
1226
1227 fn summaryLatencyGreaterThan(_: void, left: Summary, right: Summary) bool {
1228 if (left.ready_latency_max_ns != right.ready_latency_max_ns) {
1229 return left.ready_latency_max_ns > right.ready_latency_max_ns;
1230 }
1231 if (left.ready_latency_p99_ns != right.ready_latency_p99_ns) {
1232 return left.ready_latency_p99_ns > right.ready_latency_p99_ns;
1233 }
1234 return summarySwitchesGreaterThan({}, left, right);
1235 }
1236
1237 fn summarySwitchesGreaterThan(_: void, left: Summary, right: Summary) bool {
1238 const left_switches = left.switch_ins + left.switch_outs + left.count;
1239 const right_switches = right.switch_ins + right.switch_outs + right.count;
1240 if (left_switches != right_switches) return left_switches > right_switches;
1241 return std.mem.lessThan(u8, left.label, right.label);
1242 }
1243
1244 fn summaryWakeupsGreaterThan(_: void, left: Summary, right: Summary) bool {
1245 if (left.wakeups != right.wakeups) return left.wakeups > right.wakeups;
1246 return summarySwitchesGreaterThan({}, left, right);
1247 }
1248
1249 fn summaryMigrationsGreaterThan(_: void, left: Summary, right: Summary) bool {
1250 if (left.migrations != right.migrations) return left.migrations > right.migrations;
1251 return summarySwitchesGreaterThan({}, left, right);
1252 }
1253
1254 fn summaryLastGreaterThan(_: void, left: Summary, right: Summary) bool {
1255 if (left.last_ns != right.last_ns) return left.last_ns > right.last_ns;
1256 return summarySwitchesGreaterThan({}, left, right);
1257 }
1258
1259 fn summaryThreadLessThan(_: void, left: Summary, right: Summary) bool {
1260 const left_thread = left.thread orelse 0;
1261 const right_thread = right.thread orelse 0;
1262 if (left_thread != right_thread) return left_thread < right_thread;
1263 return summarySwitchesGreaterThan({}, left, right);
1264 }
1265
1266 fn summaryCpuLessThan(_: void, left: Summary, right: Summary) bool {
1267 const left_cpu = left.cpu orelse 0;
1268 const right_cpu = right.cpu orelse 0;
1269 if (left_cpu != right_cpu) return left_cpu < right_cpu;
1270 return summarySwitchesGreaterThan({}, left, right);
1271 }
1272
1273 fn summaryLabelLessThan(_: void, left: Summary, right: Summary) bool {
1274 return std.mem.lessThan(u8, left.label, right.label);
1275 }
1276
1277 fn sortOccurrences(items: []Occurrence, sort: Sort) void {
1278 switch (sort) {
1279 .latency => std.mem.sort(Occurrence, items, {}, occurrenceLatencyGreaterThan),
1280 .last => std.mem.sort(Occurrence, items, {}, occurrenceTimeGreaterThan),
1281 else => std.mem.sort(Occurrence, items, {}, occurrenceTimeLessThan),
1282 }
1283 }
1284
1285 fn occurrenceLatencyGreaterThan(_: void, left: Occurrence, right: Occurrence) bool {
1286 const left_latency = left.ready_latency_ns orelse 0;
1287 const right_latency = right.ready_latency_ns orelse 0;
1288 if (left_latency != right_latency) return left_latency > right_latency;
1289 return occurrenceTimeLessThan({}, left, right);
1290 }
1291
1292 fn occurrenceTimeLessThan(_: void, left: Occurrence, right: Occurrence) bool {
1293 if (left.time_ns != right.time_ns) return left.time_ns < right.time_ns;
1294 return left.seq < right.seq;
1295 }
1296
1297 fn occurrenceTimeGreaterThan(_: void, left: Occurrence, right: Occurrence) bool {
1298 if (left.time_ns != right.time_ns) return left.time_ns > right.time_ns;
1299 return left.seq > right.seq;
1300 }
1301
1302 fn contextStateReady(state: ?[]const u8) bool {
1303 const text = state orelse return false;
1304 inline for (.{ "ready", "runnable", "r", "r+" }) |ready| {
1305 if (asciiEqlIgnoreCase(text, ready)) return true;
1306 }
1307 return false;
1308 }
1309
1310 fn readyLatencyPercentile(sorted: []const u64, percent: u64) u64 {
1311 if (sorted.len == 0) return 0;
1312 const rank: usize = @intCast((@as(u128, @min(percent, 100)) * sorted.len + 99) / 100);
1313 const index = @min(@max(rank, 1) - 1, sorted.len - 1);
1314 return sorted[index];
1315 }
1316
1317 fn contains(haystack: []const u8, needle: []const u8, ignore_case: bool) bool {
1318 if (!ignore_case) return std.mem.indexOf(u8, haystack, needle) != null;
1319 if (needle.len == 0) return true;
1320 if (needle.len > haystack.len) return false;
1321 var index: usize = 0;
1322 while (index + needle.len <= haystack.len) : (index += 1) {
1323 if (asciiEqlIgnoreCase(haystack[index .. index + needle.len], needle)) return true;
1324 }
1325 return false;
1326 }
1327
1328 fn asciiEqlIgnoreCase(left: []const u8, right: []const u8) bool {
1329 if (left.len != right.len) return false;
1330 for (left, right) |a, b| {
1331 if (std.ascii.toLower(a) != std.ascii.toLower(b)) return false;
1332 }
1333 return true;
1334 }
1335
1336 test "context analyzer preserves preempted ready latency" {
1337 var trace = std.Io.Writer.Allocating.init(std.testing.allocator);
1338 defer trace.deinit();
1339 try (event.TraceEvent{ .seq = 1, .kind = .start, .time_ns = 90, .thread = 1, .name = "test" }).writeJsonLine(&trace.writer);
1340 try (event.TraceEvent{ .seq = 2, .kind = .thread_name, .time_ns = 95, .thread = 10, .name = "worker" }).writeJsonLine(&trace.writer);
1341 try (event.TraceEvent{ .seq = 3, .kind = .context_switch, .time_ns = 100, .thread = 1, .cpu = 0, .next_thread = 10 }).writeJsonLine(&trace.writer);
1342 try (event.TraceEvent{ .seq = 4, .kind = .context_switch, .time_ns = 150, .thread = 1, .cpu = 0, .prev_thread = 10, .next_thread = 11, .context_reason = "WrPreempted", .context_state = "Ready" }).writeJsonLine(&trace.writer);
1343 try (event.TraceEvent{ .seq = 5, .kind = .thread_wakeup, .time_ns = 180, .thread = 1, .target_thread = 10, .cpu = 0, .context_reason = "Unwait" }).writeJsonLine(&trace.writer);
1344 try (event.TraceEvent{ .seq = 6, .kind = .context_switch, .time_ns = 220, .thread = 1, .cpu = 1, .prev_thread = 11, .next_thread = 10, .context_reason = "WrYieldExecution", .context_state = "Ready" }).writeJsonLine(&trace.writer);
1345 try (event.TraceEvent{ .seq = 7, .kind = .stop, .time_ns = 260, .thread = 1 }).writeJsonLine(&trace.writer);
1346
1347 var analyzer = Analyzer.init(std.testing.allocator);
1348 defer analyzer.deinit();
1349 try analyzer.ingestJsonlBytes(trace.written());
1350 try std.testing.expectEqual(@as(u64, 3), analyzer.counters.switches);
1351 try std.testing.expectEqual(@as(u64, 1), analyzer.counters.wakeups);
1352 try std.testing.expectEqual(@as(u64, 50), analyzer.threads.get(10).?.running_ns);
1353 try std.testing.expectEqual(@as(u64, 0), analyzer.threads.get(10).?.blocked_ns);
1354 try std.testing.expectEqual(@as(u64, 70), analyzer.threads.get(10).?.ready_ns);
1355 try std.testing.expectEqual(@as(u64, 1), analyzer.counters.ready_latency_samples);
1356 try std.testing.expectEqual(@as(u64, 1), analyzer.counters.redundant_wakeups);
1357 try std.testing.expectEqual(@as(u64, 1), analyzer.threads.get(10).?.migrations);
1358
1359 var out = std.Io.Writer.Allocating.init(std.testing.allocator);
1360 defer out.deinit();
1361 try writeText(std.testing.allocator, &analyzer, &out.writer, .{ .group = .thread, .sort = .running, .top = 4, .occurrences = 4 });
1362 const text = out.written();
1363 try expectContextContains(
1364 text,
1365 "ready_latency_samples=1 redundant_wakeups=1 ready_time_regressions=0 " ++
1366 "pending_ready_intervals=1 ready_latency_evidence=partial",
1367 );
1368 try expectContextContains(
1369 text,
1370 "context group=thread label=\"thread 10 worker\" count=4 thread=10 cpu=1 " ++
1371 "switches_in=2 switches_out=1 wakeups=1 running_ns=50 ready_ns=70 " ++
1372 "ready_latency_samples=1 ready_latency_mean_ns=70 ready_latency_min_ns=70 " ++
1373 "ready_latency_p50_ns=70 ready_latency_p90_ns=70 ready_latency_p99_ns=70 " ++
1374 "ready_latency_max_ns=70 migrations=1",
1375 );
1376 try expectContextContains(
1377 text,
1378 "context-occurrence kind=switch time_ns=220 source_thread=1 cpu=1 " ++
1379 "prev_thread=11 next_thread=10 reason=\"WrYieldExecution\" state=\"Ready\" " ++
1380 "ready_latency_ns=70",
1381 );
1382 try std.testing.expect(std.mem.indexOf(u8, text, "context-occurrence kind=switch time_ns=150 source_thread=1 cpu=0 prev_thread=10 next_thread=11 reason=\"WrPreempted\" state=\"Ready\"") != null);
1383 }
1384
1385 test "context jsonl filters by cpu reason and thread" {
1386 var trace = std.Io.Writer.Allocating.init(std.testing.allocator);
1387 defer trace.deinit();
1388 try (event.TraceEvent{ .seq = 1, .kind = .context_switch, .time_ns = 100, .thread = 1, .cpu = 0, .next_thread = 10 }).writeJsonLine(&trace.writer);
1389 try (event.TraceEvent{ .seq = 2, .kind = .context_switch, .time_ns = 140, .thread = 1, .cpu = 0, .prev_thread = 10, .next_thread = 11, .context_reason = "WrMutex", .context_state = "Waiting" }).writeJsonLine(&trace.writer);
1390 try (event.TraceEvent{ .seq = 3, .kind = .thread_wakeup, .time_ns = 160, .thread = 1, .target_thread = 10, .cpu = 0 }).writeJsonLine(&trace.writer);
1391 try (event.TraceEvent{ .seq = 4, .kind = .context_switch, .time_ns = 180, .thread = 1, .cpu = 0, .prev_thread = 11, .next_thread = 10 }).writeJsonLine(&trace.writer);
1392
1393 var analyzer = Analyzer.init(std.testing.allocator);
1394 defer analyzer.deinit();
1395 try analyzer.ingestJsonlBytes(trace.written());
1396
1397 var out = std.Io.Writer.Allocating.init(std.testing.allocator);
1398 defer out.deinit();
1399 try writeJsonl(std.testing.allocator, &analyzer, &out.writer, .{ .group = .reason, .cpu = 0, .thread = 10, .match = "wrmutex", .ignore_case = true });
1400 const text = out.written();
1401 try std.testing.expect(std.mem.indexOf(u8, text, "\"schema\":\"tracy.context/v0\"") != null);
1402 try std.testing.expect(std.mem.indexOf(u8, text, "\"kind\":\"summary\",\"groups\":1") != null);
1403 try std.testing.expect(std.mem.indexOf(u8, text, "\"label\":\"WrMutex\"") != null);
1404 try std.testing.expect(std.mem.indexOf(u8, text, "\"blocked_ns\":20") != null);
1405 }
1406
1407 test "context reports ready latency distributions and worst occurrences" {
1408 var trace = std.Io.Writer.Allocating.init(std.testing.allocator);
1409 defer trace.deinit();
1410 var seq: u64 = 1;
1411 try (event.TraceEvent{ .seq = seq, .kind = .start, .time_ns = 1 }).writeJsonLine(&trace.writer);
1412 seq += 1;
1413 var time_ns: u64 = 100;
1414 for ([_]u64{ 10, 20, 30, 40, 100 }) |latency_ns| {
1415 try appendReadyLatencySample(&trace.writer, &seq, 10, time_ns, latency_ns);
1416 time_ns += latency_ns + 10;
1417 }
1418 try appendReadyLatencySample(&trace.writer, &seq, 20, time_ns, 200);
1419 try (event.TraceEvent{ .seq = seq, .kind = .stop, .time_ns = time_ns + 210 })
1420 .writeJsonLine(&trace.writer);
1421
1422 var analyzer = Analyzer.init(std.testing.allocator);
1423 defer analyzer.deinit();
1424 try analyzer.ingestJsonlBytes(trace.written());
1425 try std.testing.expectEqual(@as(u64, 6), analyzer.counters.ready_latency_samples);
1426 try std.testing.expectEqualStrings("complete", analyzer.readyLatencyEvidence());
1427
1428 var rows = try analyzer.collectSummaries(std.testing.allocator, .{ .sort = .latency });
1429 defer deinitSummaries(std.testing.allocator, &rows);
1430 try std.testing.expectEqual(@as(?u64, 20), rows.items[0].thread);
1431 const worker = rows.items[1];
1432 try std.testing.expectEqual(@as(?u64, 10), worker.thread);
1433 try std.testing.expectEqual(@as(u64, 5), worker.ready_latency_samples);
1434 try std.testing.expectEqual(@as(u64, 40), worker.ready_latency_mean_ns);
1435 try std.testing.expectEqual(@as(u64, 10), worker.ready_latency_min_ns);
1436 try std.testing.expectEqual(@as(u64, 30), worker.ready_latency_p50_ns);
1437 try std.testing.expectEqual(@as(u64, 100), worker.ready_latency_p90_ns);
1438 try std.testing.expectEqual(@as(u64, 100), worker.ready_latency_p99_ns);
1439 try std.testing.expectEqual(@as(u64, 100), worker.ready_latency_max_ns);
1440
1441 var out = std.Io.Writer.Allocating.init(std.testing.allocator);
1442 defer out.deinit();
1443 try writeJsonl(
1444 std.testing.allocator,
1445 &analyzer,
1446 &out.writer,
1447 .{ .sort = .latency, .top = 3, .occurrences = 1 },
1448 );
1449 try expectContextContains(out.written(), "\"ready_latency_evidence\":\"complete\"");
1450 try expectContextContains(out.written(), "\"ready_latency_p99_ns\":100");
1451 try expectContextContains(out.written(), "\"ready_latency_ns\":200");
1452 }
1453
1454 test "context retains flight reports and invalid ready evidence" {
1455 var trace = std.Io.Writer.Allocating.init(std.testing.allocator);
1456 defer trace.deinit();
1457 try (event.TraceEvent{ .seq = 1, .kind = .start, .time_ns = 10 })
1458 .writeJsonLine(&trace.writer);
1459 try (event.TraceEvent{
1460 .seq = 3,
1461 .kind = .thread_wakeup,
1462 .time_ns = 100,
1463 .target_thread = 10,
1464 }).writeJsonLine(&trace.writer);
1465 try (event.TraceEvent{
1466 .seq = 4,
1467 .kind = .context_switch,
1468 .time_ns = 90,
1469 .next_thread = 10,
1470 }).writeJsonLine(&trace.writer);
1471 try (event.TraceEvent{ .seq = 5, .kind = .stop, .time_ns = 110 })
1472 .writeJsonLine(&trace.writer);
1473 const flight_report = contextTestFlightReport();
1474 try flight_report.writeJsonl(&trace.writer);
1475
1476 var analyzer = Analyzer.init(std.testing.allocator);
1477 defer analyzer.deinit();
1478 try analyzer.ingestJsonlBytes(trace.written());
1479 try std.testing.expectEqual(@as(u64, 1), analyzer.counters.ready_time_regressions);
1480 try std.testing.expectEqualStrings("partial", analyzer.readyLatencyEvidence());
1481 const integrity = analyzer.captureIntegrity();
1482 try std.testing.expectEqualStrings("sequence_gaps", integrity.status);
1483 try std.testing.expectEqualDeep(flight_report, integrity.flight_report.?);
1484
1485 var out = std.Io.Writer.Allocating.init(std.testing.allocator);
1486 defer out.deinit();
1487 try writeJsonl(std.testing.allocator, &analyzer, &out.writer, .{});
1488 try expectContextContains(out.written(), "\"ready_time_regressions\":1");
1489 try expectContextContains(out.written(), "\"flight_report\":{");
1490 }
1491
1492 test "context releases ready latency evidence on allocation failure" {
1493 try std.testing.checkAllAllocationFailures(
1494 std.testing.allocator,
1495 analyzeReadyLatency,
1496 .{},
1497 );
1498 }
1499
1500 fn analyzeReadyLatency(allocator: std.mem.Allocator) !void {
1501 const trace =
1502 "{\"v\":0,\"seq\":1,\"kind\":\"start\",\"time_ns\":10}\n" ++
1503 "{\"v\":0,\"seq\":2,\"kind\":\"context.switch\",\"time_ns\":20," ++
1504 "\"prev_thread\":7,\"next_thread\":8,\"context_reason\":\"preempted\"," ++
1505 "\"context_state\":\"Ready\"}\n" ++
1506 "{\"v\":0,\"seq\":3,\"kind\":\"context.switch\",\"time_ns\":40," ++
1507 "\"prev_thread\":8,\"next_thread\":7,\"context_reason\":\"waiting\"," ++
1508 "\"context_state\":\"Waiting\"}\n" ++
1509 "{\"v\":0,\"seq\":4,\"kind\":\"stop\",\"time_ns\":50}\n";
1510 var analyzer = Analyzer.init(allocator);
1511 defer analyzer.deinit();
1512 try analyzer.ingestJsonlBytes(trace);
1513 var out = std.Io.Writer.Allocating.init(std.testing.allocator);
1514 defer out.deinit();
1515 try writeJsonl(allocator, &analyzer, &out.writer, .{ .sort = .latency });
1516 }
1517
1518 fn appendReadyLatencySample(
1519 writer: *std.Io.Writer,
1520 seq: *u64,
1521 thread: u64,
1522 start_ns: u64,
1523 latency_ns: u64,
1524 ) !void {
1525 try (event.TraceEvent{
1526 .seq = seq.*,
1527 .kind = .thread_wakeup,
1528 .time_ns = start_ns,
1529 .thread = 99,
1530 .target_thread = thread,
1531 .cpu = 0,
1532 }).writeJsonLine(writer);
1533 seq.* += 1;
1534 try (event.TraceEvent{
1535 .seq = seq.*,
1536 .kind = .context_switch,
1537 .time_ns = start_ns + latency_ns,
1538 .thread = 99,
1539 .cpu = 0,
1540 .prev_thread = 99,
1541 .next_thread = thread,
1542 }).writeJsonLine(writer);
1543 seq.* += 1;
1544 try (event.TraceEvent{
1545 .seq = seq.*,
1546 .kind = .context_switch,
1547 .time_ns = start_ns + latency_ns + 1,
1548 .thread = 99,
1549 .cpu = 0,
1550 .prev_thread = thread,
1551 .next_thread = 99,
1552 .context_state = "Waiting",
1553 }).writeJsonLine(writer);
1554 seq.* += 1;
1555 }
1556
1557 fn contextTestFlightReport() transport.Report {
1558 return .{
1559 .policy = .overwrite_oldest,
1560 .state = .accepting,
1561 .capacity_bytes = 64,
1562 .retained_bytes = 32,
1563 .event_capacity_bytes = 16,
1564 .writer_capacity_bytes = 8,
1565 .observed_events = 5,
1566 .stored_events = 5,
1567 .retained_events = 4,
1568 .overwritten_events = 1,
1569 .dropped_events = 0,
1570 .oversized_events = 0,
1571 .partial_event_bytes = 0,
1572 .discarding_oversized_event = false,
1573 };
1574 }
1575
1576 fn expectContextContains(haystack: []const u8, needle: []const u8) !void {
1577 try std.testing.expect(std.mem.indexOf(u8, haystack, needle) != null);
1578 }