lib/tracy/src/message.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const pretty_json = @import("pretty").json;
3 const sys = @import("sys");
4 const event = @import("event.zig");
5 const record_mod = @import("record.zig");
6
7 pub const schema = "tracy.messages/v0";
8
9 pub const Kind = enum {
10 all,
11 message,
12 app_info,
13
14 pub fn fromName(text: []const u8) ?Kind {
15 if (std.mem.eql(u8, text, "all")) return .all;
16 if (std.mem.eql(u8, text, "message")) return .message;
17 if (std.mem.eql(u8, text, "app-info")) return .app_info;
18 return null;
19 }
20
21 fn tag(self: Kind) []const u8 {
22 return switch (self) {
23 .all => "all",
24 .message => "message",
25 .app_info => "app-info",
26 };
27 }
28 };
29
30 pub const Group = enum {
31 thread,
32 text,
33 kind,
34 none,
35
36 pub fn fromName(text: []const u8) ?Group {
37 if (std.mem.eql(u8, text, "thread")) return .thread;
38 if (std.mem.eql(u8, text, "text")) return .text;
39 if (std.mem.eql(u8, text, "kind")) return .kind;
40 if (std.mem.eql(u8, text, "none")) return .none;
41 return null;
42 }
43
44 fn tag(self: Group) []const u8 {
45 return switch (self) {
46 .thread => "thread",
47 .text => "text",
48 .kind => "kind",
49 .none => "none",
50 };
51 }
52 };
53
54 pub const Sort = enum {
55 time,
56 count,
57 last,
58 thread,
59 text,
60 kind,
61
62 pub fn fromName(text: []const u8) ?Sort {
63 if (std.mem.eql(u8, text, "time")) return .time;
64 if (std.mem.eql(u8, text, "count")) return .count;
65 if (std.mem.eql(u8, text, "last")) return .last;
66 if (std.mem.eql(u8, text, "thread")) return .thread;
67 if (std.mem.eql(u8, text, "text")) return .text;
68 if (std.mem.eql(u8, text, "kind")) return .kind;
69 return null;
70 }
71
72 fn tag(self: Sort) []const u8 {
73 return switch (self) {
74 .time => "time",
75 .count => "count",
76 .last => "last",
77 .thread => "thread",
78 .text => "text",
79 .kind => "kind",
80 };
81 }
82 };
83
84 pub const Options = struct {
85 top: usize = 20,
86 occurrences: usize = 100,
87 kind: Kind = .all,
88 group: Group = .thread,
89 sort: Sort = .time,
90 thread: ?u64 = null,
91 since_ns: ?u64 = null,
92 until_ns: ?u64 = null,
93 match: ?[]const u8 = null,
94 ignore_case: bool = false,
95 };
96
97 pub const Counters = struct {
98 events: u64 = 0,
99 messages: u64 = 0,
100 app_infos: u64 = 0,
101 thread_names: u64 = 0,
102 filtered: u64 = 0,
103 groups: u64 = 0,
104 duration_ns: u64 = 0,
105 };
106
107 const ThreadName = struct {
108 name: []u8,
109
110 fn deinit(self: *ThreadName, allocator: std.mem.Allocator) void {
111 allocator.free(self.name);
112 self.* = undefined;
113 }
114 };
115
116 const Entry = struct {
117 kind: Kind,
118 seq: u64 = 0,
119 time_ns: u64 = 0,
120 thread: u64 = 0,
121 thread_name: ?[]u8 = null,
122 text: []u8,
123 color: ?u32 = null,
124
125 fn deinit(self: *Entry, allocator: std.mem.Allocator) void {
126 if (self.thread_name) |thread_name| allocator.free(thread_name);
127 allocator.free(self.text);
128 self.* = undefined;
129 }
130 };
131
132 const GroupRow = struct {
133 group: Group,
134 label: []u8,
135 kind: ?Kind = null,
136 thread: ?u64 = null,
137 thread_name: ?[]u8 = null,
138 text: ?[]u8 = null,
139 count: u64 = 0,
140 first_ns: u64 = 0,
141 last_ns: u64 = 0,
142
143 fn deinit(self: *GroupRow, allocator: std.mem.Allocator) void {
144 allocator.free(self.label);
145 if (self.thread_name) |thread_name| allocator.free(thread_name);
146 if (self.text) |text| allocator.free(text);
147 self.* = undefined;
148 }
149 };
150
151 const GroupView = struct {
152 group: Group,
153 label: []const u8,
154 kind: ?Kind,
155 thread: ?u64,
156 thread_name: ?[]const u8,
157 text: ?[]const u8,
158 count: u64,
159 first_ns: u64,
160 last_ns: u64,
161 };
162
163 pub const Analyzer = struct {
164 allocator: std.mem.Allocator,
165 groups: std.StringHashMapUnmanaged(GroupRow) = .{},
166 entries: std.ArrayListUnmanaged(Entry) = .empty,
167 threads: std.AutoHashMapUnmanaged(u64, ThreadName) = .{},
168 counters: Counters = .{},
169 start_ns: ?u64 = null,
170 end_ns: ?u64 = null,
171
172 pub fn init(allocator: std.mem.Allocator) Analyzer {
173 return .{ .allocator = allocator };
174 }
175
176 pub fn deinit(self: *Analyzer) void {
177 var group_iter = self.groups.iterator();
178 while (group_iter.next()) |entry| {
179 self.allocator.free(entry.key_ptr.*);
180 entry.value_ptr.deinit(self.allocator);
181 }
182 self.groups.deinit(self.allocator);
183 for (self.entries.items) |*entry| entry.deinit(self.allocator);
184 self.entries.deinit(self.allocator);
185 var thread_iter = self.threads.valueIterator();
186 while (thread_iter.next()) |thread| thread.deinit(self.allocator);
187 self.threads.deinit(self.allocator);
188 self.* = undefined;
189 }
190
191 pub fn ingestJsonlBytes(self: *Analyzer, bytes: []const u8, options: Options) !void {
192 var lines = std.mem.splitScalar(u8, bytes, '\n');
193 while (lines.next()) |line| try self.ingestJsonLine(line, options);
194 self.finish(options);
195 }
196
197 pub fn ingestJsonLine(self: *Analyzer, line: []const u8, options: Options) !void {
198 const text = std.mem.trim(u8, line, " \t\r\n");
199 if (text.len == 0) return;
200 var parsed = (try record_mod.parseEventLine(self.allocator, text)) orelse return;
201 defer parsed.deinit();
202 try self.ingest(parsed, options);
203 }
204
205 pub fn ingest(self: *Analyzer, parsed: event.Parsed, options: Options) !void {
206 self.counters.events += 1;
207 if (self.start_ns == null and parsed.time_ns != 0) self.start_ns = parsed.time_ns;
208 if (parsed.time_ns != 0) self.end_ns = parsed.time_ns;
209 switch (parsed.kind) {
210 .thread_name => try self.recordThreadName(parsed),
211 .message => {
212 self.counters.messages += 1;
213 try self.recordEntry(.message, parsed, options);
214 },
215 .app_info => {
216 self.counters.app_infos += 1;
217 try self.recordEntry(.app_info, parsed, options);
218 },
219 else => {},
220 }
221 }
222
223 pub fn finish(self: *Analyzer, options: Options) void {
224 self.counters.groups = @intCast(self.groups.count());
225 self.counters.duration_ns = self.durationNs();
226 sortEntries(self.entries.items, options.sort);
227 }
228
229 fn recordThreadName(self: *Analyzer, parsed: event.Parsed) !void {
230 const name = parsed.name orelse return;
231 const entry = try self.threads.getOrPut(self.allocator, parsed.thread);
232 if (entry.found_existing) entry.value_ptr.deinit(self.allocator);
233 entry.value_ptr.* = .{ .name = try self.allocator.dupe(u8, name) };
234 self.counters.thread_names += 1;
235 }
236
237 fn recordEntry(self: *Analyzer, kind: Kind, parsed: event.Parsed, options: Options) !void {
238 const text = parsed.text orelse "";
239 if (!entryMatches(kind, parsed, text, options)) {
240 self.counters.filtered += 1;
241 return;
242 }
243 const thread_name = self.threadName(parsed.thread);
244 var entry = Entry{
245 .kind = kind,
246 .seq = parsed.seq,
247 .time_ns = parsed.time_ns,
248 .thread = parsed.thread,
249 .thread_name = try dupeOptional(self.allocator, thread_name),
250 .text = try self.allocator.dupe(u8, text),
251 .color = parsed.color,
252 };
253 var owns_entry = true;
254 errdefer if (owns_entry) entry.deinit(self.allocator);
255 try self.recordGroup(entry, options);
256 try self.entries.append(self.allocator, entry);
257 owns_entry = false;
258 }
259
260 fn recordGroup(self: *Analyzer, entry: Entry, options: Options) !void {
261 var key_writer = std.Io.Writer.Allocating.init(self.allocator);
262 defer key_writer.deinit();
263 try writeGroupKey(&key_writer.writer, entry, options.group);
264 const key_text = key_writer.written();
265 const group_entry = try self.groups.getOrPut(self.allocator, key_text);
266 if (!group_entry.found_existing) {
267 const owned_key = try self.allocator.dupe(u8, key_text);
268 group_entry.key_ptr.* = owned_key;
269 group_entry.value_ptr.* = try self.newGroup(entry, options.group);
270 }
271 const row = group_entry.value_ptr;
272 row.count += 1;
273 if (row.count == 1 or entry.time_ns < row.first_ns) row.first_ns = entry.time_ns;
274 if (entry.time_ns > row.last_ns) row.last_ns = entry.time_ns;
275 }
276
277 fn newGroup(self: *Analyzer, entry: Entry, group: Group) !GroupRow {
278 const label = try groupLabel(self.allocator, entry, group);
279 errdefer self.allocator.free(label);
280 const thread_name = if (group == .thread) try dupeOptional(self.allocator, entry.thread_name) else null;
281 errdefer if (thread_name) |actual| self.allocator.free(actual);
282 const text = if (group == .text) try self.allocator.dupe(u8, entry.text) else null;
283 errdefer if (text) |actual| self.allocator.free(actual);
284 return .{
285 .group = group,
286 .label = label,
287 .kind = if (group == .kind) entry.kind else null,
288 .thread = if (group == .thread) entry.thread else null,
289 .thread_name = thread_name,
290 .text = text,
291 };
292 }
293
294 fn collectGroups(self: *Analyzer, allocator: std.mem.Allocator, options: Options) !std.ArrayListUnmanaged(GroupView) {
295 var views: std.ArrayListUnmanaged(GroupView) = .empty;
296 var iter = self.groups.valueIterator();
297 while (iter.next()) |row| {
298 try views.append(allocator, .{
299 .group = row.group,
300 .label = row.label,
301 .kind = row.kind,
302 .thread = row.thread,
303 .thread_name = row.thread_name,
304 .text = row.text,
305 .count = row.count,
306 .first_ns = row.first_ns,
307 .last_ns = row.last_ns,
308 });
309 }
310 sortGroups(views.items, options.sort);
311 return views;
312 }
313
314 fn threadName(self: Analyzer, thread: u64) ?[]const u8 {
315 if (self.threads.get(thread)) |name| return name.name;
316 return null;
317 }
318
319 fn durationNs(self: Analyzer) u64 {
320 const start_ns = self.start_ns orelse return 0;
321 const end_ns = self.end_ns orelse return 0;
322 if (end_ns <= start_ns) return 0;
323 return end_ns - start_ns;
324 }
325 };
326
327 pub fn writeTextFromJsonlPath(
328 allocator: std.mem.Allocator,
329 path: []const u8,
330 writer: *std.Io.Writer,
331 options: Options,
332 ) !void {
333 var analyzer = Analyzer.init(allocator);
334 defer analyzer.deinit();
335 try ingestPath(&analyzer, path, options);
336 try writeText(allocator, &analyzer, writer, options);
337 }
338
339 pub fn writeJsonlFromJsonlPath(
340 allocator: std.mem.Allocator,
341 path: []const u8,
342 writer: *std.Io.Writer,
343 options: Options,
344 ) !void {
345 var analyzer = Analyzer.init(allocator);
346 defer analyzer.deinit();
347 try ingestPath(&analyzer, path, options);
348 try writeJsonl(allocator, &analyzer, writer, options);
349 }
350
351 fn ingestPath(analyzer: *Analyzer, path: []const u8, options: Options) !void {
352 var file = try sys.fs.cwd().openFile(sys.fs.debugIo(), path, .{});
353 defer file.close(sys.fs.debugIo());
354 var buffer: [64 * 1024]u8 = undefined;
355 var reader = file.reader(sys.fs.debugIo(), &buffer);
356 while (true) {
357 const line = reader.interface.takeDelimiter('\n') catch |err| switch (err) {
358 error.ReadFailed => return reader.err.?,
359 else => return err,
360 };
361 const actual = line orelse break;
362 try analyzer.ingestJsonLine(actual, options);
363 }
364 analyzer.finish(options);
365 }
366
367 fn writeText(allocator: std.mem.Allocator, analyzer: *Analyzer, writer: *std.Io.Writer, options: Options) !void {
368 var groups = try analyzer.collectGroups(allocator, options);
369 defer groups.deinit(allocator);
370 try writer.print(
371 "tracy messages groups={d} entries={d} events={d} messages={d} app_infos={d} thread_names={d} filtered={d} duration_ns={d} kind={s} group={s} sort={s}",
372 .{
373 groups.items.len,
374 analyzer.entries.items.len,
375 analyzer.counters.events,
376 analyzer.counters.messages,
377 analyzer.counters.app_infos,
378 analyzer.counters.thread_names,
379 analyzer.counters.filtered,
380 analyzer.counters.duration_ns,
381 options.kind.tag(),
382 options.group.tag(),
383 options.sort.tag(),
384 },
385 );
386 if (options.thread) |thread| try writer.print(" thread={d}", .{thread});
387 if (options.since_ns) |since_ns| try writer.print(" since_ns={d}", .{since_ns});
388 if (options.until_ns) |until_ns| try writer.print(" until_ns={d}", .{until_ns});
389 if (options.match) |pattern| {
390 try writer.writeAll(" match=");
391 try pretty_json.writeString(writer, pattern);
392 }
393 try writer.writeByte('\n');
394 const group_limit = @min(options.top, groups.items.len);
395 for (groups.items[0..group_limit]) |group| {
396 try writer.print("message-group group={s} label=", .{group.group.tag()});
397 try pretty_json.writeString(writer, group.label);
398 try writer.print(" count={d} first_ns={d} last_ns={d}", .{ group.count, group.first_ns, group.last_ns });
399 if (group.kind) |kind| try writer.print(" kind={s}", .{kind.tag()});
400 if (group.thread) |thread| try writer.print(" thread={d}", .{thread});
401 if (group.thread_name) |thread_name| {
402 try writer.writeAll(" thread_name=");
403 try pretty_json.writeString(writer, thread_name);
404 }
405 if (group.text) |text| {
406 try writer.writeAll(" text=");
407 try pretty_json.writeString(writer, text);
408 }
409 try writer.writeByte('\n');
410 }
411 const occurrence_limit = @min(options.occurrences, analyzer.entries.items.len);
412 for (analyzer.entries.items[0..occurrence_limit]) |entry| {
413 try writer.print("message kind={s} seq={d} time_ns={d} thread={d}", .{ entry.kind.tag(), entry.seq, entry.time_ns, entry.thread });
414 if (entry.thread_name) |thread_name| {
415 try writer.writeAll(" thread_name=");
416 try pretty_json.writeString(writer, thread_name);
417 }
418 try writer.writeAll(" text=");
419 try pretty_json.writeString(writer, entry.text);
420 if (entry.color) |color| try writer.print(" color={d}", .{color});
421 try writer.writeByte('\n');
422 }
423 }
424
425 fn writeJsonl(allocator: std.mem.Allocator, analyzer: *Analyzer, writer: *std.Io.Writer, options: Options) !void {
426 var groups = try analyzer.collectGroups(allocator, options);
427 defer groups.deinit(allocator);
428 var summary_stream = pretty_json.Writer.init(writer, .minified);
429 const summary = try summary_stream.object();
430 try summary.field("schema", schema);
431 try summary.field("kind", "summary");
432 try summary.field("groups", groups.items.len);
433 try summary.field("entries", analyzer.entries.items.len);
434 try summary.field("events", analyzer.counters.events);
435 try summary.field("messages", analyzer.counters.messages);
436 try summary.field("app_infos", analyzer.counters.app_infos);
437 try summary.field("thread_names", analyzer.counters.thread_names);
438 try summary.field("filtered", analyzer.counters.filtered);
439 try summary.field("duration_ns", analyzer.counters.duration_ns);
440 try summary.field("filter_kind", options.kind.tag());
441 try summary.field("group", options.group.tag());
442 try summary.field("sort", options.sort.tag());
443 if (options.thread) |thread| try summary.field("thread", thread);
444 if (options.since_ns) |since_ns| try summary.field("since_ns", since_ns);
445 if (options.until_ns) |until_ns| try summary.field("until_ns", until_ns);
446 if (options.match) |pattern| try summary.field("match", pattern);
447 try summary.endLine();
448 const group_limit = @min(options.top, groups.items.len);
449 for (groups.items[0..group_limit]) |group| {
450 var stream = pretty_json.Writer.init(writer, .minified);
451 const object = try stream.object();
452 try object.field("schema", schema);
453 try object.field("kind", "group");
454 try object.field("group", group.group.tag());
455 try object.field("label", group.label);
456 try object.field("count", group.count);
457 try object.field("first_ns", group.first_ns);
458 try object.field("last_ns", group.last_ns);
459 if (group.kind) |kind| try object.field("message_kind", kind.tag());
460 if (group.thread) |thread| try object.field("thread", thread);
461 if (group.thread_name) |name| try object.field("thread_name", name);
462 if (group.text) |text| try object.field("text", text);
463 try object.endLine();
464 }
465 const occurrence_limit = @min(options.occurrences, analyzer.entries.items.len);
466 for (analyzer.entries.items[0..occurrence_limit]) |entry| {
467 var stream = pretty_json.Writer.init(writer, .minified);
468 const object = try stream.object();
469 try object.field("schema", schema);
470 try object.field("kind", "message");
471 try object.field("message_kind", entry.kind.tag());
472 try object.field("seq", entry.seq);
473 try object.field("time_ns", entry.time_ns);
474 try object.field("thread", entry.thread);
475 if (entry.thread_name) |name| try object.field("thread_name", name);
476 try object.field("text", entry.text);
477 if (entry.color) |color| try object.field("color", color);
478 try object.endLine();
479 }
480 }
481
482 fn entryMatches(kind: Kind, parsed: event.Parsed, text: []const u8, options: Options) bool {
483 switch (options.kind) {
484 .all => {},
485 .message => if (kind != .message) return false,
486 .app_info => if (kind != .app_info) return false,
487 }
488 if (options.thread) |thread| {
489 if (parsed.thread != thread) return false;
490 }
491 if (options.since_ns) |since_ns| {
492 if (parsed.time_ns < since_ns) return false;
493 }
494 if (options.until_ns) |until_ns| {
495 if (parsed.time_ns > until_ns) return false;
496 }
497 if (options.match) |pattern| {
498 if (!contains(text, pattern, options.ignore_case)) return false;
499 }
500 return true;
501 }
502
503 fn writeGroupKey(writer: *std.Io.Writer, entry: Entry, group: Group) !void {
504 switch (group) {
505 .thread => try writer.print("thread:{d}", .{entry.thread}),
506 .text => try writer.writeAll(entry.text),
507 .kind => try writer.writeAll(entry.kind.tag()),
508 .none => try writer.print("message:{d}:{d}:{d}", .{ entry.time_ns, entry.seq, entry.thread }),
509 }
510 }
511
512 fn groupLabel(allocator: std.mem.Allocator, entry: Entry, group: Group) ![]u8 {
513 var writer = std.Io.Writer.Allocating.init(allocator);
514 defer writer.deinit();
515 switch (group) {
516 .thread => {
517 if (entry.thread_name) |thread_name| {
518 try writer.writer.print("{s} ({d})", .{ thread_name, entry.thread });
519 } else {
520 try writer.writer.print("thread {d}", .{entry.thread});
521 }
522 },
523 .text => try writer.writer.writeAll(entry.text),
524 .kind => try writer.writer.writeAll(entry.kind.tag()),
525 .none => try writer.writer.print("{s}#{d}", .{ entry.kind.tag(), entry.seq }),
526 }
527 return try allocator.dupe(u8, writer.written());
528 }
529
530 fn contains(haystack: []const u8, needle: []const u8, ignore_case: bool) bool {
531 if (!ignore_case) return std.mem.indexOf(u8, haystack, needle) != null;
532 if (needle.len == 0) return true;
533 if (needle.len > haystack.len) return false;
534 var index: usize = 0;
535 while (index + needle.len <= haystack.len) : (index += 1) {
536 if (asciiEqlIgnoreCase(haystack[index .. index + needle.len], needle)) return true;
537 }
538 return false;
539 }
540
541 fn asciiEqlIgnoreCase(left: []const u8, right: []const u8) bool {
542 if (left.len != right.len) return false;
543 for (left, right) |a, b| {
544 if (std.ascii.toLower(a) != std.ascii.toLower(b)) return false;
545 }
546 return true;
547 }
548
549 fn dupeOptional(allocator: std.mem.Allocator, text: ?[]const u8) !?[]u8 {
550 const actual = text orelse return null;
551 return try allocator.dupe(u8, actual);
552 }
553
554 fn sortGroups(items: []GroupView, sort: Sort) void {
555 switch (sort) {
556 .time => std.mem.sort(GroupView, items, {}, groupTimeLessThan),
557 .count => std.mem.sort(GroupView, items, {}, groupCountGreaterThan),
558 .last => std.mem.sort(GroupView, items, {}, groupLastGreaterThan),
559 .thread => std.mem.sort(GroupView, items, {}, groupThreadLessThan),
560 .text => std.mem.sort(GroupView, items, {}, groupTextLessThan),
561 .kind => std.mem.sort(GroupView, items, {}, groupKindLessThan),
562 }
563 }
564
565 fn sortEntries(items: []Entry, sort: Sort) void {
566 switch (sort) {
567 .time, .count, .last => std.mem.sort(Entry, items, {}, entryTimeLessThan),
568 .thread => std.mem.sort(Entry, items, {}, entryThreadLessThan),
569 .text => std.mem.sort(Entry, items, {}, entryTextLessThan),
570 .kind => std.mem.sort(Entry, items, {}, entryKindLessThan),
571 }
572 }
573
574 fn groupTimeLessThan(_: void, left: GroupView, right: GroupView) bool {
575 if (left.first_ns != right.first_ns) return left.first_ns < right.first_ns;
576 return groupTextLessThan({}, left, right);
577 }
578
579 fn groupCountGreaterThan(_: void, left: GroupView, right: GroupView) bool {
580 if (left.count != right.count) return left.count > right.count;
581 return groupTimeLessThan({}, left, right);
582 }
583
584 fn groupLastGreaterThan(_: void, left: GroupView, right: GroupView) bool {
585 if (left.last_ns != right.last_ns) return left.last_ns > right.last_ns;
586 return groupTimeLessThan({}, left, right);
587 }
588
589 fn groupThreadLessThan(_: void, left: GroupView, right: GroupView) bool {
590 const left_thread = left.thread orelse 0;
591 const right_thread = right.thread orelse 0;
592 if (left_thread != right_thread) return left_thread < right_thread;
593 return groupTextLessThan({}, left, right);
594 }
595
596 fn groupTextLessThan(_: void, left: GroupView, right: GroupView) bool {
597 const cmp = std.mem.order(u8, left.label, right.label);
598 if (cmp != .eq) return cmp == .lt;
599 return left.first_ns < right.first_ns;
600 }
601
602 fn groupKindLessThan(_: void, left: GroupView, right: GroupView) bool {
603 const left_kind = if (left.kind) |kind| kind.tag() else "";
604 const right_kind = if (right.kind) |kind| kind.tag() else "";
605 const cmp = std.mem.order(u8, left_kind, right_kind);
606 if (cmp != .eq) return cmp == .lt;
607 return groupTextLessThan({}, left, right);
608 }
609
610 fn entryTimeLessThan(_: void, left: Entry, right: Entry) bool {
611 if (left.time_ns != right.time_ns) return left.time_ns < right.time_ns;
612 return left.seq < right.seq;
613 }
614
615 fn entryThreadLessThan(_: void, left: Entry, right: Entry) bool {
616 if (left.thread != right.thread) return left.thread < right.thread;
617 return entryTimeLessThan({}, left, right);
618 }
619
620 fn entryTextLessThan(_: void, left: Entry, right: Entry) bool {
621 const cmp = std.mem.order(u8, left.text, right.text);
622 if (cmp != .eq) return cmp == .lt;
623 return entryTimeLessThan({}, left, right);
624 }
625
626 fn entryKindLessThan(_: void, left: Entry, right: Entry) bool {
627 const cmp = std.mem.order(u8, left.kind.tag(), right.kind.tag());
628 if (cmp != .eq) return cmp == .lt;
629 return entryTimeLessThan({}, left, right);
630 }
631
632 test "messages groups text output by thread" {
633 var trace = std.Io.Writer.Allocating.init(std.testing.allocator);
634 defer trace.deinit();
635 try (event.TraceEvent{ .seq = 1, .kind = .start, .time_ns = 90, .thread = 1, .name = "test" }).writeJsonLine(&trace.writer);
636 try (event.TraceEvent{ .seq = 2, .kind = .thread_name, .time_ns = 95, .thread = 1, .name = "main" }).writeJsonLine(&trace.writer);
637 try (event.TraceEvent{ .seq = 3, .kind = .message, .time_ns = 100, .thread = 1, .text = "queue ready" }).writeJsonLine(&trace.writer);
638 try (event.TraceEvent{ .seq = 4, .kind = .message, .time_ns = 130, .thread = 1, .text = "queue drained" }).writeJsonLine(&trace.writer);
639 try (event.TraceEvent{ .seq = 5, .kind = .message, .time_ns = 140, .thread = 2, .text = "other" }).writeJsonLine(&trace.writer);
640 try (event.TraceEvent{ .seq = 6, .kind = .stop, .time_ns = 180, .thread = 1 }).writeJsonLine(&trace.writer);
641
642 var analyzer = Analyzer.init(std.testing.allocator);
643 defer analyzer.deinit();
644 const options = Options{ .match = "queue", .group = .thread, .sort = .count, .ignore_case = true };
645 try analyzer.ingestJsonlBytes(trace.written(), options);
646
647 var out = std.Io.Writer.Allocating.init(std.testing.allocator);
648 defer out.deinit();
649 try writeText(std.testing.allocator, &analyzer, &out.writer, options);
650 const text = out.written();
651 try std.testing.expect(std.mem.indexOf(u8, text, "tracy messages groups=1 entries=2 events=6 messages=3 app_infos=0 thread_names=1 filtered=1") != null);
652 try std.testing.expect(std.mem.indexOf(u8, text, "message-group group=thread label=\"main (1)\" count=2 first_ns=100 last_ns=130 thread=1 thread_name=\"main\"") != null);
653 try std.testing.expect(std.mem.indexOf(u8, text, "message kind=message seq=3 time_ns=100 thread=1 thread_name=\"main\" text=\"queue ready\"") != null);
654 }
655
656 test "messages jsonl groups app info by text in time window" {
657 var trace = std.Io.Writer.Allocating.init(std.testing.allocator);
658 defer trace.deinit();
659 try (event.TraceEvent{ .seq = 1, .kind = .app_info, .time_ns = 100, .thread = 1, .text = "build debug" }).writeJsonLine(&trace.writer);
660 try (event.TraceEvent{ .seq = 2, .kind = .app_info, .time_ns = 120, .thread = 1, .text = "build debug" }).writeJsonLine(&trace.writer);
661 try (event.TraceEvent{ .seq = 3, .kind = .message, .time_ns = 130, .thread = 1, .text = "skip" }).writeJsonLine(&trace.writer);
662 try (event.TraceEvent{ .seq = 4, .kind = .app_info, .time_ns = 200, .thread = 1, .text = "outside" }).writeJsonLine(&trace.writer);
663
664 var analyzer = Analyzer.init(std.testing.allocator);
665 defer analyzer.deinit();
666 const options = Options{ .kind = .app_info, .group = .text, .sort = .count, .since_ns = 90, .until_ns = 150, .occurrences = 4 };
667 try analyzer.ingestJsonlBytes(trace.written(), options);
668
669 var out = std.Io.Writer.Allocating.init(std.testing.allocator);
670 defer out.deinit();
671 try writeJsonl(std.testing.allocator, &analyzer, &out.writer, options);
672 const text = out.written();
673 try std.testing.expect(std.mem.indexOf(u8, text, "\"schema\":\"tracy.messages/v0\"") != null);
674 try std.testing.expect(std.mem.indexOf(u8, text, "\"kind\":\"summary\",\"groups\":1,\"entries\":2,\"events\":4,\"messages\":1,\"app_infos\":3") != null);
675 try std.testing.expect(std.mem.indexOf(u8, text, "\"kind\":\"group\",\"group\":\"text\",\"label\":\"build debug\",\"count\":2") != null);
676 try std.testing.expect(std.mem.indexOf(u8, text, "\"kind\":\"message\",\"message_kind\":\"app-info\",\"seq\":2") != null);
677 }