lib/tracy/src/find.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const pretty_json = @import("pretty").json;
3 const event = @import("event.zig");
4 const tree = @import("tree.zig");
5
6 pub const schema = "tracy.find/v0";
7
8 pub const Group = enum {
9 source,
10 name,
11 thread,
12 parent,
13 text,
14 none,
15
16 pub fn fromName(text: []const u8) ?Group {
17 if (std.mem.eql(u8, text, "source")) return .source;
18 if (std.mem.eql(u8, text, "name")) return .name;
19 if (std.mem.eql(u8, text, "thread")) return .thread;
20 if (std.mem.eql(u8, text, "parent")) return .parent;
21 if (std.mem.eql(u8, text, "text")) return .text;
22 if (std.mem.eql(u8, text, "none")) return .none;
23 return null;
24 }
25
26 fn tag(self: Group) []const u8 {
27 return switch (self) {
28 .source => "source",
29 .name => "name",
30 .thread => "thread",
31 .parent => "parent",
32 .text => "text",
33 .none => "none",
34 };
35 }
36 };
37
38 pub const Sort = enum {
39 total,
40 self,
41 count,
42 mean,
43 max,
44 name,
45 time,
46
47 pub fn fromName(text: []const u8) ?Sort {
48 if (std.mem.eql(u8, text, "total")) return .total;
49 if (std.mem.eql(u8, text, "self")) return .self;
50 if (std.mem.eql(u8, text, "count")) return .count;
51 if (std.mem.eql(u8, text, "mean")) return .mean;
52 if (std.mem.eql(u8, text, "max")) return .max;
53 if (std.mem.eql(u8, text, "name")) return .name;
54 if (std.mem.eql(u8, text, "time")) return .time;
55 return null;
56 }
57
58 fn tag(self: Sort) []const u8 {
59 return switch (self) {
60 .total => "total",
61 .self => "self",
62 .count => "count",
63 .mean => "mean",
64 .max => "max",
65 .name => "name",
66 .time => "time",
67 };
68 }
69 };
70
71 pub const Options = struct {
72 pattern: []const u8,
73 top: usize = 20,
74 occurrences: usize = 20,
75 group: Group = .source,
76 sort: Sort = .total,
77 thread: ?u64 = null,
78 since_ns: ?u64 = null,
79 until_ns: ?u64 = null,
80 min_duration_ns: u64 = 0,
81 ignore_case: bool = false,
82 };
83
84 pub const Counters = struct {
85 spans: u64 = 0,
86 matched_spans: u64 = 0,
87 open_spans: u64 = 0,
88 superseded_spans: u64 = 0,
89 invalid_duration_spans: u64 = 0,
90 zero_duration_spans: u64 = 0,
91 filtered_spans: u64 = 0,
92 clipped_spans: u64 = 0,
93 groups: u64 = 0,
94 duration_ns: u64 = 0,
95 };
96
97 const GroupRow = struct {
98 group: Group,
99 label: []u8,
100 name: ?[]u8 = null,
101 text: ?[]u8 = null,
102 file: ?[]u8 = null,
103 function: ?[]u8 = null,
104 line: u32 = 0,
105 column: u32 = 0,
106 thread: ?u64 = null,
107 parent_id: ?u64 = null,
108 count: u64 = 0,
109 total_ns: u64 = 0,
110 self_ns: u64 = 0,
111 min_ns: u64 = std.math.maxInt(u64),
112 max_ns: u64 = 0,
113 first_ns: u64 = 0,
114 last_ns: u64 = 0,
115 samples: std.ArrayListUnmanaged(u64) = .empty,
116 threads: std.ArrayListUnmanaged(u64) = .empty,
117
118 fn deinit(self: *GroupRow, allocator: std.mem.Allocator) void {
119 allocator.free(self.label);
120 if (self.name) |name| allocator.free(name);
121 if (self.text) |text| allocator.free(text);
122 if (self.file) |file| allocator.free(file);
123 if (self.function) |function| allocator.free(function);
124 self.samples.deinit(allocator);
125 self.threads.deinit(allocator);
126 self.* = undefined;
127 }
128
129 fn meanNs(self: GroupRow) u64 {
130 if (self.count == 0) return 0;
131 return self.total_ns / self.count;
132 }
133
134 fn threadCount(self: GroupRow) u64 {
135 return @intCast(self.threads.items.len);
136 }
137 };
138
139 const Occurrence = struct {
140 id: u64,
141 parent_id: ?u64 = null,
142 parent_name: ?[]u8 = null,
143 name: []u8,
144 text: ?[]u8 = null,
145 file: ?[]u8 = null,
146 function: ?[]u8 = null,
147 line: u32 = 0,
148 column: u32 = 0,
149 thread: u64 = 0,
150 depth: usize = 0,
151 start_ns: u64 = 0,
152 end_ns: u64 = 0,
153 duration_ns: u64 = 0,
154 self_ns: u64 = 0,
155 children: u64 = 0,
156
157 fn deinit(self: *Occurrence, allocator: std.mem.Allocator) void {
158 if (self.parent_name) |parent_name| allocator.free(parent_name);
159 allocator.free(self.name);
160 if (self.text) |text| allocator.free(text);
161 if (self.file) |file| allocator.free(file);
162 if (self.function) |function| allocator.free(function);
163 self.* = undefined;
164 }
165 };
166
167 const GroupView = struct {
168 group: Group,
169 label: []const u8,
170 name: ?[]const u8,
171 text: ?[]const u8,
172 file: ?[]const u8,
173 function: ?[]const u8,
174 line: u32,
175 column: u32,
176 thread: ?u64,
177 parent_id: ?u64,
178 count: u64,
179 total_ns: u64,
180 self_ns: u64,
181 mean_ns: u64,
182 min_ns: u64,
183 p50_ns: u64,
184 p90_ns: u64,
185 p99_ns: u64,
186 max_ns: u64,
187 first_ns: u64,
188 last_ns: u64,
189 threads: u64,
190 };
191
192 pub const Analyzer = struct {
193 allocator: std.mem.Allocator,
194 groups: std.StringHashMapUnmanaged(GroupRow) = .{},
195 occurrences: std.ArrayListUnmanaged(Occurrence) = .empty,
196 counters: Counters = .{},
197 evidence: ?tree.Evidence = null,
198
199 pub fn init(allocator: std.mem.Allocator) Analyzer {
200 return .{ .allocator = allocator };
201 }
202
203 pub fn deinit(self: *Analyzer) void {
204 var iter = self.groups.iterator();
205 while (iter.next()) |entry| {
206 self.allocator.free(entry.key_ptr.*);
207 entry.value_ptr.deinit(self.allocator);
208 }
209 self.groups.deinit(self.allocator);
210 for (self.occurrences.items) |*occurrence| occurrence.deinit(self.allocator);
211 self.occurrences.deinit(self.allocator);
212 self.* = undefined;
213 }
214
215 pub fn ingestTree(self: *Analyzer, trace: *tree.Analyzer, options: Options) !void {
216 self.counters.duration_ns = trace.durationNs();
217 self.evidence = trace.evidence();
218 var spans = try trace.collectSpans(self.allocator);
219 defer spans.deinit(self.allocator);
220 self.counters.spans = @intCast(spans.items.len);
221 for (spans.items) |span| {
222 switch (span.state) {
223 .open => {
224 self.counters.open_spans += 1;
225 continue;
226 },
227 .superseded => {
228 self.counters.superseded_spans += 1;
229 continue;
230 },
231 .complete => {},
232 }
233 if (!span.duration_valid) {
234 self.counters.invalid_duration_spans += 1;
235 continue;
236 }
237 if (!spanMatches(span, options)) {
238 self.counters.filtered_spans += 1;
239 continue;
240 }
241 const duration = clippedDuration(span, options) orelse {
242 if (span.total_ns == 0) self.counters.zero_duration_spans += 1;
243 self.counters.filtered_spans += 1;
244 continue;
245 };
246 if (duration < options.min_duration_ns) {
247 self.counters.filtered_spans += 1;
248 continue;
249 }
250 const parent = parentSpan(spans.items, span.parent_id);
251 try self.record(span, parent, duration, options);
252 self.counters.matched_spans += 1;
253 if (duration != span.total_ns) self.counters.clipped_spans += 1;
254 }
255 self.counters.groups = @intCast(self.groups.count());
256 sortOccurrences(self.occurrences.items, options.sort);
257 }
258
259 fn record(self: *Analyzer, span: tree.Span, parent: ?tree.Span, duration: u64, options: Options) !void {
260 var key_writer = std.Io.Writer.Allocating.init(self.allocator);
261 defer key_writer.deinit();
262 try writeGroupKey(&key_writer.writer, span, parent, options);
263 const key_text = key_writer.written();
264 const entry = try self.groups.getOrPut(self.allocator, key_text);
265 if (!entry.found_existing) {
266 const owned_key = try self.allocator.dupe(u8, key_text);
267 entry.key_ptr.* = owned_key;
268 entry.value_ptr.* = try self.newGroup(span, parent, options);
269 }
270 const row = entry.value_ptr;
271 row.count += 1;
272 row.total_ns +|= duration;
273 row.self_ns +|= @min(span.self_ns, duration);
274 row.min_ns = @min(row.min_ns, duration);
275 row.max_ns = @max(row.max_ns, duration);
276 if (row.count == 1 or span.start_ns < row.first_ns) row.first_ns = span.start_ns;
277 if (span.end_ns > row.last_ns) row.last_ns = span.end_ns;
278 try row.samples.append(self.allocator, duration);
279 try appendThread(self.allocator, &row.threads, span.thread);
280 try self.occurrences.append(self.allocator, try occurrenceView(self.allocator, span, parent, duration));
281 }
282
283 fn newGroup(self: *Analyzer, span: tree.Span, parent: ?tree.Span, options: Options) !GroupRow {
284 const label = try groupLabel(self.allocator, span, parent, options.group);
285 return .{
286 .group = options.group,
287 .label = label,
288 .name = try dupeOptional(self.allocator, span.name),
289 .text = try dupeOptional(self.allocator, span.text),
290 .file = try dupeOptional(self.allocator, span.file),
291 .function = try dupeOptional(self.allocator, span.function),
292 .line = span.line,
293 .column = span.column,
294 .thread = if (options.group == .thread) span.thread else null,
295 .parent_id = if (options.group == .parent) span.parent_id else null,
296 };
297 }
298
299 fn collectGroups(self: *Analyzer, allocator: std.mem.Allocator, options: Options) !std.ArrayListUnmanaged(GroupView) {
300 var views: std.ArrayListUnmanaged(GroupView) = .empty;
301 var iter = self.groups.valueIterator();
302 while (iter.next()) |row| {
303 std.mem.sort(u64, row.samples.items, {}, u64LessThan);
304 try views.append(allocator, .{
305 .group = row.group,
306 .label = row.label,
307 .name = row.name,
308 .text = row.text,
309 .file = row.file,
310 .function = row.function,
311 .line = row.line,
312 .column = row.column,
313 .thread = row.thread,
314 .parent_id = row.parent_id,
315 .count = row.count,
316 .total_ns = row.total_ns,
317 .self_ns = row.self_ns,
318 .mean_ns = row.meanNs(),
319 .min_ns = if (row.min_ns == std.math.maxInt(u64)) 0 else row.min_ns,
320 .p50_ns = percentile(row.samples.items, 50),
321 .p90_ns = percentile(row.samples.items, 90),
322 .p99_ns = percentile(row.samples.items, 99),
323 .max_ns = row.max_ns,
324 .first_ns = row.first_ns,
325 .last_ns = row.last_ns,
326 .threads = row.threadCount(),
327 });
328 }
329 sortGroups(views.items, options.sort);
330 return views;
331 }
332 };
333
334 pub fn writeTextFromJsonlPath(
335 allocator: std.mem.Allocator,
336 path: []const u8,
337 writer: *std.Io.Writer,
338 options: Options,
339 ) !void {
340 var trace = tree.Analyzer.init(allocator);
341 defer trace.deinit();
342 try tree.ingestPath(&trace, path);
343 var analyzer = Analyzer.init(allocator);
344 defer analyzer.deinit();
345 try analyzer.ingestTree(&trace, options);
346 try writeText(allocator, &analyzer, writer, options);
347 }
348
349 pub fn writeJsonlFromJsonlPath(
350 allocator: std.mem.Allocator,
351 path: []const u8,
352 writer: *std.Io.Writer,
353 options: Options,
354 ) !void {
355 var trace = tree.Analyzer.init(allocator);
356 defer trace.deinit();
357 try tree.ingestPath(&trace, path);
358 var analyzer = Analyzer.init(allocator);
359 defer analyzer.deinit();
360 try analyzer.ingestTree(&trace, options);
361 try writeJsonl(allocator, &analyzer, writer, options);
362 }
363
364 fn writeText(allocator: std.mem.Allocator, analyzer: *Analyzer, writer: *std.Io.Writer, options: Options) !void {
365 var groups = try analyzer.collectGroups(allocator, options);
366 defer groups.deinit(allocator);
367 try writer.writeAll("tracy find pattern=");
368 try pretty_json.writeString(writer, options.pattern);
369 try writer.print(
370 " groups={d} occurrences={d} spans={d} matched_spans={d} " ++
371 "open_spans={d} superseded_spans={d} invalid_duration_spans={d} " ++
372 "zero_duration_spans={d} filtered_spans={d} clipped_spans={d} " ++
373 "duration_ns={d} group={s} sort={s}\n",
374 .{
375 groups.items.len,
376 analyzer.occurrences.items.len,
377 analyzer.counters.spans,
378 analyzer.counters.matched_spans,
379 analyzer.counters.open_spans,
380 analyzer.counters.superseded_spans,
381 analyzer.counters.invalid_duration_spans,
382 analyzer.counters.zero_duration_spans,
383 analyzer.counters.filtered_spans,
384 analyzer.counters.clipped_spans,
385 analyzer.counters.duration_ns,
386 options.group.tag(),
387 options.sort.tag(),
388 },
389 );
390 try analyzer.evidence.?.writeText(writer);
391 const group_limit = @min(options.top, groups.items.len);
392 for (groups.items[0..group_limit]) |group| {
393 try writer.print("find-group group={s} label=", .{group.group.tag()});
394 try pretty_json.writeString(writer, group.label);
395 try writer.print(
396 " count={d} total_ns={d} self_ns={d} mean_ns={d} min_ns={d} p50_ns={d} p90_ns={d} p99_ns={d} max_ns={d} first_ns={d} last_ns={d} threads={d}",
397 .{
398 group.count,
399 group.total_ns,
400 group.self_ns,
401 group.mean_ns,
402 group.min_ns,
403 group.p50_ns,
404 group.p90_ns,
405 group.p99_ns,
406 group.max_ns,
407 group.first_ns,
408 group.last_ns,
409 group.threads,
410 },
411 );
412 try writeGroupMetadataText(writer, group);
413 try writer.writeByte('\n');
414 }
415 const occurrence_limit = @min(options.occurrences, analyzer.occurrences.items.len);
416 for (analyzer.occurrences.items[0..occurrence_limit]) |occurrence| {
417 try writeOccurrenceText(writer, occurrence);
418 }
419 }
420
421 fn writeJsonl(allocator: std.mem.Allocator, analyzer: *Analyzer, writer: *std.Io.Writer, options: Options) !void {
422 var groups = try analyzer.collectGroups(allocator, options);
423 defer groups.deinit(allocator);
424 var summary_stream = pretty_json.Writer.init(writer, .minified);
425 const summary_record = try summary_stream.object();
426 try summary_record.field("schema", schema);
427 try summary_record.field("kind", "summary");
428 try summary_record.field("pattern", options.pattern);
429 try summary_record.field("groups", groups.items.len);
430 try summary_record.field("occurrences", analyzer.occurrences.items.len);
431 try summary_record.field("spans", analyzer.counters.spans);
432 try summary_record.field("matched_spans", analyzer.counters.matched_spans);
433 try summary_record.field("open_spans", analyzer.counters.open_spans);
434 try summary_record.field("superseded_spans", analyzer.counters.superseded_spans);
435 try summary_record.field("invalid_duration_spans", analyzer.counters.invalid_duration_spans);
436 try summary_record.field("zero_duration_spans", analyzer.counters.zero_duration_spans);
437 try summary_record.field("filtered_spans", analyzer.counters.filtered_spans);
438 try summary_record.field("clipped_spans", analyzer.counters.clipped_spans);
439 try summary_record.field("duration_ns", analyzer.counters.duration_ns);
440 try summary_record.field("group", options.group.tag());
441 try summary_record.field("sort", options.sort.tag());
442 try analyzer.evidence.?.writeFields(summary_record);
443 try summary_record.endLine();
444 const group_limit = @min(options.top, groups.items.len);
445 for (groups.items[0..group_limit]) |group| {
446 var stream = pretty_json.Writer.init(writer, .minified);
447 const object = try stream.object();
448 try object.field("schema", schema);
449 try object.field("kind", "group");
450 try object.field("group", group.group.tag());
451 try object.field("label", group.label);
452 try object.field("count", group.count);
453 try object.field("total_ns", group.total_ns);
454 try object.field("self_ns", group.self_ns);
455 try object.field("mean_ns", group.mean_ns);
456 try object.field("min_ns", group.min_ns);
457 try object.field("p50_ns", group.p50_ns);
458 try object.field("p90_ns", group.p90_ns);
459 try object.field("p99_ns", group.p99_ns);
460 try object.field("max_ns", group.max_ns);
461 try object.field("first_ns", group.first_ns);
462 try object.field("last_ns", group.last_ns);
463 try object.field("threads", group.threads);
464 try writeGroupMetadataFields(object, group);
465 try object.endLine();
466 }
467 const occurrence_limit = @min(options.occurrences, analyzer.occurrences.items.len);
468 for (analyzer.occurrences.items[0..occurrence_limit]) |occurrence| {
469 try writeOccurrenceJson(writer, occurrence);
470 }
471 }
472
473 fn writeOccurrenceText(writer: *std.Io.Writer, occurrence: Occurrence) !void {
474 try writer.print(
475 "find-zone id={d} start_ns={d} end_ns={d} duration_ns={d} self_ns={d} thread={d} depth={d} children={d} name=",
476 .{ occurrence.id, occurrence.start_ns, occurrence.end_ns, occurrence.duration_ns, occurrence.self_ns, occurrence.thread, occurrence.depth, occurrence.children },
477 );
478 try pretty_json.writeString(writer, occurrence.name);
479 if (occurrence.parent_id) |parent_id| try writer.print(" parent_id={d}", .{parent_id});
480 if (occurrence.parent_name) |parent_name| {
481 try writer.writeAll(" parent=");
482 try pretty_json.writeString(writer, parent_name);
483 }
484 if (occurrence.text) |text| {
485 try writer.writeAll(" text=");
486 try pretty_json.writeString(writer, text);
487 }
488 if (occurrence.file) |file| try writer.print(" file={s}:{d}", .{ file, occurrence.line });
489 if (occurrence.function) |function| {
490 try writer.writeAll(" function=");
491 try pretty_json.writeString(writer, function);
492 }
493 try writer.writeByte('\n');
494 }
495
496 fn writeOccurrenceJson(writer: *std.Io.Writer, occurrence: Occurrence) !void {
497 var stream = pretty_json.Writer.init(writer, .minified);
498 const object = try stream.object();
499 try object.field("schema", schema);
500 try object.field("kind", "zone");
501 try object.field("id", occurrence.id);
502 try object.field("start_ns", occurrence.start_ns);
503 try object.field("end_ns", occurrence.end_ns);
504 try object.field("duration_ns", occurrence.duration_ns);
505 try object.field("self_ns", occurrence.self_ns);
506 try object.field("thread", occurrence.thread);
507 try object.field("depth", occurrence.depth);
508 try object.field("children", occurrence.children);
509 try object.field("name", occurrence.name);
510 if (occurrence.parent_id) |parent_id| try object.field("parent_id", parent_id);
511 if (occurrence.parent_name) |parent_name| try object.field("parent_name", parent_name);
512 if (occurrence.text) |text| try object.field("text", text);
513 if (occurrence.file) |file| {
514 try object.field("file", file);
515 try object.field("line", occurrence.line);
516 }
517 if (occurrence.function) |function| try object.field("function", function);
518 try object.endLine();
519 }
520
521 fn writeGroupMetadataText(writer: *std.Io.Writer, group: GroupView) !void {
522 if (group.thread) |thread| try writer.print(" thread={d}", .{thread});
523 if (group.parent_id) |parent_id| try writer.print(" parent_id={d}", .{parent_id});
524 if (group.name) |name| {
525 try writer.writeAll(" name=");
526 try pretty_json.writeString(writer, name);
527 }
528 if (group.text) |text| {
529 try writer.writeAll(" text=");
530 try pretty_json.writeString(writer, text);
531 }
532 if (group.file) |file| try writer.print(" file={s}:{d}", .{ file, group.line });
533 if (group.function) |function| {
534 try writer.writeAll(" function=");
535 try pretty_json.writeString(writer, function);
536 }
537 }
538
539 fn writeGroupMetadataFields(object: pretty_json.Object, group: GroupView) !void {
540 if (group.thread) |thread| try object.field("thread", thread);
541 if (group.parent_id) |parent_id| try object.field("parent_id", parent_id);
542 if (group.name) |name| try object.field("name", name);
543 if (group.text) |text| try object.field("text", text);
544 if (group.file) |file| {
545 try object.field("file", file);
546 try object.field("line", group.line);
547 }
548 if (group.function) |function| try object.field("function", function);
549 }
550
551 fn occurrenceView(allocator: std.mem.Allocator, span: tree.Span, parent: ?tree.Span, duration: u64) !Occurrence {
552 return .{
553 .id = span.id,
554 .parent_id = span.parent_id,
555 .parent_name = if (parent) |actual| try allocator.dupe(u8, actual.name) else null,
556 .name = try allocator.dupe(u8, span.name),
557 .text = try dupeOptional(allocator, span.text),
558 .file = try dupeOptional(allocator, span.file),
559 .function = try dupeOptional(allocator, span.function),
560 .line = span.line,
561 .column = span.column,
562 .thread = span.thread,
563 .depth = span.depth,
564 .start_ns = span.start_ns,
565 .end_ns = span.end_ns,
566 .duration_ns = duration,
567 .self_ns = @min(span.self_ns, duration),
568 .children = span.children,
569 };
570 }
571
572 fn spanMatches(span: tree.Span, options: Options) bool {
573 if (options.thread) |thread| {
574 if (span.thread != thread) return false;
575 }
576 const pattern = options.pattern;
577 if (pattern.len == 0) return true;
578 if (contains(span.name, pattern, options.ignore_case)) return true;
579 if (span.text) |text| if (contains(text, pattern, options.ignore_case)) return true;
580 if (span.file) |file| if (contains(file, pattern, options.ignore_case)) return true;
581 if (span.function) |function| if (contains(function, pattern, options.ignore_case)) return true;
582 return false;
583 }
584
585 fn clippedDuration(span: tree.Span, options: Options) ?u64 {
586 var start_ns = span.start_ns;
587 var end_ns = span.end_ns;
588 if (options.since_ns) |since_ns| start_ns = @max(start_ns, since_ns);
589 if (options.until_ns) |until_ns| end_ns = @min(end_ns, until_ns);
590 if (end_ns <= start_ns) return null;
591 return end_ns - start_ns;
592 }
593
594 fn parentSpan(spans: []const tree.Span, parent_id: ?u64) ?tree.Span {
595 const id = parent_id orelse return null;
596 for (spans) |span| {
597 if (span.id == id) return span;
598 }
599 return null;
600 }
601
602 fn writeGroupKey(writer: *std.Io.Writer, span: tree.Span, parent: ?tree.Span, options: Options) !void {
603 switch (options.group) {
604 .source => {
605 try writer.writeAll(span.name);
606 try writer.writeByte('\x1f');
607 if (span.file) |file| try writer.writeAll(file);
608 try writer.writeByte('\x1f');
609 if (span.function) |function| try writer.writeAll(function);
610 try writer.print("\x1f{d}:{d}", .{ span.line, span.column });
611 },
612 .name => try writer.writeAll(span.name),
613 .thread => try writer.print("thread:{d}", .{span.thread}),
614 .parent => {
615 if (parent) |actual| {
616 try writer.print("parent:{d}:", .{actual.id});
617 try writer.writeAll(actual.name);
618 } else {
619 try writer.writeAll("parent:root");
620 }
621 },
622 .text => try writer.writeAll(span.text orelse ""),
623 .none => try writer.print("zone:{d}", .{span.id}),
624 }
625 }
626
627 fn groupLabel(allocator: std.mem.Allocator, span: tree.Span, parent: ?tree.Span, group: Group) ![]u8 {
628 var writer = std.Io.Writer.Allocating.init(allocator);
629 defer writer.deinit();
630 switch (group) {
631 .source => try writer.writer.writeAll(span.name),
632 .name => try writer.writer.writeAll(span.name),
633 .thread => try writer.writer.print("thread {d}", .{span.thread}),
634 .parent => {
635 if (parent) |actual| {
636 try writer.writer.writeAll(actual.name);
637 } else {
638 try writer.writer.writeAll("<root>");
639 }
640 },
641 .text => try writer.writer.writeAll(span.text orelse ""),
642 .none => try writer.writer.print("{s}#{d}", .{ span.name, span.id }),
643 }
644 return try allocator.dupe(u8, writer.written());
645 }
646
647 fn contains(haystack: []const u8, needle: []const u8, ignore_case: bool) bool {
648 if (!ignore_case) return std.mem.indexOf(u8, haystack, needle) != null;
649 if (needle.len == 0) return true;
650 if (needle.len > haystack.len) return false;
651 var index: usize = 0;
652 while (index + needle.len <= haystack.len) : (index += 1) {
653 if (asciiEqlIgnoreCase(haystack[index .. index + needle.len], needle)) return true;
654 }
655 return false;
656 }
657
658 fn asciiEqlIgnoreCase(left: []const u8, right: []const u8) bool {
659 if (left.len != right.len) return false;
660 for (left, right) |a, b| {
661 if (std.ascii.toLower(a) != std.ascii.toLower(b)) return false;
662 }
663 return true;
664 }
665
666 fn appendThread(allocator: std.mem.Allocator, threads: *std.ArrayListUnmanaged(u64), thread: u64) !void {
667 for (threads.items) |existing| {
668 if (existing == thread) return;
669 }
670 try threads.append(allocator, thread);
671 }
672
673 fn dupeOptional(allocator: std.mem.Allocator, text: ?[]const u8) !?[]u8 {
674 const actual = text orelse return null;
675 return try allocator.dupe(u8, actual);
676 }
677
678 fn percentile(sorted: []const u64, percent: usize) u64 {
679 if (sorted.len == 0) return 0;
680 const index = @min(sorted.len - 1, (sorted.len - 1) * percent / 100);
681 return sorted[index];
682 }
683
684 fn sortGroups(items: []GroupView, sort: Sort) void {
685 switch (sort) {
686 .total => std.mem.sort(GroupView, items, {}, groupTotalGreaterThan),
687 .self => std.mem.sort(GroupView, items, {}, groupSelfGreaterThan),
688 .count => std.mem.sort(GroupView, items, {}, groupCountGreaterThan),
689 .mean => std.mem.sort(GroupView, items, {}, groupMeanGreaterThan),
690 .max => std.mem.sort(GroupView, items, {}, groupMaxGreaterThan),
691 .name => std.mem.sort(GroupView, items, {}, groupNameLessThan),
692 .time => std.mem.sort(GroupView, items, {}, groupTimeLessThan),
693 }
694 }
695
696 fn sortOccurrences(items: []Occurrence, sort: Sort) void {
697 switch (sort) {
698 .time => std.mem.sort(Occurrence, items, {}, occurrenceTimeLessThan),
699 .name => std.mem.sort(Occurrence, items, {}, occurrenceNameLessThan),
700 else => std.mem.sort(Occurrence, items, {}, occurrenceDurationGreaterThan),
701 }
702 }
703
704 fn groupTotalGreaterThan(_: void, left: GroupView, right: GroupView) bool {
705 if (left.total_ns != right.total_ns) return left.total_ns > right.total_ns;
706 return groupNameLessThan({}, left, right);
707 }
708
709 fn groupSelfGreaterThan(_: void, left: GroupView, right: GroupView) bool {
710 if (left.self_ns != right.self_ns) return left.self_ns > right.self_ns;
711 return groupTotalGreaterThan({}, left, right);
712 }
713
714 fn groupCountGreaterThan(_: void, left: GroupView, right: GroupView) bool {
715 if (left.count != right.count) return left.count > right.count;
716 return groupTotalGreaterThan({}, left, right);
717 }
718
719 fn groupMeanGreaterThan(_: void, left: GroupView, right: GroupView) bool {
720 if (left.mean_ns != right.mean_ns) return left.mean_ns > right.mean_ns;
721 return groupTotalGreaterThan({}, left, right);
722 }
723
724 fn groupMaxGreaterThan(_: void, left: GroupView, right: GroupView) bool {
725 if (left.max_ns != right.max_ns) return left.max_ns > right.max_ns;
726 return groupTotalGreaterThan({}, left, right);
727 }
728
729 fn groupTimeLessThan(_: void, left: GroupView, right: GroupView) bool {
730 if (left.first_ns != right.first_ns) return left.first_ns < right.first_ns;
731 return groupNameLessThan({}, left, right);
732 }
733
734 fn groupNameLessThan(_: void, left: GroupView, right: GroupView) bool {
735 const label_cmp = std.mem.order(u8, left.label, right.label);
736 if (label_cmp != .eq) return label_cmp == .lt;
737 return left.first_ns < right.first_ns;
738 }
739
740 fn occurrenceDurationGreaterThan(_: void, left: Occurrence, right: Occurrence) bool {
741 if (left.duration_ns != right.duration_ns) return left.duration_ns > right.duration_ns;
742 return occurrenceTimeLessThan({}, left, right);
743 }
744
745 fn occurrenceTimeLessThan(_: void, left: Occurrence, right: Occurrence) bool {
746 if (left.start_ns != right.start_ns) return left.start_ns < right.start_ns;
747 return left.id < right.id;
748 }
749
750 fn occurrenceNameLessThan(_: void, left: Occurrence, right: Occurrence) bool {
751 const name_cmp = std.mem.order(u8, left.name, right.name);
752 if (name_cmp != .eq) return name_cmp == .lt;
753 return occurrenceTimeLessThan({}, left, right);
754 }
755
756 fn u64LessThan(_: void, left: u64, right: u64) bool {
757 return left < right;
758 }
759
760 test "find aggregates matched zones by source" {
761 var trace_bytes = std.Io.Writer.Allocating.init(std.testing.allocator);
762 defer trace_bytes.deinit();
763 try (event.TraceEvent{ .seq = 1, .kind = .zone_begin, .time_ns = 100, .thread = 1, .id = 1, .name = "root", .file = "root.zig", .line = 1 }).writeJsonLine(&trace_bytes.writer);
764 try (event.TraceEvent{ .seq = 2, .kind = .zone_begin, .time_ns = 120, .thread = 1, .id = 2, .name = "child phase", .file = "child.zig", .line = 2 }).writeJsonLine(&trace_bytes.writer);
765 try (event.TraceEvent{ .seq = 3, .kind = .zone_end, .time_ns = 150, .thread = 1, .id = 2 }).writeJsonLine(&trace_bytes.writer);
766 try (event.TraceEvent{ .seq = 4, .kind = .zone_end, .time_ns = 200, .thread = 1, .id = 1 }).writeJsonLine(&trace_bytes.writer);
767 try (event.TraceEvent{ .seq = 5, .kind = .zone_begin, .time_ns = 210, .thread = 2, .id = 3, .name = "Child phase", .file = "child.zig", .line = 2 }).writeJsonLine(&trace_bytes.writer);
768 try (event.TraceEvent{ .seq = 6, .kind = .zone_end, .time_ns = 260, .thread = 2, .id = 3 }).writeJsonLine(&trace_bytes.writer);
769
770 var trace = tree.Analyzer.init(std.testing.allocator);
771 defer trace.deinit();
772 try trace.ingestJsonlBytes(trace_bytes.written());
773
774 var analyzer = Analyzer.init(std.testing.allocator);
775 defer analyzer.deinit();
776 const options = Options{ .pattern = "child", .ignore_case = true };
777 try analyzer.ingestTree(&trace, options);
778
779 var out = std.Io.Writer.Allocating.init(std.testing.allocator);
780 defer out.deinit();
781 try writeText(std.testing.allocator, &analyzer, &out.writer, options);
782 const text = out.written();
783 try std.testing.expect(std.mem.indexOf(u8, text, "tracy find pattern=\"child\" groups=2 occurrences=2 spans=3 matched_spans=2") != null);
784 try std.testing.expect(std.mem.indexOf(u8, text, "find-group group=source label=\"Child phase\" count=1 total_ns=50") != null);
785 try std.testing.expect(std.mem.indexOf(u8, text, "find-group group=source label=\"child phase\" count=1 total_ns=30") != null);
786 try std.testing.expect(std.mem.indexOf(u8, text, "find-zone id=3 start_ns=210 end_ns=260 duration_ns=50 self_ns=50 thread=2") != null);
787 }
788
789 test "find jsonl groups by parent and clips windowed matches" {
790 var trace_bytes = std.Io.Writer.Allocating.init(std.testing.allocator);
791 defer trace_bytes.deinit();
792 try (event.TraceEvent{ .seq = 1, .kind = .zone_begin, .time_ns = 100, .thread = 1, .id = 1, .name = "root" }).writeJsonLine(&trace_bytes.writer);
793 try (event.TraceEvent{ .seq = 2, .kind = .zone_begin, .time_ns = 120, .thread = 1, .id = 2, .name = "leaf" }).writeJsonLine(&trace_bytes.writer);
794 try (event.TraceEvent{ .seq = 3, .kind = .zone_text, .time_ns = 125, .thread = 1, .id = 2, .text = "queue wait" }).writeJsonLine(&trace_bytes.writer);
795 try (event.TraceEvent{ .seq = 4, .kind = .zone_end, .time_ns = 180, .thread = 1, .id = 2 }).writeJsonLine(&trace_bytes.writer);
796 try (event.TraceEvent{ .seq = 5, .kind = .zone_end, .time_ns = 220, .thread = 1, .id = 1 }).writeJsonLine(&trace_bytes.writer);
797
798 var trace = tree.Analyzer.init(std.testing.allocator);
799 defer trace.deinit();
800 try trace.ingestJsonlBytes(trace_bytes.written());
801
802 var analyzer = Analyzer.init(std.testing.allocator);
803 defer analyzer.deinit();
804 const options = Options{ .pattern = "queue", .group = .parent, .since_ns = 130, .until_ns = 170, .occurrences = 4 };
805 try analyzer.ingestTree(&trace, options);
806
807 var out = std.Io.Writer.Allocating.init(std.testing.allocator);
808 defer out.deinit();
809 try writeJsonl(std.testing.allocator, &analyzer, &out.writer, options);
810 const text = out.written();
811 try std.testing.expect(std.mem.indexOf(u8, text, "\"schema\":\"tracy.find/v0\"") != null);
812 try std.testing.expect(std.mem.indexOf(u8, text, "\"kind\":\"summary\",\"pattern\":\"queue\",\"groups\":1,\"occurrences\":1") != null);
813 try std.testing.expect(std.mem.indexOf(u8, text, "\"group\":\"parent\",\"label\":\"root\"") != null);
814 try std.testing.expect(std.mem.indexOf(u8, text, "\"clipped_spans\":1") != null);
815 try std.testing.expect(std.mem.indexOf(u8, text, "\"duration_ns\":40") != null);
816 try std.testing.expect(std.mem.indexOf(u8, text, "\"text\":\"queue wait\"") != null);
817 }