lib/tracy/src/fiber.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const pretty_json = @import("pretty").json;
3 const report = @import("report.zig");
4 const event = @import("event.zig");
5 const record_mod = @import("record.zig");
6
7 pub const schema = "tracy.fibers/v0";
8
9 pub const Group = enum {
10 fiber,
11 thread,
12 transition,
13 none,
14
15 pub fn fromName(text: []const u8) ?Group {
16 if (std.mem.eql(u8, text, "fiber")) return .fiber;
17 if (std.mem.eql(u8, text, "thread")) return .thread;
18 if (std.mem.eql(u8, text, "transition")) return .transition;
19 if (std.mem.eql(u8, text, "none")) return .none;
20 return null;
21 }
22
23 fn tag(self: Group) []const u8 {
24 return switch (self) {
25 .fiber => "fiber",
26 .thread => "thread",
27 .transition => "transition",
28 .none => "none",
29 };
30 }
31 };
32
33 pub const Sort = enum {
34 running,
35 enters,
36 leaves,
37 migrations,
38 unmatched,
39 last,
40 fiber,
41 thread,
42 label,
43
44 pub fn fromName(text: []const u8) ?Sort {
45 if (std.mem.eql(u8, text, "running")) return .running;
46 if (std.mem.eql(u8, text, "enters")) return .enters;
47 if (std.mem.eql(u8, text, "leaves")) return .leaves;
48 if (std.mem.eql(u8, text, "migrations")) return .migrations;
49 if (std.mem.eql(u8, text, "unmatched")) return .unmatched;
50 if (std.mem.eql(u8, text, "last")) return .last;
51 if (std.mem.eql(u8, text, "fiber")) return .fiber;
52 if (std.mem.eql(u8, text, "thread")) return .thread;
53 if (std.mem.eql(u8, text, "label")) return .label;
54 return null;
55 }
56
57 fn tag(self: Sort) []const u8 {
58 return switch (self) {
59 .running => "running",
60 .enters => "enters",
61 .leaves => "leaves",
62 .migrations => "migrations",
63 .unmatched => "unmatched",
64 .last => "last",
65 .fiber => "fiber",
66 .thread => "thread",
67 .label => "label",
68 };
69 }
70 };
71
72 pub const Options = struct {
73 top: usize = 20,
74 occurrences: usize = 80,
75 group: Group = .fiber,
76 sort: Sort = .running,
77 fiber: ?u64 = null,
78 thread: ?u64 = null,
79 since_ns: ?u64 = null,
80 until_ns: ?u64 = null,
81 match: ?[]const u8 = null,
82 ignore_case: bool = false,
83 };
84
85 pub const Counters = struct {
86 events: u64 = 0,
87 enters: u64 = 0,
88 leaves: u64 = 0,
89 names: u64 = 0,
90 thread_names: u64 = 0,
91 unmatched_leaves: u64 = 0,
92 filtered: u64 = 0,
93 groups: u64 = 0,
94 active_fibers: u64 = 0,
95 duration_ns: u64 = 0,
96 };
97
98 const FiberState = struct {
99 id: u64,
100 name: ?[]u8 = null,
101 group_hint: ?i64 = null,
102 enters: u64 = 0,
103 leaves: u64 = 0,
104 running_ns: u64 = 0,
105 migrations: u64 = 0,
106 unmatched_leaves: u64 = 0,
107 active_since_ns: ?u64 = null,
108 active_thread: ?u64 = null,
109 last_thread: ?u64 = null,
110 threads: std.AutoHashMapUnmanaged(u64, void) = .{},
111 first_ns: u64 = 0,
112 last_ns: u64 = 0,
113
114 fn deinit(self: *FiberState, allocator: std.mem.Allocator) void {
115 if (self.name) |name| allocator.free(name);
116 self.threads.deinit(allocator);
117 self.* = undefined;
118 }
119
120 fn displayName(self: FiberState) []const u8 {
121 return self.name orelse "";
122 }
123 };
124
125 const ThreadState = struct {
126 id: u64,
127 name: ?[]u8 = null,
128 enters: u64 = 0,
129 leaves: u64 = 0,
130 running_ns: u64 = 0,
131 unmatched_leaves: u64 = 0,
132 current_fiber: u64 = 0,
133 current_since_ns: ?u64 = null,
134 first_ns: u64 = 0,
135 last_ns: u64 = 0,
136
137 fn deinit(self: *ThreadState, allocator: std.mem.Allocator) void {
138 if (self.name) |name| allocator.free(name);
139 self.* = undefined;
140 }
141
142 fn displayName(self: ThreadState) []const u8 {
143 return self.name orelse "";
144 }
145 };
146
147 const TransitionState = struct {
148 label: []u8,
149 from_thread: u64 = 0,
150 to_thread: u64 = 0,
151 fibers: std.AutoHashMapUnmanaged(u64, void) = .{},
152 count: u64 = 0,
153 first_ns: u64 = 0,
154 last_ns: u64 = 0,
155
156 fn deinit(self: *TransitionState, allocator: std.mem.Allocator) void {
157 allocator.free(self.label);
158 self.fibers.deinit(allocator);
159 self.* = undefined;
160 }
161 };
162
163 const OccurrenceKind = enum {
164 name,
165 enter,
166 leave,
167
168 fn tag(self: OccurrenceKind) []const u8 {
169 return switch (self) {
170 .name => "name",
171 .enter => "enter",
172 .leave => "leave",
173 };
174 }
175 };
176
177 const Occurrence = struct {
178 kind: OccurrenceKind,
179 seq: u64 = 0,
180 time_ns: u64 = 0,
181 fiber: u64 = 0,
182 thread: u64 = 0,
183 name: ?[]u8 = null,
184 group_hint: ?i64 = null,
185 duration_ns: u64 = 0,
186 unmatched: bool = false,
187
188 fn deinit(self: *Occurrence, allocator: std.mem.Allocator) void {
189 if (self.name) |name| allocator.free(name);
190 self.* = undefined;
191 }
192 };
193
194 pub const Summary = struct {
195 group: Group,
196 label: []u8,
197 count: u64 = 0,
198 fiber: ?u64 = null,
199 thread: ?u64 = null,
200 from_thread: u64 = 0,
201 to_thread: u64 = 0,
202 group_hint: ?i64 = null,
203 enters: u64 = 0,
204 leaves: u64 = 0,
205 running_ns: u64 = 0,
206 migrations: u64 = 0,
207 unmatched_leaves: u64 = 0,
208 active: bool = false,
209 first_ns: u64 = 0,
210 last_ns: u64 = 0,
211
212 pub fn deinit(self: *Summary, allocator: std.mem.Allocator) void {
213 allocator.free(self.label);
214 self.* = undefined;
215 }
216 };
217
218 pub const Analyzer = struct {
219 allocator: std.mem.Allocator,
220 fibers: std.AutoHashMapUnmanaged(u64, FiberState) = .{},
221 threads: std.AutoHashMapUnmanaged(u64, ThreadState) = .{},
222 transitions: std.StringHashMapUnmanaged(TransitionState) = .{},
223 occurrences: std.ArrayListUnmanaged(Occurrence) = .empty,
224 counters: Counters = .{},
225 start_ns: ?u64 = null,
226 end_ns: ?u64 = null,
227
228 pub fn init(allocator: std.mem.Allocator) Analyzer {
229 return .{ .allocator = allocator };
230 }
231
232 pub fn deinit(self: *Analyzer) void {
233 var fiber_iter = self.fibers.valueIterator();
234 while (fiber_iter.next()) |fiber_state| fiber_state.deinit(self.allocator);
235 self.fibers.deinit(self.allocator);
236 var thread_iter = self.threads.valueIterator();
237 while (thread_iter.next()) |thread_state| thread_state.deinit(self.allocator);
238 self.threads.deinit(self.allocator);
239 var transition_iter = self.transitions.iterator();
240 while (transition_iter.next()) |entry| {
241 self.allocator.free(entry.key_ptr.*);
242 entry.value_ptr.deinit(self.allocator);
243 }
244 self.transitions.deinit(self.allocator);
245 for (self.occurrences.items) |*occurrence| occurrence.deinit(self.allocator);
246 self.occurrences.deinit(self.allocator);
247 self.* = undefined;
248 }
249
250 pub fn ingestJsonlBytes(self: *Analyzer, bytes: []const u8) !void {
251 var lines = std.mem.splitScalar(u8, bytes, '\n');
252 while (lines.next()) |line| try self.ingestJsonLine(line);
253 }
254
255 pub fn ingestJsonLine(self: *Analyzer, line: []const u8) !void {
256 const text = std.mem.trim(u8, line, " \t\r\n");
257 if (text.len == 0) return;
258 var parsed = (try record_mod.parseEventLine(self.allocator, text)) orelse return;
259 defer parsed.deinit();
260 try self.ingest(parsed);
261 }
262
263 pub fn ingest(self: *Analyzer, parsed: event.Parsed) !void {
264 self.counters.events += 1;
265 if (self.start_ns == null and parsed.time_ns != 0) self.start_ns = parsed.time_ns;
266 if (parsed.time_ns != 0) self.end_ns = parsed.time_ns;
267 switch (parsed.kind) {
268 .start => {
269 if (parsed.time_ns != 0) self.start_ns = parsed.time_ns;
270 },
271 .stop => {
272 if (parsed.time_ns != 0) self.end_ns = parsed.time_ns;
273 },
274 .thread_name => try self.recordThreadName(parsed),
275 .fiber_name => try self.recordFiberName(parsed),
276 .fiber_enter => try self.recordEnter(parsed),
277 .fiber_leave => try self.recordLeave(parsed),
278 else => {},
279 }
280 }
281
282 pub fn collectSummaries(self: *Analyzer, allocator: std.mem.Allocator, options: Options) !std.ArrayListUnmanaged(Summary) {
283 var summaries: std.ArrayListUnmanaged(Summary) = .empty;
284 errdefer deinitSummaries(allocator, &summaries);
285 self.counters.filtered = 0;
286 self.counters.active_fibers = self.activeCount();
287 switch (options.group) {
288 .fiber => {
289 var iter = self.fibers.valueIterator();
290 while (iter.next()) |fiber_state| {
291 if (!self.fiberMatches(fiber_state.*, options)) {
292 self.counters.filtered += 1;
293 continue;
294 }
295 try summaries.append(allocator, try fiberSummary(allocator, fiber_state.*, self.end_ns));
296 }
297 },
298 .thread => {
299 var iter = self.threads.valueIterator();
300 while (iter.next()) |thread_state| {
301 if (!threadMatches(thread_state.*, options)) {
302 self.counters.filtered += 1;
303 continue;
304 }
305 try summaries.append(allocator, try threadSummary(allocator, thread_state.*, self.end_ns));
306 }
307 },
308 .transition => {
309 var iter = self.transitions.valueIterator();
310 while (iter.next()) |transition| {
311 if (!transitionMatches(transition.*, options)) {
312 self.counters.filtered += 1;
313 continue;
314 }
315 try summaries.append(allocator, try transitionSummary(allocator, transition.*));
316 }
317 },
318 .none => {
319 for (self.occurrences.items) |occurrence| {
320 if (!occurrenceMatches(occurrence, options)) {
321 self.counters.filtered += 1;
322 continue;
323 }
324 try summaries.append(allocator, try occurrenceSummary(allocator, occurrence));
325 }
326 },
327 }
328 self.counters.groups = @intCast(summaries.items.len);
329 self.counters.duration_ns = self.durationNs();
330 sortSummaries(summaries.items, options.sort);
331 return summaries;
332 }
333
334 pub fn collectOccurrences(self: *Analyzer, allocator: std.mem.Allocator, options: Options) !std.ArrayListUnmanaged(Occurrence) {
335 var occurrences: std.ArrayListUnmanaged(Occurrence) = .empty;
336 for (self.occurrences.items) |occurrence| {
337 if (!occurrenceMatches(occurrence, options)) continue;
338 try occurrences.append(allocator, occurrence);
339 }
340 sortOccurrences(occurrences.items, options.sort);
341 return occurrences;
342 }
343
344 pub fn durationNs(self: Analyzer) u64 {
345 const start_ns = self.start_ns orelse return 0;
346 const end_ns = self.end_ns orelse return 0;
347 if (end_ns <= start_ns) return 0;
348 return end_ns - start_ns;
349 }
350
351 pub fn runningNs(self: Analyzer) u64 {
352 var total: u64 = 0;
353 var iter = self.fibers.valueIterator();
354 while (iter.next()) |fiber_state| total +|= effectiveFiberRunning(fiber_state.*, self.end_ns);
355 return total;
356 }
357
358 fn recordThreadName(self: *Analyzer, parsed: event.Parsed) !void {
359 const name = parsed.name orelse return;
360 const thread_state = try self.threadState(parsed.thread);
361 if (thread_state.name) |old| self.allocator.free(old);
362 thread_state.name = try self.allocator.dupe(u8, name);
363 self.counters.thread_names += 1;
364 }
365
366 fn recordFiberName(self: *Analyzer, parsed: event.Parsed) !void {
367 const id = parsed.id;
368 if (id == 0) return;
369 const fiber_state = try self.fiberState(id);
370 try self.setFiberName(fiber_state, parsed.name);
371 fiber_state.group_hint = parsed.group_hint;
372 noteRange(&fiber_state.first_ns, &fiber_state.last_ns, parsed.time_ns);
373 self.counters.names += 1;
374 try self.recordOccurrence(.{
375 .kind = .name,
376 .seq = parsed.seq,
377 .time_ns = parsed.time_ns,
378 .fiber = id,
379 .thread = parsed.thread,
380 .name = try dupeOptional(self.allocator, parsed.name),
381 .group_hint = parsed.group_hint,
382 });
383 }
384
385 fn recordEnter(self: *Analyzer, parsed: event.Parsed) !void {
386 const id = parsed.id;
387 if (id == 0) return;
388 self.counters.enters += 1;
389 const thread_state = try self.threadState(parsed.thread);
390 closeCurrentThreadFiber(self, thread_state, parsed.time_ns) catch |err| return err;
391 const fiber_state = try self.fiberState(id);
392 if (parsed.name != null) try self.setFiberName(fiber_state, parsed.name);
393 if (parsed.group_hint) |group_hint| fiber_state.group_hint = group_hint;
394 if (fiber_state.active_since_ns) |active_since| {
395 if (parsed.time_ns >= active_since) fiber_state.running_ns +|= parsed.time_ns - active_since;
396 }
397 if (fiber_state.last_thread) |last_thread| {
398 if (last_thread != parsed.thread) {
399 fiber_state.migrations += 1;
400 try self.recordTransition(id, last_thread, parsed.thread, parsed.time_ns);
401 }
402 }
403 fiber_state.enters += 1;
404 fiber_state.active_since_ns = parsed.time_ns;
405 fiber_state.active_thread = parsed.thread;
406 fiber_state.last_thread = parsed.thread;
407 try fiber_state.threads.put(self.allocator, parsed.thread, {});
408 noteRange(&fiber_state.first_ns, &fiber_state.last_ns, parsed.time_ns);
409 thread_state.enters += 1;
410 thread_state.current_fiber = id;
411 thread_state.current_since_ns = parsed.time_ns;
412 noteRange(&thread_state.first_ns, &thread_state.last_ns, parsed.time_ns);
413 try self.recordOccurrence(.{
414 .kind = .enter,
415 .seq = parsed.seq,
416 .time_ns = parsed.time_ns,
417 .fiber = id,
418 .thread = parsed.thread,
419 .name = try dupeOptional(self.allocator, fiber_state.name orelse parsed.name),
420 .group_hint = fiber_state.group_hint,
421 });
422 }
423
424 fn recordLeave(self: *Analyzer, parsed: event.Parsed) !void {
425 self.counters.leaves += 1;
426 const thread_state = try self.threadState(parsed.thread);
427 const id = if (parsed.id != 0) parsed.id else thread_state.current_fiber;
428 var unmatched = false;
429 var duration: u64 = 0;
430 if (id == 0) {
431 unmatched = true;
432 thread_state.unmatched_leaves += 1;
433 self.counters.unmatched_leaves += 1;
434 } else {
435 const fiber_state = try self.fiberState(id);
436 fiber_state.leaves += 1;
437 noteRange(&fiber_state.first_ns, &fiber_state.last_ns, parsed.time_ns);
438 if (fiber_state.active_since_ns) |active_since| {
439 if (fiber_state.active_thread == parsed.thread and parsed.time_ns >= active_since) {
440 duration = parsed.time_ns - active_since;
441 fiber_state.running_ns +|= duration;
442 fiber_state.active_since_ns = null;
443 fiber_state.active_thread = null;
444 } else {
445 unmatched = true;
446 }
447 } else {
448 unmatched = true;
449 }
450 if (thread_state.current_fiber == id and thread_state.current_since_ns != null) {
451 const current_since = thread_state.current_since_ns.?;
452 if (parsed.time_ns >= current_since) thread_state.running_ns +|= parsed.time_ns - current_since;
453 thread_state.current_fiber = 0;
454 thread_state.current_since_ns = null;
455 } else if (unmatched) {
456 thread_state.unmatched_leaves += 1;
457 }
458 if (unmatched) {
459 fiber_state.unmatched_leaves += 1;
460 self.counters.unmatched_leaves += 1;
461 }
462 thread_state.leaves += 1;
463 noteRange(&thread_state.first_ns, &thread_state.last_ns, parsed.time_ns);
464 }
465 try self.recordOccurrence(.{
466 .kind = .leave,
467 .seq = parsed.seq,
468 .time_ns = parsed.time_ns,
469 .fiber = id,
470 .thread = parsed.thread,
471 .duration_ns = duration,
472 .unmatched = unmatched,
473 });
474 }
475
476 fn setFiberName(self: *Analyzer, fiber_state: *FiberState, name: ?[]const u8) !void {
477 const actual = name orelse return;
478 if (fiber_state.name) |old| self.allocator.free(old);
479 fiber_state.name = try self.allocator.dupe(u8, actual);
480 }
481
482 fn fiberState(self: *Analyzer, id: u64) !*FiberState {
483 const entry = try self.fibers.getOrPut(self.allocator, id);
484 if (!entry.found_existing) entry.value_ptr.* = .{ .id = id };
485 return entry.value_ptr;
486 }
487
488 fn threadState(self: *Analyzer, id: u64) !*ThreadState {
489 const entry = try self.threads.getOrPut(self.allocator, id);
490 if (!entry.found_existing) entry.value_ptr.* = .{ .id = id };
491 return entry.value_ptr;
492 }
493
494 fn recordTransition(self: *Analyzer, fiber: u64, from_thread: u64, to_thread: u64, time_ns: u64) !void {
495 var label_writer = std.Io.Writer.Allocating.init(self.allocator);
496 defer label_writer.deinit();
497 try label_writer.writer.print("thread {d}->{d}", .{ from_thread, to_thread });
498 const label = label_writer.written();
499 const entry = try self.transitions.getOrPut(self.allocator, label);
500 if (!entry.found_existing) {
501 const owned_label = try self.allocator.dupe(u8, label);
502 entry.key_ptr.* = owned_label;
503 entry.value_ptr.* = .{
504 .label = try self.allocator.dupe(u8, owned_label),
505 .from_thread = from_thread,
506 .to_thread = to_thread,
507 };
508 }
509 entry.value_ptr.count += 1;
510 try entry.value_ptr.fibers.put(self.allocator, fiber, {});
511 noteRange(&entry.value_ptr.first_ns, &entry.value_ptr.last_ns, time_ns);
512 }
513
514 fn recordOccurrence(self: *Analyzer, occurrence: Occurrence) !void {
515 try self.occurrences.append(self.allocator, occurrence);
516 }
517
518 pub fn activeCount(self: Analyzer) u64 {
519 var count: u64 = 0;
520 var iter = self.fibers.valueIterator();
521 while (iter.next()) |fiber_state| {
522 if (fiber_state.active_since_ns != null) count += 1;
523 }
524 return count;
525 }
526
527 fn fiberMatches(self: Analyzer, fiber_state: FiberState, options: Options) bool {
528 if (options.fiber) |fiber_filter| if (fiber_state.id != fiber_filter) return false;
529 if (options.thread) |thread_filter| if (!fiber_state.threads.contains(thread_filter)) return false;
530 if (!rangeMatches(fiber_state.first_ns, fiber_state.last_ns, options)) return false;
531 if (options.match) |needle| {
532 if (contains(fiber_state.displayName(), needle, options.ignore_case)) return true;
533 var buffer: [32]u8 = undefined;
534 const id_text = std.fmt.bufPrint(&buffer, "{d}", .{fiber_state.id}) catch "";
535 if (contains(id_text, needle, options.ignore_case)) return true;
536 return false;
537 }
538 _ = self;
539 return true;
540 }
541 };
542
543 pub fn deinitSummaries(allocator: std.mem.Allocator, summaries: *std.ArrayListUnmanaged(Summary)) void {
544 for (summaries.items) |*summary| summary.deinit(allocator);
545 summaries.deinit(allocator);
546 }
547
548 pub fn writeTextFromJsonlPath(
549 allocator: std.mem.Allocator,
550 path: []const u8,
551 writer: *std.Io.Writer,
552 options: Options,
553 ) !void {
554 return report.writeFromJsonlPath(Analyzer, writeText, allocator, path, writer, options);
555 }
556
557 pub fn writeJsonlFromJsonlPath(
558 allocator: std.mem.Allocator,
559 path: []const u8,
560 writer: *std.Io.Writer,
561 options: Options,
562 ) !void {
563 return report.writeFromJsonlPath(Analyzer, writeJsonl, allocator, path, writer, options);
564 }
565
566 pub fn ingestPath(analyzer: *Analyzer, path: []const u8) !void {
567 return report.ingestJsonlPath(analyzer, path);
568 }
569
570 fn writeText(
571 allocator: std.mem.Allocator,
572 analyzer: *Analyzer,
573 writer: *std.Io.Writer,
574 options: Options,
575 ) !void {
576 var summaries = try analyzer.collectSummaries(allocator, options);
577 defer deinitSummaries(allocator, &summaries);
578 var occurrences = try analyzer.collectOccurrences(allocator, options);
579 defer occurrences.deinit(allocator);
580
581 try writer.print(
582 "tracy fibers groups={d} fibers={d} threads={d} enters={d} leaves={d} names={d} running_ns={d} migrations={d} unmatched_leaves={d} active={d} filtered={d} duration_ns={d} group={s} sort={s}\n",
583 .{
584 summaries.items.len,
585 analyzer.fibers.count(),
586 analyzer.threads.count(),
587 analyzer.counters.enters,
588 analyzer.counters.leaves,
589 analyzer.counters.names,
590 analyzer.runningNs(),
591 totalMigrations(analyzer),
592 analyzer.counters.unmatched_leaves,
593 analyzer.counters.active_fibers,
594 analyzer.counters.filtered,
595 analyzer.durationNs(),
596 options.group.tag(),
597 options.sort.tag(),
598 },
599 );
600 const summary_limit = @min(options.top, summaries.items.len);
601 for (summaries.items[0..summary_limit]) |summary| {
602 try writer.print("fiber group={s} label=", .{summary.group.tag()});
603 try pretty_json.writeString(writer, summary.label);
604 try writer.print(" count={d}", .{summary.count});
605 try writeSummaryFieldsText(writer, summary);
606 try writer.writeByte('\n');
607 }
608
609 const occurrence_limit = @min(options.occurrences, occurrences.items.len);
610 for (occurrences.items[0..occurrence_limit]) |occurrence| {
611 try writer.print("fiber-occurrence kind={s} time_ns={d} thread={d}", .{ occurrence.kind.tag(), occurrence.time_ns, occurrence.thread });
612 try writeOccurrenceFieldsText(writer, occurrence);
613 try writer.writeByte('\n');
614 }
615 }
616
617 fn writeJsonl(
618 allocator: std.mem.Allocator,
619 analyzer: *Analyzer,
620 writer: *std.Io.Writer,
621 options: Options,
622 ) !void {
623 var summaries = try analyzer.collectSummaries(allocator, options);
624 defer deinitSummaries(allocator, &summaries);
625 var occurrences = try analyzer.collectOccurrences(allocator, options);
626 defer occurrences.deinit(allocator);
627
628 var summary_stream = pretty_json.Writer.init(writer, .minified);
629 const summary_record = try summary_stream.object();
630 try summary_record.field("schema", schema);
631 try summary_record.field("kind", "summary");
632 try summary_record.field("groups", summaries.items.len);
633 try summary_record.field("fibers", analyzer.fibers.count());
634 try summary_record.field("threads", analyzer.threads.count());
635 try summary_record.field("enters", analyzer.counters.enters);
636 try summary_record.field("leaves", analyzer.counters.leaves);
637 try summary_record.field("names", analyzer.counters.names);
638 try summary_record.field("running_ns", analyzer.runningNs());
639 try summary_record.field("migrations", totalMigrations(analyzer));
640 try summary_record.field("unmatched_leaves", analyzer.counters.unmatched_leaves);
641 try summary_record.field("active", analyzer.counters.active_fibers);
642 try summary_record.field("filtered", analyzer.counters.filtered);
643 try summary_record.field("duration_ns", analyzer.durationNs());
644 try summary_record.field("group", options.group.tag());
645 try summary_record.field("sort", options.sort.tag());
646 try summary_record.endLine();
647
648 const summary_limit = @min(options.top, summaries.items.len);
649 for (summaries.items[0..summary_limit]) |summary| {
650 var stream = pretty_json.Writer.init(writer, .minified);
651 const object = try stream.object();
652 try object.field("schema", schema);
653 try object.field("kind", "group");
654 try object.field("group", summary.group.tag());
655 try object.field("label", summary.label);
656 try object.field("count", summary.count);
657 try writeSummaryFields(object, summary);
658 try object.endLine();
659 }
660
661 const occurrence_limit = @min(options.occurrences, occurrences.items.len);
662 for (occurrences.items[0..occurrence_limit]) |occurrence| {
663 var stream = pretty_json.Writer.init(writer, .minified);
664 const object = try stream.object();
665 try object.field("schema", schema);
666 try object.field("kind", occurrence.kind.tag());
667 try object.field("time_ns", occurrence.time_ns);
668 try object.field("thread", occurrence.thread);
669 try writeOccurrenceFields(object, occurrence);
670 try object.endLine();
671 }
672 }
673
674 fn closeCurrentThreadFiber(analyzer: *Analyzer, thread_state: *ThreadState, time_ns: u64) !void {
675 if (thread_state.current_fiber == 0) return;
676 if (thread_state.current_since_ns) |current_since| {
677 if (time_ns >= current_since) thread_state.running_ns +|= time_ns - current_since;
678 }
679 const fiber_state = try analyzer.fiberState(thread_state.current_fiber);
680 if (fiber_state.active_since_ns) |active_since| {
681 if (time_ns >= active_since) fiber_state.running_ns +|= time_ns - active_since;
682 fiber_state.active_since_ns = null;
683 fiber_state.active_thread = null;
684 }
685 thread_state.current_fiber = 0;
686 thread_state.current_since_ns = null;
687 }
688
689 fn fiberSummary(allocator: std.mem.Allocator, fiber_state: FiberState, end_ns: ?u64) !Summary {
690 var label = std.Io.Writer.Allocating.init(allocator);
691 defer label.deinit();
692 try label.writer.print("fiber {d}", .{fiber_state.id});
693 if (fiber_state.name) |name| {
694 try label.writer.writeByte(' ');
695 try label.writer.writeAll(name);
696 }
697 return .{
698 .group = .fiber,
699 .label = try allocator.dupe(u8, label.written()),
700 .count = fiber_state.enters + fiber_state.leaves,
701 .fiber = fiber_state.id,
702 .thread = fiber_state.last_thread,
703 .group_hint = fiber_state.group_hint,
704 .enters = fiber_state.enters,
705 .leaves = fiber_state.leaves,
706 .running_ns = effectiveFiberRunning(fiber_state, end_ns),
707 .migrations = fiber_state.migrations,
708 .unmatched_leaves = fiber_state.unmatched_leaves,
709 .active = fiber_state.active_since_ns != null,
710 .first_ns = fiber_state.first_ns,
711 .last_ns = fiber_state.last_ns,
712 };
713 }
714
715 fn threadSummary(allocator: std.mem.Allocator, thread_state: ThreadState, end_ns: ?u64) !Summary {
716 var label = std.Io.Writer.Allocating.init(allocator);
717 defer label.deinit();
718 try label.writer.print("thread {d}", .{thread_state.id});
719 if (thread_state.name) |name| {
720 try label.writer.writeByte(' ');
721 try label.writer.writeAll(name);
722 }
723 return .{
724 .group = .thread,
725 .label = try allocator.dupe(u8, label.written()),
726 .count = thread_state.enters + thread_state.leaves,
727 .thread = thread_state.id,
728 .enters = thread_state.enters,
729 .leaves = thread_state.leaves,
730 .running_ns = effectiveThreadRunning(thread_state, end_ns),
731 .unmatched_leaves = thread_state.unmatched_leaves,
732 .active = thread_state.current_fiber != 0,
733 .first_ns = thread_state.first_ns,
734 .last_ns = thread_state.last_ns,
735 };
736 }
737
738 fn transitionSummary(allocator: std.mem.Allocator, transition: TransitionState) !Summary {
739 return .{
740 .group = .transition,
741 .label = try allocator.dupe(u8, transition.label),
742 .count = transition.count,
743 .from_thread = transition.from_thread,
744 .to_thread = transition.to_thread,
745 .migrations = transition.count,
746 .first_ns = transition.first_ns,
747 .last_ns = transition.last_ns,
748 };
749 }
750
751 fn occurrenceSummary(allocator: std.mem.Allocator, occurrence: Occurrence) !Summary {
752 var label = std.Io.Writer.Allocating.init(allocator);
753 defer label.deinit();
754 try label.writer.print("{s} {d}", .{ occurrence.kind.tag(), occurrence.time_ns });
755 return .{
756 .group = .none,
757 .label = try allocator.dupe(u8, label.written()),
758 .count = 1,
759 .fiber = if (occurrence.fiber == 0) null else occurrence.fiber,
760 .thread = occurrence.thread,
761 .group_hint = occurrence.group_hint,
762 .running_ns = occurrence.duration_ns,
763 .unmatched_leaves = if (occurrence.unmatched) 1 else 0,
764 .first_ns = occurrence.time_ns,
765 .last_ns = occurrence.time_ns,
766 };
767 }
768
769 fn effectiveFiberRunning(fiber_state: FiberState, end_ns: ?u64) u64 {
770 var total = fiber_state.running_ns;
771 if (fiber_state.active_since_ns) |active_since| {
772 if (end_ns) |end| {
773 if (end >= active_since) total +|= end - active_since;
774 }
775 }
776 return total;
777 }
778
779 fn effectiveThreadRunning(thread_state: ThreadState, end_ns: ?u64) u64 {
780 var total = thread_state.running_ns;
781 if (thread_state.current_since_ns) |current_since| {
782 if (end_ns) |end| {
783 if (end >= current_since) total +|= end - current_since;
784 }
785 }
786 return total;
787 }
788
789 fn totalMigrations(analyzer: *Analyzer) u64 {
790 var total: u64 = 0;
791 var iter = analyzer.fibers.valueIterator();
792 while (iter.next()) |fiber_state| total +|= fiber_state.migrations;
793 return total;
794 }
795
796 fn threadMatches(thread_state: ThreadState, options: Options) bool {
797 if (options.thread) |thread_filter| if (thread_state.id != thread_filter) return false;
798 if (!rangeMatches(thread_state.first_ns, thread_state.last_ns, options)) return false;
799 if (options.match) |needle| {
800 if (contains(thread_state.displayName(), needle, options.ignore_case)) return true;
801 var buffer: [32]u8 = undefined;
802 const thread_text = std.fmt.bufPrint(&buffer, "{d}", .{thread_state.id}) catch "";
803 if (contains(thread_text, needle, options.ignore_case)) return true;
804 return false;
805 }
806 return true;
807 }
808
809 fn transitionMatches(transition: TransitionState, options: Options) bool {
810 if (options.fiber) |fiber_filter| if (!transition.fibers.contains(fiber_filter)) return false;
811 if (options.thread) |thread_filter| {
812 if (transition.from_thread != thread_filter and transition.to_thread != thread_filter) return false;
813 }
814 if (!rangeMatches(transition.first_ns, transition.last_ns, options)) return false;
815 if (options.match) |needle| return contains(transition.label, needle, options.ignore_case);
816 return true;
817 }
818
819 fn occurrenceMatches(occurrence: Occurrence, options: Options) bool {
820 if (options.fiber) |fiber_filter| if (occurrence.fiber != fiber_filter) return false;
821 if (options.thread) |thread_filter| if (occurrence.thread != thread_filter) return false;
822 if (options.since_ns) |since_ns| if (occurrence.time_ns < since_ns) return false;
823 if (options.until_ns) |until_ns| if (occurrence.time_ns > until_ns) return false;
824 if (options.match) |needle| {
825 if (contains(occurrence.kind.tag(), needle, options.ignore_case)) return true;
826 if (occurrence.name) |name| if (contains(name, needle, options.ignore_case)) return true;
827 var buffer: [32]u8 = undefined;
828 const fiber_text = std.fmt.bufPrint(&buffer, "{d}", .{occurrence.fiber}) catch "";
829 if (contains(fiber_text, needle, options.ignore_case)) return true;
830 return false;
831 }
832 return true;
833 }
834
835 fn rangeMatches(first_ns: u64, last_ns: u64, options: Options) bool {
836 if (options.since_ns) |since_ns| if (last_ns != 0 and last_ns < since_ns) return false;
837 if (options.until_ns) |until_ns| if (first_ns != 0 and first_ns > until_ns) return false;
838 return true;
839 }
840
841 fn writeSummaryFieldsText(writer: *std.Io.Writer, summary: Summary) !void {
842 if (summary.fiber) |fiber| try writer.print(" fiber={d}", .{fiber});
843 if (summary.thread) |thread| try writer.print(" thread={d}", .{thread});
844 if (summary.from_thread != 0 or summary.to_thread != 0) try writer.print(" from_thread={d} to_thread={d}", .{ summary.from_thread, summary.to_thread });
845 if (summary.group_hint) |group_hint| try writer.print(" group_hint={d}", .{group_hint});
846 if (summary.enters != 0) try writer.print(" enters={d}", .{summary.enters});
847 if (summary.leaves != 0) try writer.print(" leaves={d}", .{summary.leaves});
848 if (summary.running_ns != 0) try writer.print(" running_ns={d}", .{summary.running_ns});
849 if (summary.migrations != 0) try writer.print(" migrations={d}", .{summary.migrations});
850 if (summary.unmatched_leaves != 0) try writer.print(" unmatched_leaves={d}", .{summary.unmatched_leaves});
851 if (summary.active) try writer.writeAll(" active=true");
852 if (summary.first_ns != 0) try writer.print(" first_ns={d}", .{summary.first_ns});
853 if (summary.last_ns != 0) try writer.print(" last_ns={d}", .{summary.last_ns});
854 }
855
856 fn writeSummaryFields(object: pretty_json.Object, summary: Summary) !void {
857 if (summary.fiber) |fiber| try object.field("fiber", fiber);
858 if (summary.thread) |thread| try object.field("thread", thread);
859 if (summary.from_thread != 0 or summary.to_thread != 0) {
860 try object.field("from_thread", summary.from_thread);
861 try object.field("to_thread", summary.to_thread);
862 }
863 if (summary.group_hint) |group_hint| try object.field("group_hint", group_hint);
864 if (summary.enters != 0) try object.field("enters", summary.enters);
865 if (summary.leaves != 0) try object.field("leaves", summary.leaves);
866 if (summary.running_ns != 0) try object.field("running_ns", summary.running_ns);
867 if (summary.migrations != 0) try object.field("migrations", summary.migrations);
868 if (summary.unmatched_leaves != 0) try object.field("unmatched_leaves", summary.unmatched_leaves);
869 if (summary.active) try object.field("active", true);
870 if (summary.first_ns != 0) try object.field("first_ns", summary.first_ns);
871 if (summary.last_ns != 0) try object.field("last_ns", summary.last_ns);
872 }
873
874 fn writeOccurrenceFieldsText(writer: *std.Io.Writer, occurrence: Occurrence) !void {
875 if (occurrence.fiber != 0) try writer.print(" fiber={d}", .{occurrence.fiber});
876 if (occurrence.name) |name| {
877 try writer.writeAll(" name=");
878 try pretty_json.writeString(writer, name);
879 }
880 if (occurrence.group_hint) |group_hint| try writer.print(" group_hint={d}", .{group_hint});
881 if (occurrence.duration_ns != 0) try writer.print(" duration_ns={d}", .{occurrence.duration_ns});
882 if (occurrence.unmatched) try writer.writeAll(" unmatched=true");
883 }
884
885 fn writeOccurrenceFields(object: pretty_json.Object, occurrence: Occurrence) !void {
886 if (occurrence.fiber != 0) try object.field("fiber", occurrence.fiber);
887 if (occurrence.name) |name| try object.field("name", name);
888 if (occurrence.group_hint) |group_hint| try object.field("group_hint", group_hint);
889 if (occurrence.duration_ns != 0) try object.field("duration_ns", occurrence.duration_ns);
890 if (occurrence.unmatched) try object.field("unmatched", true);
891 }
892
893 fn sortSummaries(items: []Summary, sort: Sort) void {
894 std.mem.sort(Summary, items, sort, summaryLessThan);
895 }
896
897 fn summaryLessThan(sort: Sort, left: Summary, right: Summary) bool {
898 return switch (sort) {
899 .running => summaryRunningGreaterThan({}, left, right),
900 .enters => summaryEntersGreaterThan({}, left, right),
901 .leaves => summaryLeavesGreaterThan({}, left, right),
902 .migrations => summaryMigrationsGreaterThan({}, left, right),
903 .unmatched => summaryUnmatchedGreaterThan({}, left, right),
904 .last => summaryLastGreaterThan({}, left, right),
905 .fiber => summaryFiberLessThan({}, left, right),
906 .thread => summaryThreadLessThan({}, left, right),
907 .label => summaryLabelLessThan({}, left, right),
908 };
909 }
910
911 fn summaryRunningGreaterThan(_: void, left: Summary, right: Summary) bool {
912 if (left.running_ns != right.running_ns) return left.running_ns > right.running_ns;
913 return summaryEntersGreaterThan({}, left, right);
914 }
915
916 fn summaryEntersGreaterThan(_: void, left: Summary, right: Summary) bool {
917 if (left.enters != right.enters) return left.enters > right.enters;
918 if (left.count != right.count) return left.count > right.count;
919 return std.mem.lessThan(u8, left.label, right.label);
920 }
921
922 fn summaryLeavesGreaterThan(_: void, left: Summary, right: Summary) bool {
923 if (left.leaves != right.leaves) return left.leaves > right.leaves;
924 return summaryEntersGreaterThan({}, left, right);
925 }
926
927 fn summaryMigrationsGreaterThan(_: void, left: Summary, right: Summary) bool {
928 if (left.migrations != right.migrations) return left.migrations > right.migrations;
929 return summaryEntersGreaterThan({}, left, right);
930 }
931
932 fn summaryUnmatchedGreaterThan(_: void, left: Summary, right: Summary) bool {
933 if (left.unmatched_leaves != right.unmatched_leaves) return left.unmatched_leaves > right.unmatched_leaves;
934 return summaryEntersGreaterThan({}, left, right);
935 }
936
937 fn summaryLastGreaterThan(_: void, left: Summary, right: Summary) bool {
938 if (left.last_ns != right.last_ns) return left.last_ns > right.last_ns;
939 return summaryEntersGreaterThan({}, left, right);
940 }
941
942 fn summaryFiberLessThan(_: void, left: Summary, right: Summary) bool {
943 const left_fiber = left.fiber orelse 0;
944 const right_fiber = right.fiber orelse 0;
945 if (left_fiber != right_fiber) return left_fiber < right_fiber;
946 return summaryEntersGreaterThan({}, left, right);
947 }
948
949 fn summaryThreadLessThan(_: void, left: Summary, right: Summary) bool {
950 const left_thread = left.thread orelse left.from_thread;
951 const right_thread = right.thread orelse right.from_thread;
952 if (left_thread != right_thread) return left_thread < right_thread;
953 return summaryEntersGreaterThan({}, left, right);
954 }
955
956 fn summaryLabelLessThan(_: void, left: Summary, right: Summary) bool {
957 return std.mem.lessThan(u8, left.label, right.label);
958 }
959
960 fn sortOccurrences(items: []Occurrence, sort: Sort) void {
961 switch (sort) {
962 .last => std.mem.sort(Occurrence, items, {}, occurrenceTimeGreaterThan),
963 else => std.mem.sort(Occurrence, items, {}, occurrenceTimeLessThan),
964 }
965 }
966
967 fn occurrenceTimeLessThan(_: void, left: Occurrence, right: Occurrence) bool {
968 if (left.time_ns != right.time_ns) return left.time_ns < right.time_ns;
969 return left.seq < right.seq;
970 }
971
972 fn occurrenceTimeGreaterThan(_: void, left: Occurrence, right: Occurrence) bool {
973 if (left.time_ns != right.time_ns) return left.time_ns > right.time_ns;
974 return left.seq > right.seq;
975 }
976
977 fn noteRange(first_ns: *u64, last_ns: *u64, time_ns: u64) void {
978 if (time_ns == 0) return;
979 if (first_ns.* == 0 or time_ns < first_ns.*) first_ns.* = time_ns;
980 last_ns.* = @max(last_ns.*, time_ns);
981 }
982
983 fn dupeOptional(allocator: std.mem.Allocator, text: ?[]const u8) !?[]u8 {
984 const actual = text orelse return null;
985 return try allocator.dupe(u8, actual);
986 }
987
988 fn contains(haystack: []const u8, needle: []const u8, ignore_case: bool) bool {
989 if (!ignore_case) return std.mem.indexOf(u8, haystack, needle) != null;
990 if (needle.len == 0) return true;
991 if (needle.len > haystack.len) return false;
992 var index: usize = 0;
993 while (index + needle.len <= haystack.len) : (index += 1) {
994 if (asciiEqlIgnoreCase(haystack[index .. index + needle.len], needle)) return true;
995 }
996 return false;
997 }
998
999 fn asciiEqlIgnoreCase(left: []const u8, right: []const u8) bool {
1000 if (left.len != right.len) return false;
1001 for (left, right) |a, b| {
1002 if (std.ascii.toLower(a) != std.ascii.toLower(b)) return false;
1003 }
1004 return true;
1005 }
1006
1007 test "fiber analyzer derives running time migrations and unmatched leaves" {
1008 var trace = std.Io.Writer.Allocating.init(std.testing.allocator);
1009 defer trace.deinit();
1010 try (event.TraceEvent{ .seq = 1, .kind = .start, .time_ns = 90, .thread = 1, .name = "test" }).writeJsonLine(&trace.writer);
1011 try (event.TraceEvent{ .seq = 2, .kind = .thread_name, .time_ns = 95, .thread = 100, .name = "worker-a" }).writeJsonLine(&trace.writer);
1012 try (event.TraceEvent{ .seq = 3, .kind = .thread_name, .time_ns = 96, .thread = 101, .name = "worker-b" }).writeJsonLine(&trace.writer);
1013 try (event.TraceEvent{ .seq = 4, .kind = .fiber_name, .time_ns = 100, .thread = 100, .id = 7, .name = "job.parse", .group_hint = 3 }).writeJsonLine(&trace.writer);
1014 try (event.TraceEvent{ .seq = 5, .kind = .fiber_enter, .time_ns = 110, .thread = 100, .id = 7, .name = "job.parse", .group_hint = 3 }).writeJsonLine(&trace.writer);
1015 try (event.TraceEvent{ .seq = 6, .kind = .fiber_leave, .time_ns = 150, .thread = 100, .id = 7 }).writeJsonLine(&trace.writer);
1016 try (event.TraceEvent{ .seq = 7, .kind = .fiber_enter, .time_ns = 180, .thread = 101, .id = 7 }).writeJsonLine(&trace.writer);
1017 try (event.TraceEvent{ .seq = 8, .kind = .fiber_leave, .time_ns = 230, .thread = 101, .id = 7 }).writeJsonLine(&trace.writer);
1018 try (event.TraceEvent{ .seq = 9, .kind = .fiber_leave, .time_ns = 240, .thread = 101, .id = 9 }).writeJsonLine(&trace.writer);
1019 try (event.TraceEvent{ .seq = 10, .kind = .stop, .time_ns = 260, .thread = 1 }).writeJsonLine(&trace.writer);
1020
1021 var analyzer = Analyzer.init(std.testing.allocator);
1022 defer analyzer.deinit();
1023 try analyzer.ingestJsonlBytes(trace.written());
1024 try std.testing.expectEqual(@as(u64, 2), analyzer.counters.enters);
1025 try std.testing.expectEqual(@as(u64, 3), analyzer.counters.leaves);
1026 try std.testing.expectEqual(@as(u64, 1), analyzer.counters.unmatched_leaves);
1027 try std.testing.expectEqual(@as(u64, 90), analyzer.fibers.get(7).?.running_ns);
1028 try std.testing.expectEqual(@as(u64, 1), analyzer.fibers.get(7).?.migrations);
1029
1030 var out = std.Io.Writer.Allocating.init(std.testing.allocator);
1031 defer out.deinit();
1032 try writeText(std.testing.allocator, &analyzer, &out.writer, .{ .group = .fiber, .sort = .running, .top = 4, .occurrences = 8 });
1033 const text = out.written();
1034 try std.testing.expect(std.mem.indexOf(u8, text, "tracy fibers groups=2 fibers=2 threads=2 enters=2 leaves=3 names=1 running_ns=90 migrations=1 unmatched_leaves=1") != null);
1035 try std.testing.expect(std.mem.indexOf(u8, text, "fiber group=fiber label=\"fiber 7 job.parse\" count=4 fiber=7 thread=101 group_hint=3 enters=2 leaves=2 running_ns=90 migrations=1") != null);
1036 try std.testing.expect(std.mem.indexOf(u8, text, "fiber-occurrence kind=leave time_ns=240 thread=101 fiber=9 unmatched=true") != null);
1037 }
1038
1039 test "fiber jsonl filters by thread fiber and name" {
1040 var trace = std.Io.Writer.Allocating.init(std.testing.allocator);
1041 defer trace.deinit();
1042 try (event.TraceEvent{ .seq = 1, .kind = .fiber_name, .time_ns = 100, .thread = 100, .id = 7, .name = "job.parse" }).writeJsonLine(&trace.writer);
1043 try (event.TraceEvent{ .seq = 2, .kind = .fiber_enter, .time_ns = 110, .thread = 100, .id = 7 }).writeJsonLine(&trace.writer);
1044 try (event.TraceEvent{ .seq = 3, .kind = .fiber_leave, .time_ns = 150, .thread = 100, .id = 7 }).writeJsonLine(&trace.writer);
1045 try (event.TraceEvent{ .seq = 4, .kind = .fiber_name, .time_ns = 160, .thread = 101, .id = 8, .name = "job.render" }).writeJsonLine(&trace.writer);
1046
1047 var analyzer = Analyzer.init(std.testing.allocator);
1048 defer analyzer.deinit();
1049 try analyzer.ingestJsonlBytes(trace.written());
1050
1051 var out = std.Io.Writer.Allocating.init(std.testing.allocator);
1052 defer out.deinit();
1053 try writeJsonl(std.testing.allocator, &analyzer, &out.writer, .{ .group = .fiber, .fiber = 7, .thread = 100, .match = "PARSE", .ignore_case = true });
1054 const text = out.written();
1055 try std.testing.expect(std.mem.indexOf(u8, text, "\"schema\":\"tracy.fibers/v0\"") != null);
1056 try std.testing.expect(std.mem.indexOf(u8, text, "\"kind\":\"summary\",\"groups\":1") != null);
1057 try std.testing.expect(std.mem.indexOf(u8, text, "\"label\":\"fiber 7 job.parse\"") != null);
1058 try std.testing.expect(std.mem.indexOf(u8, text, "\"running_ns\":40") != null);
1059 try std.testing.expect(std.mem.indexOf(u8, text, "job.render") == null);
1060 }