lib/tracy/src/sample.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.samples/v0";
8
9 pub const Group = enum {
10 source,
11 symbol,
12 stack,
13 thread,
14 kind,
15 none,
16
17 pub fn fromName(text: []const u8) ?Group {
18 if (std.mem.eql(u8, text, "source")) return .source;
19 if (std.mem.eql(u8, text, "symbol")) return .symbol;
20 if (std.mem.eql(u8, text, "stack")) return .stack;
21 if (std.mem.eql(u8, text, "thread")) return .thread;
22 if (std.mem.eql(u8, text, "kind")) return .kind;
23 if (std.mem.eql(u8, text, "none")) return .none;
24 return null;
25 }
26
27 fn tag(self: Group) []const u8 {
28 return switch (self) {
29 .source => "source",
30 .symbol => "symbol",
31 .stack => "stack",
32 .thread => "thread",
33 .kind => "kind",
34 .none => "none",
35 };
36 }
37 };
38
39 pub const Sort = enum {
40 samples,
41 weight,
42 last,
43 first,
44 thread,
45 name,
46 kind,
47
48 pub fn fromName(text: []const u8) ?Sort {
49 if (std.mem.eql(u8, text, "samples")) return .samples;
50 if (std.mem.eql(u8, text, "weight")) return .weight;
51 if (std.mem.eql(u8, text, "last")) return .last;
52 if (std.mem.eql(u8, text, "first")) return .first;
53 if (std.mem.eql(u8, text, "thread")) return .thread;
54 if (std.mem.eql(u8, text, "name")) return .name;
55 if (std.mem.eql(u8, text, "kind")) return .kind;
56 return null;
57 }
58
59 fn tag(self: Sort) []const u8 {
60 return switch (self) {
61 .samples => "samples",
62 .weight => "weight",
63 .last => "last",
64 .first => "first",
65 .thread => "thread",
66 .name => "name",
67 .kind => "kind",
68 };
69 }
70 };
71
72 pub const Options = struct {
73 top: usize = 20,
74 occurrences: usize = 80,
75 group: Group = .source,
76 sort: Sort = .samples,
77 min_weight: u64 = 0,
78 thread: ?u64 = null,
79 since_ns: ?u64 = null,
80 until_ns: ?u64 = null,
81 kind: ?[]const u8 = null,
82 match: ?[]const u8 = null,
83 ignore_case: bool = false,
84 };
85
86 pub const Counters = struct {
87 events: u64 = 0,
88 samples: u64 = 0,
89 frames: u64 = 0,
90 filtered: u64 = 0,
91 groups: u64 = 0,
92 duration_ns: u64 = 0,
93 };
94
95 const ThreadName = struct {
96 name: []u8,
97
98 fn deinit(self: *ThreadName, allocator: std.mem.Allocator) void {
99 allocator.free(self.name);
100 self.* = undefined;
101 }
102 };
103
104 const Frame = struct {
105 depth: u32 = 0,
106 name: ?[]u8 = null,
107 function: ?[]u8 = null,
108 file: ?[]u8 = null,
109 line: u32 = 0,
110 column: u32 = 0,
111 address: u64 = 0,
112
113 fn deinit(self: *Frame, allocator: std.mem.Allocator) void {
114 if (self.name) |name| allocator.free(name);
115 if (self.function) |function| allocator.free(function);
116 if (self.file) |file| allocator.free(file);
117 self.* = undefined;
118 }
119 };
120
121 const SourceView = struct {
122 name: ?[]const u8 = null,
123 function: ?[]const u8 = null,
124 file: ?[]const u8 = null,
125 line: u32 = 0,
126 column: u32 = 0,
127 address: u64 = 0,
128 };
129
130 const Sample = struct {
131 id: u64 = 0,
132 seen: bool = false,
133 seq: u64 = 0,
134 time_ns: u64 = 0,
135 thread: u64 = 0,
136 name: ?[]u8 = null,
137 sample_kind: ?[]u8 = null,
138 function: ?[]u8 = null,
139 file: ?[]u8 = null,
140 line: u32 = 0,
141 column: u32 = 0,
142 address: u64 = 0,
143 weight: u64 = 1,
144 frames: std.ArrayListUnmanaged(Frame) = .empty,
145
146 fn deinit(self: *Sample, allocator: std.mem.Allocator) void {
147 self.clearHeader(allocator);
148 for (self.frames.items) |*frame| frame.deinit(allocator);
149 self.frames.deinit(allocator);
150 self.* = undefined;
151 }
152
153 fn clearHeader(self: *Sample, allocator: std.mem.Allocator) void {
154 if (self.name) |name| allocator.free(name);
155 if (self.sample_kind) |sample_kind| allocator.free(sample_kind);
156 if (self.function) |function| allocator.free(function);
157 if (self.file) |file| allocator.free(file);
158 self.name = null;
159 self.sample_kind = null;
160 self.function = null;
161 self.file = null;
162 }
163
164 fn sortFrames(self: *Sample) void {
165 std.mem.sort(Frame, self.frames.items, {}, frameLessThan);
166 }
167
168 fn kind(self: Sample) []const u8 {
169 return self.sample_kind orelse "manual";
170 }
171
172 fn source(self: Sample) SourceView {
173 if (self.file != null or self.function != null or self.address != 0) {
174 return .{
175 .name = self.name,
176 .function = self.function,
177 .file = self.file,
178 .line = self.line,
179 .column = self.column,
180 .address = self.address,
181 };
182 }
183 if (self.frames.items.len != 0) {
184 const frame = self.frames.items[self.frames.items.len - 1];
185 return .{
186 .name = frame.name,
187 .function = frame.function,
188 .file = frame.file,
189 .line = frame.line,
190 .column = frame.column,
191 .address = frame.address,
192 };
193 }
194 return .{ .name = self.name, .address = self.address };
195 }
196 };
197
198 pub const Summary = struct {
199 group: Group,
200 label: []u8,
201 count: u64 = 0,
202 weight: u64 = 0,
203 first_ns: u64 = 0,
204 last_ns: u64 = 0,
205 thread: ?u64 = null,
206 kind: ?[]const u8 = null,
207 name: ?[]const u8 = null,
208 file: ?[]const u8 = null,
209 function: ?[]const u8 = null,
210 line: u32 = 0,
211 column: u32 = 0,
212 address: u64 = 0,
213
214 pub fn deinit(self: *Summary, allocator: std.mem.Allocator) void {
215 allocator.free(self.label);
216 self.* = undefined;
217 }
218 };
219
220 const Occurrence = struct {
221 sample: *const Sample,
222 };
223
224 pub const Analyzer = struct {
225 allocator: std.mem.Allocator,
226 samples: std.AutoHashMapUnmanaged(u64, Sample) = .{},
227 threads: std.AutoHashMapUnmanaged(u64, ThreadName) = .{},
228 counters: Counters = .{},
229 start_ns: ?u64 = null,
230 end_ns: ?u64 = null,
231 next_anonymous_id: u64 = 1,
232
233 pub fn init(allocator: std.mem.Allocator) Analyzer {
234 return .{ .allocator = allocator };
235 }
236
237 pub fn deinit(self: *Analyzer) void {
238 var sample_iter = self.samples.valueIterator();
239 while (sample_iter.next()) |sample| sample.deinit(self.allocator);
240 self.samples.deinit(self.allocator);
241 var thread_iter = self.threads.valueIterator();
242 while (thread_iter.next()) |thread| thread.deinit(self.allocator);
243 self.threads.deinit(self.allocator);
244 self.* = undefined;
245 }
246
247 pub fn ingestJsonlBytes(self: *Analyzer, bytes: []const u8) !void {
248 var lines = std.mem.splitScalar(u8, bytes, '\n');
249 while (lines.next()) |line| try self.ingestJsonLine(line);
250 }
251
252 pub fn ingestJsonLine(self: *Analyzer, line: []const u8) !void {
253 const text = std.mem.trim(u8, line, " \t\r\n");
254 if (text.len == 0) return;
255 var parsed = (try record_mod.parseEventLine(self.allocator, text)) orelse return;
256 defer parsed.deinit();
257 try self.ingest(parsed);
258 }
259
260 pub fn ingest(self: *Analyzer, parsed: event.Parsed) !void {
261 self.counters.events += 1;
262 if (self.start_ns == null and parsed.time_ns != 0) self.start_ns = parsed.time_ns;
263 if (parsed.time_ns != 0) self.end_ns = parsed.time_ns;
264 switch (parsed.kind) {
265 .start => {
266 if (parsed.time_ns != 0) self.start_ns = parsed.time_ns;
267 },
268 .stop => {
269 if (parsed.time_ns != 0) self.end_ns = parsed.time_ns;
270 },
271 .thread_name => try self.recordThreadName(parsed),
272 .sample => try self.recordSample(parsed),
273 .sample_frame => try self.recordFrame(parsed),
274 else => {},
275 }
276 }
277
278 pub fn collectSummaries(self: *Analyzer, allocator: std.mem.Allocator, options: Options) !std.ArrayListUnmanaged(Summary) {
279 self.prepareSamples();
280 var summaries: std.ArrayListUnmanaged(Summary) = .empty;
281 errdefer deinitSummaries(allocator, &summaries);
282 var groups: std.StringHashMapUnmanaged(usize) = .{};
283 defer groups.deinit(allocator);
284
285 self.counters.filtered = 0;
286 var iter = self.samples.valueIterator();
287 while (iter.next()) |sample| {
288 if (!sample.seen) continue;
289 if (!self.sampleMatches(sample.*, options)) {
290 self.counters.filtered += 1;
291 continue;
292 }
293 const label = try self.groupLabel(allocator, sample.*, options.group);
294 errdefer allocator.free(label);
295 const group_entry = try groups.getOrPut(allocator, label);
296 var summary_index: usize = undefined;
297 if (group_entry.found_existing) {
298 allocator.free(label);
299 summary_index = group_entry.value_ptr.*;
300 } else {
301 group_entry.key_ptr.* = label;
302 summary_index = summaries.items.len;
303 group_entry.value_ptr.* = summary_index;
304 const src = sample.source();
305 try summaries.append(allocator, .{
306 .group = options.group,
307 .label = label,
308 .thread = if (options.group == .thread) sample.thread else null,
309 .kind = if (options.group == .kind) sample.kind() else null,
310 .name = src.name,
311 .file = src.file,
312 .function = src.function,
313 .line = src.line,
314 .column = src.column,
315 .address = src.address,
316 });
317 }
318 const summary = &summaries.items[summary_index];
319 summary.count += 1;
320 summary.weight +|= sample.weight;
321 if (summary.first_ns == 0 or sample.time_ns < summary.first_ns) summary.first_ns = sample.time_ns;
322 summary.last_ns = @max(summary.last_ns, sample.time_ns);
323 if (summary.kind == null) summary.kind = sample.kind();
324 if (summary.thread == null and options.group == .thread) summary.thread = sample.thread;
325 }
326
327 self.counters.groups = @intCast(summaries.items.len);
328 self.counters.duration_ns = self.durationNs();
329 sortSummaries(summaries.items, options.sort);
330 return summaries;
331 }
332
333 pub fn collectOccurrences(self: *Analyzer, allocator: std.mem.Allocator, options: Options) !std.ArrayListUnmanaged(Occurrence) {
334 self.prepareSamples();
335 var occurrences: std.ArrayListUnmanaged(Occurrence) = .empty;
336 var iter = self.samples.valueIterator();
337 while (iter.next()) |sample| {
338 if (!sample.seen) continue;
339 if (!self.sampleMatches(sample.*, options)) continue;
340 try occurrences.append(allocator, .{ .sample = sample });
341 }
342 sortOccurrences(occurrences.items, options.sort);
343 return occurrences;
344 }
345
346 pub fn durationNs(self: Analyzer) u64 {
347 const start_ns = self.start_ns orelse return 0;
348 const end_ns = self.end_ns orelse return 0;
349 if (end_ns <= start_ns) return 0;
350 return end_ns - start_ns;
351 }
352
353 fn recordThreadName(self: *Analyzer, parsed: event.Parsed) !void {
354 const name = parsed.name orelse return;
355 const entry = try self.threads.getOrPut(self.allocator, parsed.thread);
356 if (entry.found_existing) entry.value_ptr.deinit(self.allocator);
357 entry.value_ptr.* = .{ .name = try self.allocator.dupe(u8, name) };
358 }
359
360 fn recordSample(self: *Analyzer, parsed: event.Parsed) !void {
361 const key = self.sampleKey(parsed);
362 const entry = try self.samples.getOrPut(self.allocator, key);
363 if (!entry.found_existing) {
364 entry.value_ptr.* = .{ .id = parsed.id };
365 } else {
366 entry.value_ptr.clearHeader(self.allocator);
367 }
368 entry.value_ptr.* = .{
369 .id = parsed.id,
370 .seen = true,
371 .seq = parsed.seq,
372 .time_ns = parsed.time_ns,
373 .thread = parsed.thread,
374 .name = try dupeOptional(self.allocator, parsed.name),
375 .sample_kind = try dupeOptional(self.allocator, parsed.sample_kind),
376 .function = try dupeOptional(self.allocator, parsed.function),
377 .file = try dupeOptional(self.allocator, parsed.file),
378 .line = parsed.line,
379 .column = parsed.column,
380 .address = parsed.address,
381 .weight = parsed.value_u64 orelse 1,
382 .frames = entry.value_ptr.frames,
383 };
384 self.counters.samples += 1;
385 }
386
387 fn recordFrame(self: *Analyzer, parsed: event.Parsed) !void {
388 if (parsed.id == 0) return;
389 const entry = try self.samples.getOrPut(self.allocator, parsed.id);
390 if (!entry.found_existing) entry.value_ptr.* = .{ .id = parsed.id };
391 try entry.value_ptr.frames.append(self.allocator, .{
392 .depth = parsed.depth,
393 .name = try dupeOptional(self.allocator, parsed.name),
394 .function = try dupeOptional(self.allocator, parsed.function),
395 .file = try dupeOptional(self.allocator, parsed.file),
396 .line = parsed.line,
397 .column = parsed.column,
398 .address = parsed.address,
399 });
400 self.counters.frames += 1;
401 }
402
403 fn sampleKey(self: *Analyzer, parsed: event.Parsed) u64 {
404 if (parsed.id != 0) return parsed.id;
405 const key = std.math.maxInt(u64) - self.next_anonymous_id;
406 self.next_anonymous_id +|= 1;
407 return key;
408 }
409
410 fn prepareSamples(self: *Analyzer) void {
411 var iter = self.samples.valueIterator();
412 while (iter.next()) |sample| sample.sortFrames();
413 }
414
415 fn sampleMatches(self: *Analyzer, sample: Sample, options: Options) bool {
416 if (options.thread) |thread| {
417 if (sample.thread != thread) return false;
418 }
419 if (options.since_ns) |since_ns| {
420 if (sample.time_ns < since_ns) return false;
421 }
422 if (options.until_ns) |until_ns| {
423 if (sample.time_ns > until_ns) return false;
424 }
425 if (sample.weight < options.min_weight) return false;
426 if (options.kind) |kind| {
427 if (!textEquals(sample.kind(), kind, options.ignore_case)) return false;
428 }
429 if (options.match) |needle| {
430 if (!self.sampleContains(sample, needle, options.ignore_case)) return false;
431 }
432 return true;
433 }
434
435 fn sampleContains(self: *Analyzer, sample: Sample, needle: []const u8, ignore_case: bool) bool {
436 if (sample.name) |name| if (contains(name, needle, ignore_case)) return true;
437 if (contains(sample.kind(), needle, ignore_case)) return true;
438 if (sample.file) |file| if (contains(file, needle, ignore_case)) return true;
439 if (sample.function) |function| if (contains(function, needle, ignore_case)) return true;
440 if (self.threadName(sample.thread)) |thread_name| if (contains(thread_name, needle, ignore_case)) return true;
441 for (sample.frames.items) |frame| {
442 if (frame.name) |name| if (contains(name, needle, ignore_case)) return true;
443 if (frame.file) |file| if (contains(file, needle, ignore_case)) return true;
444 if (frame.function) |function| if (contains(function, needle, ignore_case)) return true;
445 }
446 return false;
447 }
448
449 fn groupLabel(self: *Analyzer, allocator: std.mem.Allocator, sample: Sample, group: Group) ![]u8 {
450 var writer = std.Io.Writer.Allocating.init(allocator);
451 defer writer.deinit();
452 switch (group) {
453 .source => try writeSourceLabel(&writer.writer, sample),
454 .symbol => try writeSymbolLabel(&writer.writer, sample),
455 .stack => try writeStackLabel(&writer.writer, sample),
456 .thread => {
457 try writer.writer.print("thread {d}", .{sample.thread});
458 if (self.threadName(sample.thread)) |thread_name| {
459 try writer.writer.writeAll(" ");
460 try writer.writer.writeAll(thread_name);
461 }
462 },
463 .kind => try writer.writer.writeAll(sample.kind()),
464 .none => try writer.writer.print("sample {d} {d}", .{ sample.id, sample.time_ns }),
465 }
466 return try allocator.dupe(u8, writer.written());
467 }
468
469 fn threadName(self: *Analyzer, thread: u64) ?[]const u8 {
470 const entry = self.threads.get(thread) orelse return null;
471 return entry.name;
472 }
473 };
474
475 pub fn deinitSummaries(allocator: std.mem.Allocator, summaries: *std.ArrayListUnmanaged(Summary)) void {
476 for (summaries.items) |*summary| summary.deinit(allocator);
477 summaries.deinit(allocator);
478 }
479
480 pub fn writeTextFromJsonlPath(
481 allocator: std.mem.Allocator,
482 path: []const u8,
483 writer: *std.Io.Writer,
484 options: Options,
485 ) !void {
486 return report.writeFromJsonlPath(Analyzer, writeText, allocator, path, writer, options);
487 }
488
489 pub fn writeJsonlFromJsonlPath(
490 allocator: std.mem.Allocator,
491 path: []const u8,
492 writer: *std.Io.Writer,
493 options: Options,
494 ) !void {
495 return report.writeFromJsonlPath(Analyzer, writeJsonl, allocator, path, writer, options);
496 }
497
498 pub fn ingestPath(analyzer: *Analyzer, path: []const u8) !void {
499 return report.ingestJsonlPath(analyzer, path);
500 }
501
502 fn writeText(
503 allocator: std.mem.Allocator,
504 analyzer: *Analyzer,
505 writer: *std.Io.Writer,
506 options: Options,
507 ) !void {
508 var summaries = try analyzer.collectSummaries(allocator, options);
509 defer deinitSummaries(allocator, &summaries);
510 var occurrences = try analyzer.collectOccurrences(allocator, options);
511 defer occurrences.deinit(allocator);
512
513 try writer.print(
514 "tracy samples groups={d} samples={d} frames={d} filtered={d} duration_ns={d} group={s} sort={s}\n",
515 .{
516 summaries.items.len,
517 analyzer.counters.samples,
518 analyzer.counters.frames,
519 analyzer.counters.filtered,
520 analyzer.durationNs(),
521 options.group.tag(),
522 options.sort.tag(),
523 },
524 );
525 const summary_limit = @min(options.top, summaries.items.len);
526 for (summaries.items[0..summary_limit]) |summary| {
527 try writer.print("sample group={s} label=", .{summary.group.tag()});
528 try pretty_json.writeString(writer, summary.label);
529 try writer.print(" count={d} weight={d} first_ns={d} last_ns={d}", .{ summary.count, summary.weight, summary.first_ns, summary.last_ns });
530 if (summary.kind) |kind| {
531 try writer.writeAll(" kind=");
532 try pretty_json.writeString(writer, kind);
533 }
534 if (summary.thread) |thread| try writer.print(" thread={d}", .{thread});
535 if (summary.file) |file| try writer.print(" file={s}:{d}", .{ file, summary.line });
536 if (summary.function) |function| {
537 try writer.writeAll(" function=");
538 try pretty_json.writeString(writer, function);
539 }
540 if (summary.address != 0) try writer.print(" address={d}", .{summary.address});
541 try writer.writeByte('\n');
542 }
543
544 const occurrence_limit = @min(options.occurrences, occurrences.items.len);
545 for (occurrences.items[0..occurrence_limit]) |occurrence| {
546 const sample = occurrence.sample;
547 try writer.print("sample-occurrence time_ns={d} thread={d}", .{ sample.time_ns, sample.thread });
548 if (sample.id != 0) try writer.print(" id={d}", .{sample.id});
549 if (sample.name) |name| {
550 try writer.writeAll(" name=");
551 try pretty_json.writeString(writer, name);
552 }
553 try writer.writeAll(" kind=");
554 try pretty_json.writeString(writer, sample.kind());
555 try writer.print(" weight={d}", .{sample.weight});
556 try writeSourceText(writer, sample.*);
557 if (sample.frames.items.len != 0) {
558 const stack = try stackLabel(allocator, sample.*);
559 defer allocator.free(stack);
560 try writer.writeAll(" stack=");
561 try pretty_json.writeString(writer, stack);
562 }
563 try writer.writeByte('\n');
564 }
565 }
566
567 fn writeJsonl(
568 allocator: std.mem.Allocator,
569 analyzer: *Analyzer,
570 writer: *std.Io.Writer,
571 options: Options,
572 ) !void {
573 var summaries = try analyzer.collectSummaries(allocator, options);
574 defer deinitSummaries(allocator, &summaries);
575 var occurrences = try analyzer.collectOccurrences(allocator, options);
576 defer occurrences.deinit(allocator);
577
578 var summary_stream = pretty_json.Writer.init(writer, .minified);
579 const summary_record = try summary_stream.object();
580 try summary_record.field("schema", schema);
581 try summary_record.field("kind", "summary");
582 try summary_record.field("groups", summaries.items.len);
583 try summary_record.field("samples", analyzer.counters.samples);
584 try summary_record.field("frames", analyzer.counters.frames);
585 try summary_record.field("filtered", analyzer.counters.filtered);
586 try summary_record.field("duration_ns", analyzer.durationNs());
587 try summary_record.field("group", options.group.tag());
588 try summary_record.field("sort", options.sort.tag());
589 try summary_record.endLine();
590
591 const summary_limit = @min(options.top, summaries.items.len);
592 for (summaries.items[0..summary_limit]) |summary| {
593 var stream = pretty_json.Writer.init(writer, .minified);
594 const object = try stream.object();
595 try object.field("schema", schema);
596 try object.field("kind", "group");
597 try object.field("group", summary.group.tag());
598 try object.field("label", summary.label);
599 try object.field("count", summary.count);
600 try object.field("weight", summary.weight);
601 try object.field("first_ns", summary.first_ns);
602 try object.field("last_ns", summary.last_ns);
603 if (summary.kind) |kind| try object.field("sample_kind", kind);
604 if (summary.thread) |thread| try object.field("thread", thread);
605 if (summary.file) |file| {
606 try object.field("file", file);
607 try object.field("line", summary.line);
608 }
609 if (summary.function) |function| try object.field("function", function);
610 if (summary.address != 0) try object.field("address", summary.address);
611 try object.endLine();
612 }
613
614 const occurrence_limit = @min(options.occurrences, occurrences.items.len);
615 for (occurrences.items[0..occurrence_limit]) |occurrence| {
616 const sample = occurrence.sample;
617 var stream = pretty_json.Writer.init(writer, .minified);
618 const object = try stream.object();
619 try object.field("schema", schema);
620 try object.field("kind", "sample");
621 try object.field("time_ns", sample.time_ns);
622 try object.field("thread", sample.thread);
623 if (sample.id != 0) try object.field("id", sample.id);
624 if (sample.name) |name| try object.field("name", name);
625 try object.field("sample_kind", sample.kind());
626 try object.field("weight", sample.weight);
627 try writeSourceFields(object, sample.*);
628 if (sample.frames.items.len != 0) {
629 const stack = try stackLabel(allocator, sample.*);
630 defer allocator.free(stack);
631 try object.field("stack", stack);
632 }
633 try object.endLine();
634 }
635 }
636
637 fn writeSourceText(writer: *std.Io.Writer, sample: Sample) !void {
638 const src = sample.source();
639 if (src.file) |file| try writer.print(" file={s}:{d}", .{ file, src.line });
640 if (src.function) |function| {
641 try writer.writeAll(" function=");
642 try pretty_json.writeString(writer, function);
643 }
644 if (src.address != 0) try writer.print(" address={d}", .{src.address});
645 }
646
647 fn writeSourceFields(object: pretty_json.Object, sample: Sample) !void {
648 const src = sample.source();
649 if (src.file) |file| {
650 try object.field("file", file);
651 try object.field("line", src.line);
652 }
653 if (src.function) |function| try object.field("function", function);
654 if (src.address != 0) try object.field("address", src.address);
655 }
656
657 fn writeSourceLabel(writer: *std.Io.Writer, sample: Sample) !void {
658 const src = sample.source();
659 if (src.file) |file| {
660 try writer.print("{s}:{d}", .{ file, src.line });
661 } else {
662 try writeSymbolLabel(writer, sample);
663 }
664 }
665
666 fn writeSymbolLabel(writer: *std.Io.Writer, sample: Sample) !void {
667 const src = sample.source();
668 if (src.function) |function| {
669 try writer.writeAll(function);
670 } else if (src.name) |name| {
671 try writer.writeAll(name);
672 } else if (sample.name) |name| {
673 try writer.writeAll(name);
674 } else if (src.address != 0) {
675 try writer.print("0x{x}", .{src.address});
676 } else {
677 try writer.writeAll("<sample>");
678 }
679 }
680
681 fn writeStackLabel(writer: *std.Io.Writer, sample: Sample) !void {
682 if (sample.frames.items.len == 0) return try writeSymbolLabel(writer, sample);
683 for (sample.frames.items, 0..) |frame, index| {
684 if (index != 0) try writer.writeByte(';');
685 try writeFrameLabel(writer, frame);
686 }
687 }
688
689 fn writeFrameLabel(writer: *std.Io.Writer, frame: Frame) !void {
690 if (frame.name) |name| {
691 try writer.writeAll(name);
692 } else if (frame.function) |function| {
693 try writer.writeAll(function);
694 } else if (frame.file) |file| {
695 try writer.print("{s}:{d}", .{ file, frame.line });
696 } else if (frame.address != 0) {
697 try writer.print("0x{x}", .{frame.address});
698 } else {
699 try writer.writeAll("<frame>");
700 }
701 }
702
703 fn stackLabel(allocator: std.mem.Allocator, sample: Sample) ![]u8 {
704 var writer = std.Io.Writer.Allocating.init(allocator);
705 defer writer.deinit();
706 try writeStackLabel(&writer.writer, sample);
707 return try allocator.dupe(u8, writer.written());
708 }
709
710 fn dupeOptional(allocator: std.mem.Allocator, text: ?[]const u8) !?[]u8 {
711 const actual = text orelse return null;
712 return try allocator.dupe(u8, actual);
713 }
714
715 fn frameLessThan(_: void, left: Frame, right: Frame) bool {
716 if (left.depth != right.depth) return left.depth < right.depth;
717 return left.address < right.address;
718 }
719
720 fn sortSummaries(items: []Summary, sort: Sort) void {
721 std.mem.sort(Summary, items, sort, summaryLessThan);
722 }
723
724 fn summaryLessThan(sort: Sort, left: Summary, right: Summary) bool {
725 return switch (sort) {
726 .samples => summarySamplesGreaterThan({}, left, right),
727 .weight => summaryWeightGreaterThan({}, left, right),
728 .last => summaryLastGreaterThan({}, left, right),
729 .first => summaryFirstLessThan({}, left, right),
730 .thread => summaryThreadLessThan({}, left, right),
731 .name => summaryNameLessThan({}, left, right),
732 .kind => summaryKindLessThan({}, left, right),
733 };
734 }
735
736 fn summarySamplesGreaterThan(_: void, left: Summary, right: Summary) bool {
737 if (left.count != right.count) return left.count > right.count;
738 if (left.weight != right.weight) return left.weight > right.weight;
739 return std.mem.lessThan(u8, left.label, right.label);
740 }
741
742 fn summaryWeightGreaterThan(_: void, left: Summary, right: Summary) bool {
743 if (left.weight != right.weight) return left.weight > right.weight;
744 return summarySamplesGreaterThan({}, left, right);
745 }
746
747 fn summaryLastGreaterThan(_: void, left: Summary, right: Summary) bool {
748 if (left.last_ns != right.last_ns) return left.last_ns > right.last_ns;
749 return summarySamplesGreaterThan({}, left, right);
750 }
751
752 fn summaryFirstLessThan(_: void, left: Summary, right: Summary) bool {
753 if (left.first_ns != right.first_ns) return left.first_ns < right.first_ns;
754 return summarySamplesGreaterThan({}, left, right);
755 }
756
757 fn summaryThreadLessThan(_: void, left: Summary, right: Summary) bool {
758 const left_thread = left.thread orelse 0;
759 const right_thread = right.thread orelse 0;
760 if (left_thread != right_thread) return left_thread < right_thread;
761 return summarySamplesGreaterThan({}, left, right);
762 }
763
764 fn summaryNameLessThan(_: void, left: Summary, right: Summary) bool {
765 return std.mem.lessThan(u8, left.label, right.label);
766 }
767
768 fn summaryKindLessThan(_: void, left: Summary, right: Summary) bool {
769 const left_kind = left.kind orelse "";
770 const right_kind = right.kind orelse "";
771 if (!std.mem.eql(u8, left_kind, right_kind)) return std.mem.lessThan(u8, left_kind, right_kind);
772 return summarySamplesGreaterThan({}, left, right);
773 }
774
775 fn sortOccurrences(items: []Occurrence, sort: Sort) void {
776 switch (sort) {
777 .last, .weight => std.mem.sort(Occurrence, items, {}, occurrenceTimeGreaterThan),
778 else => std.mem.sort(Occurrence, items, {}, occurrenceTimeLessThan),
779 }
780 }
781
782 fn occurrenceTimeLessThan(_: void, left: Occurrence, right: Occurrence) bool {
783 if (left.sample.time_ns != right.sample.time_ns) return left.sample.time_ns < right.sample.time_ns;
784 return left.sample.seq < right.sample.seq;
785 }
786
787 fn occurrenceTimeGreaterThan(_: void, left: Occurrence, right: Occurrence) bool {
788 if (left.sample.time_ns != right.sample.time_ns) return left.sample.time_ns > right.sample.time_ns;
789 return left.sample.seq > right.sample.seq;
790 }
791
792 fn contains(haystack: []const u8, needle: []const u8, ignore_case: bool) bool {
793 if (!ignore_case) return std.mem.indexOf(u8, haystack, needle) != null;
794 if (needle.len == 0) return true;
795 if (needle.len > haystack.len) return false;
796 var index: usize = 0;
797 while (index + needle.len <= haystack.len) : (index += 1) {
798 if (asciiEqlIgnoreCase(haystack[index .. index + needle.len], needle)) return true;
799 }
800 return false;
801 }
802
803 fn textEquals(left: []const u8, right: []const u8, ignore_case: bool) bool {
804 if (!ignore_case) return std.mem.eql(u8, left, right);
805 return asciiEqlIgnoreCase(left, right);
806 }
807
808 fn asciiEqlIgnoreCase(left: []const u8, right: []const u8) bool {
809 if (left.len != right.len) return false;
810 for (left, right) |a, b| {
811 if (std.ascii.toLower(a) != std.ascii.toLower(b)) return false;
812 }
813 return true;
814 }
815
816 test "samples aggregate stacks and source locations" {
817 var trace = std.Io.Writer.Allocating.init(std.testing.allocator);
818 defer trace.deinit();
819 try (event.TraceEvent{ .seq = 1, .kind = .start, .time_ns = 90, .thread = 7, .name = "test" }).writeJsonLine(&trace.writer);
820 try (event.TraceEvent{ .seq = 2, .kind = .thread_name, .time_ns = 95, .thread = 7, .name = "worker" }).writeJsonLine(&trace.writer);
821 try (event.TraceEvent{ .seq = 3, .kind = .sample, .time_ns = 100, .thread = 7, .id = 1, .name = "tick", .function = "leaf", .file = "leaf.zig", .line = 9, .sample_kind = "cpu" }).writeJsonLine(&trace.writer);
822 try (event.TraceEvent{ .seq = 4, .kind = .sample_frame, .time_ns = 100, .thread = 7, .id = 1, .depth = 0, .name = "root", .function = "root", .file = "root.zig", .line = 1 }).writeJsonLine(&trace.writer);
823 try (event.TraceEvent{ .seq = 5, .kind = .sample_frame, .time_ns = 100, .thread = 7, .id = 1, .depth = 1, .name = "leaf", .function = "leaf", .file = "leaf.zig", .line = 9 }).writeJsonLine(&trace.writer);
824 try (event.TraceEvent{ .seq = 6, .kind = .sample, .time_ns = 120, .thread = 7, .id = 2, .name = "tick", .function = "leaf", .file = "leaf.zig", .line = 9, .value_u64 = 2, .sample_kind = "cpu" }).writeJsonLine(&trace.writer);
825 try (event.TraceEvent{ .seq = 7, .kind = .sample_frame, .time_ns = 120, .thread = 7, .id = 2, .depth = 0, .name = "root" }).writeJsonLine(&trace.writer);
826 try (event.TraceEvent{ .seq = 8, .kind = .sample_frame, .time_ns = 120, .thread = 7, .id = 2, .depth = 1, .name = "leaf" }).writeJsonLine(&trace.writer);
827 try (event.TraceEvent{ .seq = 9, .kind = .stop, .time_ns = 130, .thread = 7 }).writeJsonLine(&trace.writer);
828
829 var analyzer = Analyzer.init(std.testing.allocator);
830 defer analyzer.deinit();
831 try analyzer.ingestJsonlBytes(trace.written());
832 try std.testing.expectEqual(@as(u64, 2), analyzer.counters.samples);
833 try std.testing.expectEqual(@as(u64, 4), analyzer.counters.frames);
834
835 var out = std.Io.Writer.Allocating.init(std.testing.allocator);
836 defer out.deinit();
837 try writeText(std.testing.allocator, &analyzer, &out.writer, .{ .group = .stack, .sort = .samples, .top = 4, .occurrences = 4 });
838 const text = out.written();
839 try std.testing.expect(std.mem.indexOf(u8, text, "tracy samples groups=1 samples=2 frames=4") != null);
840 try std.testing.expect(std.mem.indexOf(u8, text, "label=\"root;leaf\" count=2 weight=3") != null);
841 try std.testing.expect(std.mem.indexOf(u8, text, "sample-occurrence time_ns=100 thread=7 id=1 name=\"tick\" kind=\"cpu\" weight=1 file=leaf.zig:9 function=\"leaf\" stack=\"root;leaf\"") != null);
842 }
843
844 test "samples jsonl filters by kind thread and match text" {
845 var trace = std.Io.Writer.Allocating.init(std.testing.allocator);
846 defer trace.deinit();
847 try (event.TraceEvent{ .seq = 1, .kind = .sample, .time_ns = 100, .thread = 1, .id = 1, .name = "idle", .function = "idle", .file = "idle.zig", .line = 1, .sample_kind = "wall" }).writeJsonLine(&trace.writer);
848 try (event.TraceEvent{ .seq = 2, .kind = .sample, .time_ns = 110, .thread = 2, .id = 2, .name = "parse", .function = "parse", .file = "parse.zig", .line = 7, .sample_kind = "cpu" }).writeJsonLine(&trace.writer);
849 try (event.TraceEvent{ .seq = 3, .kind = .sample_frame, .time_ns = 110, .thread = 2, .id = 2, .name = "parse" }).writeJsonLine(&trace.writer);
850
851 var analyzer = Analyzer.init(std.testing.allocator);
852 defer analyzer.deinit();
853 try analyzer.ingestJsonlBytes(trace.written());
854
855 var out = std.Io.Writer.Allocating.init(std.testing.allocator);
856 defer out.deinit();
857 try writeJsonl(std.testing.allocator, &analyzer, &out.writer, .{ .group = .source, .kind = "cpu", .thread = 2, .match = "PARSE", .ignore_case = true });
858 const text = out.written();
859 try std.testing.expect(std.mem.indexOf(u8, text, "\"schema\":\"tracy.samples/v0\"") != null);
860 try std.testing.expect(std.mem.indexOf(u8, text, "\"kind\":\"summary\",\"groups\":1") != null);
861 try std.testing.expect(std.mem.indexOf(u8, text, "\"label\":\"parse.zig:7\"") != null);
862 try std.testing.expect(std.mem.indexOf(u8, text, "\"idle\"") == null);
863 }