lib/tracy/src/flame.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.flame/v0";
7
8 pub const Sort = enum {
9 time,
10 name,
11
12 pub fn fromName(text: []const u8) ?Sort {
13 if (std.mem.eql(u8, text, "time")) return .time;
14 if (std.mem.eql(u8, text, "name")) return .name;
15 return null;
16 }
17
18 fn tag(self: Sort) []const u8 {
19 return switch (self) {
20 .time => "time",
21 .name => "name",
22 };
23 }
24 };
25
26 pub const Options = struct {
27 top: usize = 200,
28 sort: Sort = .time,
29 max_depth: usize = 64,
30 min_ns: u64 = 0,
31 thread: ?u64 = null,
32 since_ns: ?u64 = null,
33 until_ns: ?u64 = null,
34 match: ?[]const u8 = null,
35 };
36
37 pub const Counters = struct {
38 spans: u64 = 0,
39 matched_spans: u64 = 0,
40 open_spans: u64 = 0,
41 superseded_spans: u64 = 0,
42 invalid_duration_spans: u64 = 0,
43 zero_duration_spans: u64 = 0,
44 clipped_spans: u64 = 0,
45 nodes: u64 = 0,
46 max_depth: usize = 0,
47 total_ns: u64 = 0,
48 duration_ns: u64 = 0,
49 };
50
51 const Node = struct {
52 name: []u8,
53 file: ?[]u8 = null,
54 function: ?[]u8 = null,
55 line: u32 = 0,
56 column: u32 = 0,
57 total_ns: u64 = 0,
58 spans: u64 = 0,
59 threads: std.ArrayListUnmanaged(u64) = .empty,
60 children: std.ArrayListUnmanaged(Node) = .empty,
61
62 fn deinit(self: *Node, allocator: std.mem.Allocator) void {
63 allocator.free(self.name);
64 if (self.file) |file| allocator.free(file);
65 if (self.function) |function| allocator.free(function);
66 self.threads.deinit(allocator);
67 for (self.children.items) |*child| child.deinit(allocator);
68 self.children.deinit(allocator);
69 self.* = undefined;
70 }
71
72 fn childNs(self: Node) u64 {
73 var total: u64 = 0;
74 for (self.children.items) |child| total +|= child.total_ns;
75 return total;
76 }
77
78 fn selfNs(self: Node) u64 {
79 const child_ns = self.childNs();
80 if (self.total_ns <= child_ns) return 0;
81 return self.total_ns - child_ns;
82 }
83
84 fn threadCount(self: Node) u64 {
85 return @intCast(self.threads.items.len);
86 }
87 };
88
89 const SpanRecord = struct {
90 span: tree.Span,
91 children: std.ArrayListUnmanaged(usize) = .empty,
92
93 fn deinit(self: *SpanRecord, allocator: std.mem.Allocator) void {
94 self.children.deinit(allocator);
95 self.* = undefined;
96 }
97 };
98
99 pub const Analyzer = struct {
100 allocator: std.mem.Allocator,
101 roots: std.ArrayListUnmanaged(Node) = .empty,
102 counters: Counters = .{},
103 evidence: ?tree.Evidence = null,
104
105 pub fn init(allocator: std.mem.Allocator) Analyzer {
106 return .{ .allocator = allocator };
107 }
108
109 pub fn deinit(self: *Analyzer) void {
110 for (self.roots.items) |*root| root.deinit(self.allocator);
111 self.roots.deinit(self.allocator);
112 self.* = undefined;
113 }
114
115 pub fn ingestTree(self: *Analyzer, trace: *tree.Analyzer, options: Options) !void {
116 self.counters.duration_ns = trace.durationNs();
117 self.evidence = trace.evidence();
118 var spans = try trace.collectSpans(self.allocator);
119 defer spans.deinit(self.allocator);
120 self.counters.spans = @intCast(spans.items.len);
121
122 var records: std.ArrayListUnmanaged(SpanRecord) = .empty;
123 defer {
124 for (records.items) |*record| record.deinit(self.allocator);
125 records.deinit(self.allocator);
126 }
127 try records.ensureTotalCapacity(self.allocator, spans.items.len);
128
129 var ids: std.AutoHashMapUnmanaged(u64, usize) = .{};
130 defer ids.deinit(self.allocator);
131 try ids.ensureTotalCapacity(self.allocator, @intCast(spans.items.len));
132
133 for (spans.items, 0..) |span, index| {
134 records.appendAssumeCapacity(.{ .span = span });
135 ids.putAssumeCapacity(span.id, index);
136 }
137
138 var root_indices: std.ArrayListUnmanaged(usize) = .empty;
139 defer root_indices.deinit(self.allocator);
140 for (records.items, 0..) |record, index| {
141 if (record.span.parent_id) |parent_id| {
142 if (ids.get(parent_id)) |parent_index| {
143 try records.items[parent_index].children.append(self.allocator, index);
144 continue;
145 }
146 }
147 try root_indices.append(self.allocator, index);
148 }
149
150 for (root_indices.items) |root_index| try self.recordSpan(records.items, &self.roots, root_index, options);
151 sortNodes(self.roots.items, options.sort);
152 self.counters.nodes = countNodes(self.roots.items);
153 self.counters.max_depth = maxDepth(self.roots.items, 0);
154 self.counters.total_ns = totalNs(self.roots.items);
155 }
156
157 fn recordSpan(
158 self: *Analyzer,
159 records: []SpanRecord,
160 siblings: *std.ArrayListUnmanaged(Node),
161 index: usize,
162 options: Options,
163 ) !void {
164 const span = records[index].span;
165 var destination = siblings;
166 switch (span.state) {
167 .open => self.counters.open_spans += 1,
168 .superseded => self.counters.superseded_spans += 1,
169 .complete => if (!span.duration_valid) {
170 self.counters.invalid_duration_spans += 1;
171 } else if (spanMatches(span, options)) {
172 if (span.total_ns == 0) self.counters.zero_duration_spans += 1;
173 destination = try self.recordMatchedSpan(siblings, span, options);
174 },
175 }
176 for (records[index].children.items) |child_index| {
177 try self.recordSpan(records, destination, child_index, options);
178 }
179 }
180
181 fn recordMatchedSpan(
182 self: *Analyzer,
183 siblings: *std.ArrayListUnmanaged(Node),
184 span: tree.Span,
185 options: Options,
186 ) !*std.ArrayListUnmanaged(Node) {
187 var destination = siblings;
188 if (span.total_ns != 0) {
189 if (clippedDuration(span, options)) |duration| {
190 const node = try self.nodeFor(siblings, span);
191 node.total_ns +|= duration;
192 node.spans += 1;
193 try appendThread(self.allocator, &node.threads, span.thread);
194 destination = &node.children;
195 self.counters.matched_spans += 1;
196 if (duration != span.total_ns) self.counters.clipped_spans += 1;
197 }
198 }
199 return destination;
200 }
201
202 fn nodeFor(self: *Analyzer, siblings: *std.ArrayListUnmanaged(Node), span: tree.Span) !*Node {
203 for (siblings.items) |*node| {
204 if (sameSource(node.*, span)) return node;
205 }
206 try siblings.append(self.allocator, .{
207 .name = try self.allocator.dupe(u8, span.name),
208 .file = try dupeOptional(self.allocator, span.file),
209 .function = try dupeOptional(self.allocator, span.function),
210 .line = span.line,
211 .column = span.column,
212 });
213 return &siblings.items[siblings.items.len - 1];
214 }
215 };
216
217 pub fn writeTextFromJsonlPath(
218 allocator: std.mem.Allocator,
219 path: []const u8,
220 writer: *std.Io.Writer,
221 options: Options,
222 ) !void {
223 var trace = tree.Analyzer.init(allocator);
224 defer trace.deinit();
225 try tree.ingestPath(&trace, path);
226 var analyzer = Analyzer.init(allocator);
227 defer analyzer.deinit();
228 try analyzer.ingestTree(&trace, options);
229 try writeText(&analyzer, writer, options);
230 }
231
232 pub fn writeJsonlFromJsonlPath(
233 allocator: std.mem.Allocator,
234 path: []const u8,
235 writer: *std.Io.Writer,
236 options: Options,
237 ) !void {
238 var trace = tree.Analyzer.init(allocator);
239 defer trace.deinit();
240 try tree.ingestPath(&trace, path);
241 var analyzer = Analyzer.init(allocator);
242 defer analyzer.deinit();
243 try analyzer.ingestTree(&trace, options);
244 try writeJsonl(&analyzer, writer, options);
245 }
246
247 pub fn writeFoldedFromJsonlPath(
248 allocator: std.mem.Allocator,
249 path: []const u8,
250 writer: *std.Io.Writer,
251 options: Options,
252 ) !void {
253 var trace = tree.Analyzer.init(allocator);
254 defer trace.deinit();
255 try tree.ingestPath(&trace, path);
256 var analyzer = Analyzer.init(allocator);
257 defer analyzer.deinit();
258 try analyzer.ingestTree(&trace, options);
259 try writeFolded(allocator, &analyzer, writer, options);
260 }
261
262 fn writeText(analyzer: *Analyzer, writer: *std.Io.Writer, options: Options) !void {
263 try writer.print(
264 "tracy flame nodes={d} spans={d} matched_spans={d} open_spans={d} " ++
265 "superseded_spans={d} invalid_duration_spans={d} " ++
266 "zero_duration_spans={d} clipped_spans={d} total_ns={d} " ++
267 "duration_ns={d} max_depth={d} sort={s}\n",
268 .{
269 analyzer.counters.nodes,
270 analyzer.counters.spans,
271 analyzer.counters.matched_spans,
272 analyzer.counters.open_spans,
273 analyzer.counters.superseded_spans,
274 analyzer.counters.invalid_duration_spans,
275 analyzer.counters.zero_duration_spans,
276 analyzer.counters.clipped_spans,
277 analyzer.counters.total_ns,
278 analyzer.counters.duration_ns,
279 analyzer.counters.max_depth,
280 options.sort.tag(),
281 },
282 );
283 try analyzer.evidence.?.writeText(writer);
284 var shown: usize = 0;
285 try writeTextNodes(writer, analyzer.roots.items, 0, 0, options, &shown);
286 }
287
288 fn writeJsonl(analyzer: *Analyzer, writer: *std.Io.Writer, options: Options) !void {
289 var stream = pretty_json.Writer.init(writer, .minified);
290 const object = try stream.object();
291 try object.field("schema", schema);
292 try object.field("kind", "summary");
293 try object.field("nodes", analyzer.counters.nodes);
294 try object.field("spans", analyzer.counters.spans);
295 try object.field("matched_spans", analyzer.counters.matched_spans);
296 try object.field("open_spans", analyzer.counters.open_spans);
297 try object.field("superseded_spans", analyzer.counters.superseded_spans);
298 try object.field("invalid_duration_spans", analyzer.counters.invalid_duration_spans);
299 try object.field("zero_duration_spans", analyzer.counters.zero_duration_spans);
300 try object.field("clipped_spans", analyzer.counters.clipped_spans);
301 try object.field("total_ns", analyzer.counters.total_ns);
302 try object.field("duration_ns", analyzer.counters.duration_ns);
303 try object.field("max_depth", analyzer.counters.max_depth);
304 try object.field("sort", options.sort.tag());
305 try analyzer.evidence.?.writeFields(object);
306 try object.endLine();
307 var shown: usize = 0;
308 var next_id: u64 = 1;
309 try writeJsonlNodes(writer, analyzer.roots.items, 0, 0, null, options, &shown, &next_id);
310 }
311
312 fn writeFolded(allocator: std.mem.Allocator, analyzer: *Analyzer, writer: *std.Io.Writer, options: Options) !void {
313 var path: std.ArrayListUnmanaged(*const Node) = .empty;
314 defer path.deinit(allocator);
315 var shown: usize = 0;
316 for (analyzer.roots.items) |*root| {
317 try writeFoldedNode(allocator, writer, root, &path, 0, options, &shown);
318 if (shown >= options.top) return;
319 }
320 }
321
322 fn writeTextNodes(
323 writer: *std.Io.Writer,
324 nodes: []const Node,
325 depth: usize,
326 base_offset_ns: u64,
327 options: Options,
328 shown: *usize,
329 ) !void {
330 var offset_ns = base_offset_ns;
331 for (nodes) |node| {
332 const current_offset_ns = offset_ns;
333 offset_ns +|= node.total_ns;
334 const visible = outputMatches(node, depth, options);
335 if (visible and shown.* < options.top) {
336 for (0..depth) |_| try writer.writeAll(" ");
337 try writer.print("flame depth={d} offset_ns={d} total_ns={d} self_ns={d} child_ns={d} spans={d} threads={d} children={d} name=", .{
338 depth,
339 current_offset_ns,
340 node.total_ns,
341 node.selfNs(),
342 node.childNs(),
343 node.spans,
344 node.threadCount(),
345 node.children.items.len,
346 });
347 try pretty_json.writeString(writer, node.name);
348 if (node.file) |file| try writer.print(" file={s}:{d}", .{ file, node.line });
349 if (node.function) |function| {
350 try writer.writeAll(" function=");
351 try pretty_json.writeString(writer, function);
352 }
353 try writer.writeByte('\n');
354 shown.* += 1;
355 }
356 if (shown.* >= options.top) return;
357 if (depth < options.max_depth) try writeTextNodes(writer, node.children.items, depth + 1, current_offset_ns, options, shown);
358 if (shown.* >= options.top) return;
359 }
360 }
361
362 fn writeJsonlNodes(
363 writer: *std.Io.Writer,
364 nodes: []const Node,
365 depth: usize,
366 base_offset_ns: u64,
367 parent_id: ?u64,
368 options: Options,
369 shown: *usize,
370 next_id: *u64,
371 ) !void {
372 var offset_ns = base_offset_ns;
373 for (nodes) |node| {
374 const current_offset_ns = offset_ns;
375 offset_ns +|= node.total_ns;
376 var row_id: ?u64 = null;
377 const visible = outputMatches(node, depth, options);
378 if (visible and shown.* < options.top) {
379 const id = next_id.*;
380 next_id.* += 1;
381 row_id = id;
382 var stream = pretty_json.Writer.init(writer, .minified);
383 const object = try stream.object();
384 try object.field("schema", schema);
385 try object.field("kind", "node");
386 try object.field("id", id);
387 if (parent_id) |parent| try object.field("parent_id", parent);
388 try object.field("depth", depth);
389 try object.field("offset_ns", current_offset_ns);
390 try object.field("total_ns", node.total_ns);
391 try object.field("self_ns", node.selfNs());
392 try object.field("child_ns", node.childNs());
393 try object.field("spans", node.spans);
394 try object.field("threads", node.threadCount());
395 try object.field("children", node.children.items.len);
396 try object.field("name", node.name);
397 if (node.file) |file| {
398 try object.field("file", file);
399 try object.field("line", node.line);
400 }
401 if (node.function) |function| try object.field("function", function);
402 try object.endLine();
403 shown.* += 1;
404 }
405 if (shown.* >= options.top) return;
406 if (depth < options.max_depth) try writeJsonlNodes(writer, node.children.items, depth + 1, current_offset_ns, row_id orelse parent_id, options, shown, next_id);
407 if (shown.* >= options.top) return;
408 }
409 }
410
411 fn writeFoldedNode(
412 allocator: std.mem.Allocator,
413 writer: *std.Io.Writer,
414 node: *const Node,
415 path: *std.ArrayListUnmanaged(*const Node),
416 depth: usize,
417 options: Options,
418 shown: *usize,
419 ) !void {
420 try path.append(allocator, node);
421 defer _ = path.pop();
422
423 if (depth >= options.max_depth) {
424 try writeFoldedPath(writer, path.items, node.total_ns, options, shown);
425 return;
426 }
427
428 const self_ns = node.selfNs();
429 if (self_ns != 0) try writeFoldedPath(writer, path.items, self_ns, options, shown);
430 if (shown.* >= options.top) return;
431 for (node.children.items) |*child| {
432 try writeFoldedNode(allocator, writer, child, path, depth + 1, options, shown);
433 if (shown.* >= options.top) return;
434 }
435 }
436
437 fn writeFoldedPath(
438 writer: *std.Io.Writer,
439 path: []const *const Node,
440 value: u64,
441 options: Options,
442 shown: *usize,
443 ) !void {
444 if (value < options.min_ns) return;
445 if (!pathMatches(path, options)) return;
446 if (shown.* >= options.top) return;
447 for (path, 0..) |node, index| {
448 if (index != 0) try writer.writeByte(';');
449 try writeFoldedFrame(writer, node.name);
450 }
451 try writer.print(" {d}\n", .{value});
452 shown.* += 1;
453 }
454
455 fn clippedDuration(span: tree.Span, options: Options) ?u64 {
456 var start_ns = span.start_ns;
457 var end_ns = span.end_ns;
458 if (options.since_ns) |since_ns| start_ns = @max(start_ns, since_ns);
459 if (options.until_ns) |until_ns| end_ns = @min(end_ns, until_ns);
460 if (end_ns <= start_ns) return null;
461 return end_ns - start_ns;
462 }
463
464 fn spanMatches(span: tree.Span, options: Options) bool {
465 if (options.thread) |thread| {
466 if (span.thread != thread) return false;
467 }
468 return true;
469 }
470
471 fn outputMatches(node: Node, depth: usize, options: Options) bool {
472 if (depth > options.max_depth) return false;
473 if (node.total_ns < options.min_ns) return false;
474 if (options.match) |needle| {
475 if (!nodeContains(node, needle)) return false;
476 }
477 return true;
478 }
479
480 fn pathMatches(path: []const *const Node, options: Options) bool {
481 if (options.match) |needle| {
482 for (path) |node| {
483 if (nodeContains(node.*, needle)) return true;
484 }
485 return false;
486 }
487 return true;
488 }
489
490 fn nodeContains(node: Node, needle: []const u8) bool {
491 if (contains(node.name, needle)) return true;
492 if (node.file) |file| if (contains(file, needle)) return true;
493 if (node.function) |function| if (contains(function, needle)) return true;
494 return false;
495 }
496
497 fn contains(haystack: []const u8, needle: []const u8) bool {
498 return std.mem.indexOf(u8, haystack, needle) != null;
499 }
500
501 fn sameSource(node: Node, span: tree.Span) bool {
502 if (!std.mem.eql(u8, node.name, span.name)) return false;
503 if (!optionalEql(node.file, span.file)) return false;
504 if (!optionalEql(node.function, span.function)) return false;
505 return node.line == span.line and node.column == span.column;
506 }
507
508 fn optionalEql(left: ?[]const u8, right: ?[]const u8) bool {
509 if (left == null and right == null) return true;
510 if (left == null or right == null) return false;
511 return std.mem.eql(u8, left.?, right.?);
512 }
513
514 fn appendThread(allocator: std.mem.Allocator, threads: *std.ArrayListUnmanaged(u64), thread: u64) !void {
515 for (threads.items) |existing| {
516 if (existing == thread) return;
517 }
518 try threads.append(allocator, thread);
519 }
520
521 fn dupeOptional(allocator: std.mem.Allocator, text: ?[]const u8) !?[]u8 {
522 const actual = text orelse return null;
523 return try allocator.dupe(u8, actual);
524 }
525
526 fn sortNodes(nodes: []Node, sort: Sort) void {
527 switch (sort) {
528 .time => std.mem.sort(Node, nodes, {}, nodeTimeGreaterThan),
529 .name => std.mem.sort(Node, nodes, {}, nodeNameLessThan),
530 }
531 for (nodes) |node| sortNodes(node.children.items, sort);
532 }
533
534 fn nodeTimeGreaterThan(_: void, left: Node, right: Node) bool {
535 if (left.total_ns != right.total_ns) return left.total_ns > right.total_ns;
536 return nodeNameLessThan({}, left, right);
537 }
538
539 fn nodeNameLessThan(_: void, left: Node, right: Node) bool {
540 const name_cmp = std.mem.order(u8, left.name, right.name);
541 if (name_cmp != .eq) return name_cmp == .lt;
542 const left_file = left.file orelse "";
543 const right_file = right.file orelse "";
544 const file_cmp = std.mem.order(u8, left_file, right_file);
545 if (file_cmp != .eq) return file_cmp == .lt;
546 return left.line < right.line;
547 }
548
549 fn countNodes(nodes: []const Node) u64 {
550 var count: u64 = 0;
551 for (nodes) |node| count += 1 + countNodes(node.children.items);
552 return count;
553 }
554
555 fn maxDepth(nodes: []const Node, depth: usize) usize {
556 var result: usize = 0;
557 for (nodes) |node| {
558 result = @max(result, depth);
559 result = @max(result, maxDepth(node.children.items, depth + 1));
560 }
561 return result;
562 }
563
564 fn totalNs(nodes: []const Node) u64 {
565 var total: u64 = 0;
566 for (nodes) |node| total +|= node.total_ns;
567 return total;
568 }
569
570 fn writeFoldedFrame(writer: *std.Io.Writer, text: []const u8) !void {
571 for (text) |byte| {
572 switch (byte) {
573 ';' => try writer.writeAll("\\;"),
574 '\\' => try writer.writeAll("\\\\"),
575 '\n' => try writer.writeAll("\\n"),
576 '\r' => try writer.writeAll("\\r"),
577 '\t' => try writer.writeAll("\\t"),
578 else => try writer.writeByte(byte),
579 }
580 }
581 }
582
583 test "flame aggregates instrumentation zones into merged trees" {
584 var trace_bytes = std.Io.Writer.Allocating.init(std.testing.allocator);
585 defer trace_bytes.deinit();
586 try (event.TraceEvent{ .seq = 1, .kind = .start, .time_ns = 90, .thread = 1, .name = "test" }).writeJsonLine(&trace_bytes.writer);
587 try (event.TraceEvent{ .seq = 2, .kind = .zone_begin, .time_ns = 100, .thread = 1, .id = 1, .name = "root", .file = "root.zig", .line = 1 }).writeJsonLine(&trace_bytes.writer);
588 try (event.TraceEvent{ .seq = 3, .kind = .zone_begin, .time_ns = 120, .thread = 1, .id = 2, .name = "child", .file = "child.zig", .line = 2 }).writeJsonLine(&trace_bytes.writer);
589 try (event.TraceEvent{ .seq = 4, .kind = .zone_end, .time_ns = 150, .thread = 1, .id = 2 }).writeJsonLine(&trace_bytes.writer);
590 try (event.TraceEvent{ .seq = 5, .kind = .zone_end, .time_ns = 200, .thread = 1, .id = 1 }).writeJsonLine(&trace_bytes.writer);
591 try (event.TraceEvent{ .seq = 6, .kind = .zone_begin, .time_ns = 210, .thread = 2, .id = 3, .name = "root", .file = "root.zig", .line = 1 }).writeJsonLine(&trace_bytes.writer);
592 try (event.TraceEvent{ .seq = 7, .kind = .zone_begin, .time_ns = 220, .thread = 2, .id = 4, .name = "other", .file = "other.zig", .line = 3 }).writeJsonLine(&trace_bytes.writer);
593 try (event.TraceEvent{ .seq = 8, .kind = .zone_end, .time_ns = 260, .thread = 2, .id = 4 }).writeJsonLine(&trace_bytes.writer);
594 try (event.TraceEvent{ .seq = 9, .kind = .zone_end, .time_ns = 280, .thread = 2, .id = 3 }).writeJsonLine(&trace_bytes.writer);
595
596 var trace = tree.Analyzer.init(std.testing.allocator);
597 defer trace.deinit();
598 try trace.ingestJsonlBytes(trace_bytes.written());
599
600 var analyzer = Analyzer.init(std.testing.allocator);
601 defer analyzer.deinit();
602 try analyzer.ingestTree(&trace, .{});
603
604 var out = std.Io.Writer.Allocating.init(std.testing.allocator);
605 defer out.deinit();
606 try writeText(&analyzer, &out.writer, .{ .top = 8 });
607 const text = out.written();
608 try std.testing.expect(std.mem.indexOf(u8, text, "tracy flame nodes=3 spans=4 matched_spans=4") != null);
609 try std.testing.expect(std.mem.indexOf(u8, text, "flame depth=0 offset_ns=0 total_ns=170 self_ns=100 child_ns=70 spans=2 threads=2 children=2 name=\"root\"") != null);
610 try std.testing.expect(std.mem.indexOf(u8, text, "flame depth=1 offset_ns=0 total_ns=40 self_ns=40 child_ns=0 spans=1 threads=1 children=0 name=\"other\"") != null);
611 try std.testing.expect(std.mem.indexOf(u8, text, "flame depth=1 offset_ns=40 total_ns=30 self_ns=30 child_ns=0 spans=1 threads=1 children=0 name=\"child\"") != null);
612 }
613
614 test "flame jsonl and folded output preserve clipped paths" {
615 var trace_bytes = std.Io.Writer.Allocating.init(std.testing.allocator);
616 defer trace_bytes.deinit();
617 try (event.TraceEvent{ .seq = 1, .kind = .zone_begin, .time_ns = 100, .thread = 7, .id = 1, .name = "root" }).writeJsonLine(&trace_bytes.writer);
618 try (event.TraceEvent{ .seq = 2, .kind = .zone_begin, .time_ns = 125, .thread = 7, .id = 2, .name = "child;one" }).writeJsonLine(&trace_bytes.writer);
619 try (event.TraceEvent{ .seq = 3, .kind = .zone_end, .time_ns = 175, .thread = 7, .id = 2 }).writeJsonLine(&trace_bytes.writer);
620 try (event.TraceEvent{ .seq = 4, .kind = .zone_end, .time_ns = 220, .thread = 7, .id = 1 }).writeJsonLine(&trace_bytes.writer);
621
622 var trace = tree.Analyzer.init(std.testing.allocator);
623 defer trace.deinit();
624 try trace.ingestJsonlBytes(trace_bytes.written());
625
626 var analyzer = Analyzer.init(std.testing.allocator);
627 defer analyzer.deinit();
628 const options = Options{ .top = 8, .since_ns = 110, .until_ns = 180 };
629 try analyzer.ingestTree(&trace, options);
630
631 var jsonl = std.Io.Writer.Allocating.init(std.testing.allocator);
632 defer jsonl.deinit();
633 try writeJsonl(&analyzer, &jsonl.writer, options);
634 try std.testing.expect(std.mem.indexOf(u8, jsonl.written(), "\"schema\":\"tracy.flame/v0\"") != null);
635 try std.testing.expect(std.mem.indexOf(u8, jsonl.written(), "\"clipped_spans\":1") != null);
636 try std.testing.expect(std.mem.indexOf(u8, jsonl.written(), "\"name\":\"child;one\"") != null);
637
638 var folded = std.Io.Writer.Allocating.init(std.testing.allocator);
639 defer folded.deinit();
640 try writeFolded(std.testing.allocator, &analyzer, &folded.writer, options);
641 const folded_text = folded.written();
642 try std.testing.expect(std.mem.indexOf(u8, folded_text, "root 20") != null);
643 try std.testing.expect(std.mem.indexOf(u8, folded_text, "root;child\\;one 50") != null);
644 }