lib/tracy/src/source.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 tree = @import("tree.zig");
6
7 pub const schema = "tracy.source/v0";
8
9 const max_source_bytes = 8 * 1024 * 1024;
10
11 pub const Sort = enum {
12 self,
13 total,
14 mean,
15 count,
16 file,
17 name,
18
19 pub fn fromName(text: []const u8) ?Sort {
20 if (std.mem.eql(u8, text, "self")) return .self;
21 if (std.mem.eql(u8, text, "total")) return .total;
22 if (std.mem.eql(u8, text, "mean")) return .mean;
23 if (std.mem.eql(u8, text, "count")) return .count;
24 if (std.mem.eql(u8, text, "file")) return .file;
25 if (std.mem.eql(u8, text, "name")) return .name;
26 return null;
27 }
28
29 fn tag(self: Sort) []const u8 {
30 return switch (self) {
31 .self => "self",
32 .total => "total",
33 .mean => "mean",
34 .count => "count",
35 .file => "file",
36 .name => "name",
37 };
38 }
39 };
40
41 pub const Options = struct {
42 top: usize = 20,
43 sort: Sort = .self,
44 min_total_ns: u64 = 0,
45 min_self_ns: u64 = 0,
46 thread: ?u64 = null,
47 match: ?[]const u8 = null,
48 root: ?[]const u8 = null,
49 context: usize = 2,
50 context_back: usize = 2,
51 ignore_case: bool = false,
52 };
53
54 pub const Counters = struct {
55 spans: u64 = 0,
56 matched_spans: u64 = 0,
57 open_spans: u64 = 0,
58 superseded_spans: u64 = 0,
59 invalid_duration_spans: u64 = 0,
60 zero_spans: u64 = 0,
61 no_file_spans: u64 = 0,
62 filtered_spans: u64 = 0,
63 rows: u64 = 0,
64 duration_ns: u64 = 0,
65 };
66
67 const Row = struct {
68 name: []u8,
69 file: []u8,
70 function: ?[]u8 = null,
71 line: u32 = 0,
72 column: u32 = 0,
73 count: u64 = 0,
74 total_ns: u64 = 0,
75 self_ns: u64 = 0,
76 min_ns: u64 = std.math.maxInt(u64),
77 max_ns: u64 = 0,
78 min_self_ns: u64 = std.math.maxInt(u64),
79 max_self_ns: u64 = 0,
80 max_depth: usize = 0,
81 threads: std.ArrayListUnmanaged(u64) = .empty,
82
83 fn deinit(self: *Row, allocator: std.mem.Allocator) void {
84 allocator.free(self.name);
85 allocator.free(self.file);
86 if (self.function) |function| allocator.free(function);
87 self.threads.deinit(allocator);
88 self.* = undefined;
89 }
90
91 fn meanNs(self: Row) u64 {
92 if (self.count == 0) return 0;
93 return self.total_ns / self.count;
94 }
95
96 fn meanSelfNs(self: Row) u64 {
97 if (self.count == 0) return 0;
98 return self.self_ns / self.count;
99 }
100
101 fn threadCount(self: Row) u64 {
102 return @intCast(self.threads.items.len);
103 }
104 };
105
106 const View = struct {
107 name: []const u8,
108 file: []const u8,
109 function: ?[]const u8,
110 line: u32,
111 column: u32,
112 count: u64,
113 total_ns: u64,
114 self_ns: u64,
115 min_ns: u64,
116 max_ns: u64,
117 min_self_ns: u64,
118 max_self_ns: u64,
119 mean_ns: u64,
120 mean_self_ns: u64,
121 max_depth: usize,
122 threads: u64,
123 };
124
125 const SourceState = enum {
126 found,
127 missing,
128 out_of_range,
129 invalid_line,
130
131 fn tag(self: SourceState) []const u8 {
132 return switch (self) {
133 .found => "found",
134 .missing => "missing",
135 .out_of_range => "out-of-range",
136 .invalid_line => "invalid-line",
137 };
138 }
139 };
140
141 const SourceContext = struct {
142 path: []u8,
143 state: SourceState,
144 excerpt: []u8,
145 total_lines: u32 = 0,
146
147 fn deinit(self: *SourceContext, allocator: std.mem.Allocator) void {
148 allocator.free(self.path);
149 allocator.free(self.excerpt);
150 self.* = undefined;
151 }
152 };
153
154 const LineScan = struct {
155 total_lines: u32 = 0,
156 target_found: bool = false,
157 };
158
159 pub const Analyzer = struct {
160 allocator: std.mem.Allocator,
161 rows: std.StringHashMapUnmanaged(Row) = .{},
162 counters: Counters = .{},
163 evidence: ?tree.Evidence = null,
164
165 pub fn init(allocator: std.mem.Allocator) Analyzer {
166 return .{ .allocator = allocator };
167 }
168
169 pub fn deinit(self: *Analyzer) void {
170 var iter = self.rows.iterator();
171 while (iter.next()) |entry| {
172 self.allocator.free(entry.key_ptr.*);
173 entry.value_ptr.deinit(self.allocator);
174 }
175 self.rows.deinit(self.allocator);
176 self.* = undefined;
177 }
178
179 pub fn ingestTree(self: *Analyzer, trace: *tree.Analyzer, options: Options) !void {
180 self.counters.duration_ns = trace.durationNs();
181 self.evidence = trace.evidence();
182 var spans = try trace.collectSpans(self.allocator);
183 defer spans.deinit(self.allocator);
184 self.counters.spans = @intCast(spans.items.len);
185 for (spans.items) |span| {
186 switch (span.state) {
187 .open => {
188 self.counters.open_spans += 1;
189 continue;
190 },
191 .superseded => {
192 self.counters.superseded_spans += 1;
193 continue;
194 },
195 .complete => {},
196 }
197 if (!span.duration_valid) {
198 self.counters.invalid_duration_spans += 1;
199 continue;
200 }
201 if (span.total_ns == 0) {
202 self.counters.zero_spans += 1;
203 continue;
204 }
205 if (options.thread) |thread| {
206 if (span.thread != thread) {
207 self.counters.filtered_spans += 1;
208 continue;
209 }
210 }
211 if (!spanMatches(span, options)) {
212 self.counters.filtered_spans += 1;
213 continue;
214 }
215 const file = span.file orelse {
216 self.counters.no_file_spans += 1;
217 continue;
218 };
219 if (file.len == 0) {
220 self.counters.no_file_spans += 1;
221 continue;
222 }
223 try self.record(span, file);
224 self.counters.matched_spans += 1;
225 }
226 self.counters.rows = @intCast(self.rows.count());
227 }
228
229 fn record(self: *Analyzer, span: tree.Span, file: []const u8) !void {
230 var key_writer = std.Io.Writer.Allocating.init(self.allocator);
231 defer key_writer.deinit();
232 try key_writer.writer.writeAll(file);
233 try key_writer.writer.writeByte('\x1f');
234 if (span.function) |function| try key_writer.writer.writeAll(function);
235 try key_writer.writer.print("\x1f{d}:{d}", .{ span.line, span.column });
236 const key_text = key_writer.written();
237 const entry = try self.rows.getOrPut(self.allocator, key_text);
238 if (!entry.found_existing) {
239 const owned_key = try self.allocator.dupe(u8, key_text);
240 entry.key_ptr.* = owned_key;
241 entry.value_ptr.* = .{
242 .name = try self.allocator.dupe(u8, span.name),
243 .file = try self.allocator.dupe(u8, file),
244 .function = try dupeOptional(self.allocator, span.function),
245 .line = span.line,
246 .column = span.column,
247 };
248 }
249 const row = entry.value_ptr;
250 row.count += 1;
251 row.total_ns +|= span.total_ns;
252 row.self_ns +|= span.self_ns;
253 row.min_ns = @min(row.min_ns, span.total_ns);
254 row.max_ns = @max(row.max_ns, span.total_ns);
255 row.min_self_ns = @min(row.min_self_ns, span.self_ns);
256 row.max_self_ns = @max(row.max_self_ns, span.self_ns);
257 row.max_depth = @max(row.max_depth, span.depth);
258 try appendThread(self.allocator, &row.threads, span.thread);
259 }
260
261 fn collect(self: *Analyzer, allocator: std.mem.Allocator, options: Options) !std.ArrayListUnmanaged(View) {
262 var views: std.ArrayListUnmanaged(View) = .empty;
263 var iter = self.rows.valueIterator();
264 while (iter.next()) |row| {
265 if (row.total_ns < options.min_total_ns) continue;
266 if (row.self_ns < options.min_self_ns) continue;
267 try views.append(allocator, .{
268 .name = row.name,
269 .file = row.file,
270 .function = row.function,
271 .line = row.line,
272 .column = row.column,
273 .count = row.count,
274 .total_ns = row.total_ns,
275 .self_ns = row.self_ns,
276 .min_ns = if (row.min_ns == std.math.maxInt(u64)) 0 else row.min_ns,
277 .max_ns = row.max_ns,
278 .min_self_ns = if (row.min_self_ns == std.math.maxInt(u64)) 0 else row.min_self_ns,
279 .max_self_ns = row.max_self_ns,
280 .mean_ns = row.meanNs(),
281 .mean_self_ns = row.meanSelfNs(),
282 .max_depth = row.max_depth,
283 .threads = row.threadCount(),
284 });
285 }
286 sortViews(views.items, options.sort);
287 return views;
288 }
289 };
290
291 pub fn writeTextFromJsonlPath(
292 allocator: std.mem.Allocator,
293 path: []const u8,
294 writer: *std.Io.Writer,
295 options: Options,
296 ) !void {
297 var trace = tree.Analyzer.init(allocator);
298 defer trace.deinit();
299 try tree.ingestPath(&trace, path);
300 var analyzer = Analyzer.init(allocator);
301 defer analyzer.deinit();
302 try analyzer.ingestTree(&trace, options);
303 try writeText(allocator, &analyzer, writer, options);
304 }
305
306 pub fn writeJsonlFromJsonlPath(
307 allocator: std.mem.Allocator,
308 path: []const u8,
309 writer: *std.Io.Writer,
310 options: Options,
311 ) !void {
312 var trace = tree.Analyzer.init(allocator);
313 defer trace.deinit();
314 try tree.ingestPath(&trace, path);
315 var analyzer = Analyzer.init(allocator);
316 defer analyzer.deinit();
317 try analyzer.ingestTree(&trace, options);
318 try writeJsonl(allocator, &analyzer, writer, options);
319 }
320
321 fn writeText(allocator: std.mem.Allocator, analyzer: *Analyzer, writer: *std.Io.Writer, options: Options) !void {
322 var rows = try analyzer.collect(allocator, options);
323 defer rows.deinit(allocator);
324 try writer.print(
325 "tracy source rows={d} spans={d} matched_spans={d} open_spans={d} " ++
326 "superseded_spans={d} invalid_duration_spans={d} zero_spans={d} " ++
327 "no_file_spans={d} filtered_spans={d} duration_ns={d} sort={s}",
328 .{
329 rows.items.len,
330 analyzer.counters.spans,
331 analyzer.counters.matched_spans,
332 analyzer.counters.open_spans,
333 analyzer.counters.superseded_spans,
334 analyzer.counters.invalid_duration_spans,
335 analyzer.counters.zero_spans,
336 analyzer.counters.no_file_spans,
337 analyzer.counters.filtered_spans,
338 analyzer.counters.duration_ns,
339 options.sort.tag(),
340 },
341 );
342 if (options.root) |root| {
343 try writer.writeAll(" root=");
344 try pretty_json.writeString(writer, root);
345 }
346 if (options.match) |pattern| {
347 try writer.writeAll(" match=");
348 try pretty_json.writeString(writer, pattern);
349 }
350 try writer.writeByte('\n');
351 try analyzer.evidence.?.writeText(writer);
352 const limit = @min(options.top, rows.items.len);
353 for (rows.items[0..limit]) |row| {
354 var source_context = try loadSourceContext(allocator, row.file, row.line, options);
355 defer source_context.deinit(allocator);
356 try writer.writeAll("source-location name=");
357 try pretty_json.writeString(writer, row.name);
358 try writer.print(
359 " count={d} total_ns={d} self_ns={d} mean_ns={d} mean_self_ns={d} min_ns={d} max_ns={d} min_self_ns={d} max_self_ns={d} max_depth={d} threads={d} file={s}:{d}:{d} source_state={s} path=",
360 .{
361 row.count,
362 row.total_ns,
363 row.self_ns,
364 row.mean_ns,
365 row.mean_self_ns,
366 row.min_ns,
367 row.max_ns,
368 row.min_self_ns,
369 row.max_self_ns,
370 row.max_depth,
371 row.threads,
372 row.file,
373 row.line,
374 row.column,
375 source_context.state.tag(),
376 },
377 );
378 try pretty_json.writeString(writer, source_context.path);
379 try writer.print(" total_lines={d}", .{source_context.total_lines});
380 if (row.function) |function| {
381 try writer.writeAll(" function=");
382 try pretty_json.writeString(writer, function);
383 }
384 if (source_context.excerpt.len != 0) {
385 try writer.writeAll(" excerpt=");
386 try pretty_json.writeString(writer, source_context.excerpt);
387 }
388 try writer.writeByte('\n');
389 }
390 }
391
392 fn writeJsonl(allocator: std.mem.Allocator, analyzer: *Analyzer, writer: *std.Io.Writer, options: Options) !void {
393 var rows = try analyzer.collect(allocator, options);
394 defer rows.deinit(allocator);
395 try writeJsonSummary(analyzer, writer, options, rows.items.len);
396 const limit = @min(options.top, rows.items.len);
397 for (rows.items[0..limit]) |row| {
398 var source_context = try loadSourceContext(allocator, row.file, row.line, options);
399 defer source_context.deinit(allocator);
400 try writeJsonLocation(writer, row, source_context);
401 }
402 }
403
404 fn writeJsonSummary(
405 analyzer: *Analyzer,
406 writer: *std.Io.Writer,
407 options: Options,
408 rows: usize,
409 ) !void {
410 var stream = pretty_json.Writer.init(writer, .minified);
411 const object = try stream.object();
412 try object.field("schema", schema);
413 try object.field("kind", "summary");
414 try object.field("rows", rows);
415 try object.field("spans", analyzer.counters.spans);
416 try object.field("matched_spans", analyzer.counters.matched_spans);
417 try object.field("open_spans", analyzer.counters.open_spans);
418 try object.field("superseded_spans", analyzer.counters.superseded_spans);
419 try object.field("invalid_duration_spans", analyzer.counters.invalid_duration_spans);
420 try object.field("zero_spans", analyzer.counters.zero_spans);
421 try object.field("no_file_spans", analyzer.counters.no_file_spans);
422 try object.field("filtered_spans", analyzer.counters.filtered_spans);
423 try object.field("duration_ns", analyzer.counters.duration_ns);
424 try object.field("sort", options.sort.tag());
425 if (options.root) |root| try object.field("root", root);
426 if (options.match) |pattern| try object.field("match", pattern);
427 try analyzer.evidence.?.writeFields(object);
428 try object.endLine();
429 }
430
431 fn writeJsonLocation(writer: *std.Io.Writer, row: View, source_context: SourceContext) !void {
432 var stream = pretty_json.Writer.init(writer, .minified);
433 const object = try stream.object();
434 try object.field("schema", schema);
435 try object.field("kind", "location");
436 try object.field("name", row.name);
437 try object.field("count", row.count);
438 try object.field("total_ns", row.total_ns);
439 try object.field("self_ns", row.self_ns);
440 try object.field("mean_ns", row.mean_ns);
441 try object.field("mean_self_ns", row.mean_self_ns);
442 try object.field("min_ns", row.min_ns);
443 try object.field("max_ns", row.max_ns);
444 try object.field("min_self_ns", row.min_self_ns);
445 try object.field("max_self_ns", row.max_self_ns);
446 try object.field("max_depth", row.max_depth);
447 try object.field("threads", row.threads);
448 try object.field("file", row.file);
449 try object.field("line", row.line);
450 try object.field("column", row.column);
451 try object.field("source_state", source_context.state.tag());
452 try object.field("path", source_context.path);
453 try object.field("total_lines", source_context.total_lines);
454 if (row.function) |function| try object.field("function", function);
455 if (source_context.excerpt.len != 0) try object.field("excerpt", source_context.excerpt);
456 try object.endLine();
457 }
458
459 fn loadSourceContext(allocator: std.mem.Allocator, file: []const u8, line: u32, options: Options) !SourceContext {
460 const path = try resolvePath(allocator, file, options.root);
461 errdefer allocator.free(path);
462 if (line == 0) {
463 return .{
464 .path = path,
465 .state = .invalid_line,
466 .excerpt = try allocator.dupe(u8, ""),
467 };
468 }
469 const contents = sys.fs.cwd().readFileAlloc(sys.fs.debugIo(), path, allocator, .limited(max_source_bytes)) catch |err| switch (err) {
470 error.OutOfMemory => return err,
471 else => return .{
472 .path = path,
473 .state = .missing,
474 .excerpt = try allocator.dupe(u8, ""),
475 },
476 };
477 defer allocator.free(contents);
478 var excerpt = std.Io.Writer.Allocating.init(allocator);
479 defer excerpt.deinit();
480 const start_line = if (line > options.context_back) line - @as(u32, @intCast(@min(options.context_back, line - 1))) else 1;
481 const after = @as(u32, @intCast(@min(options.context, std.math.maxInt(u32) - line)));
482 const end_line = line + after;
483 const scan = try appendSourceLines(&excerpt.writer, contents, start_line, end_line, line);
484 return .{
485 .path = path,
486 .state = if (scan.target_found) .found else .out_of_range,
487 .excerpt = try allocator.dupe(u8, excerpt.written()),
488 .total_lines = scan.total_lines,
489 };
490 }
491
492 fn resolvePath(allocator: std.mem.Allocator, file: []const u8, root: ?[]const u8) ![]u8 {
493 if (std.fs.path.isAbsolute(file)) return try allocator.dupe(u8, file);
494 const actual_root = root orelse return try allocator.dupe(u8, file);
495 if (actual_root.len == 0) return try allocator.dupe(u8, file);
496 return try std.fs.path.join(allocator, &.{ actual_root, file });
497 }
498
499 fn appendSourceLines(writer: *std.Io.Writer, contents: []const u8, start_line: u32, end_line: u32, target_line: u32) !LineScan {
500 if (contents.len == 0) return .{};
501 var scan: LineScan = .{};
502 var line_no: u32 = 1;
503 var start: usize = 0;
504 var index: usize = 0;
505 while (index <= contents.len) : (index += 1) {
506 if (index == contents.len) {
507 if (start == contents.len) break;
508 } else if (contents[index] != '\n') {
509 continue;
510 }
511 var line = contents[start..index];
512 if (line.len != 0 and line[line.len - 1] == '\r') line = line[0 .. line.len - 1];
513 if (line_no >= start_line and line_no <= end_line) {
514 try writer.print("{d}:{s}\n", .{ line_no, line });
515 }
516 if (line_no == target_line) scan.target_found = true;
517 scan.total_lines = line_no;
518 if (line_no == std.math.maxInt(u32)) break;
519 line_no += 1;
520 start = index + 1;
521 }
522 return scan;
523 }
524
525 fn spanMatches(span: tree.Span, options: Options) bool {
526 const pattern = options.match orelse return true;
527 if (pattern.len == 0) return true;
528 if (contains(span.name, pattern, options.ignore_case)) return true;
529 if (span.text) |text| if (contains(text, pattern, options.ignore_case)) return true;
530 if (span.file) |file| if (contains(file, pattern, options.ignore_case)) return true;
531 if (span.function) |function| if (contains(function, pattern, options.ignore_case)) return true;
532 return false;
533 }
534
535 fn contains(haystack: []const u8, needle: []const u8, ignore_case: bool) bool {
536 if (!ignore_case) return std.mem.indexOf(u8, haystack, needle) != null;
537 if (needle.len == 0) return true;
538 if (needle.len > haystack.len) return false;
539 var index: usize = 0;
540 while (index + needle.len <= haystack.len) : (index += 1) {
541 if (asciiEqlIgnoreCase(haystack[index .. index + needle.len], needle)) return true;
542 }
543 return false;
544 }
545
546 fn asciiEqlIgnoreCase(left: []const u8, right: []const u8) bool {
547 if (left.len != right.len) return false;
548 for (left, right) |a, b| {
549 if (std.ascii.toLower(a) != std.ascii.toLower(b)) return false;
550 }
551 return true;
552 }
553
554 fn dupeOptional(allocator: std.mem.Allocator, text: ?[]const u8) !?[]u8 {
555 const actual = text orelse return null;
556 return try allocator.dupe(u8, actual);
557 }
558
559 fn appendThread(allocator: std.mem.Allocator, threads: *std.ArrayListUnmanaged(u64), thread: u64) !void {
560 for (threads.items) |existing| {
561 if (existing == thread) return;
562 }
563 try threads.append(allocator, thread);
564 }
565
566 fn sortViews(items: []View, sort: Sort) void {
567 switch (sort) {
568 .self => std.mem.sort(View, items, {}, selfGreaterThan),
569 .total => std.mem.sort(View, items, {}, totalGreaterThan),
570 .mean => std.mem.sort(View, items, {}, meanGreaterThan),
571 .count => std.mem.sort(View, items, {}, countGreaterThan),
572 .file => std.mem.sort(View, items, {}, fileLessThan),
573 .name => std.mem.sort(View, items, {}, nameLessThan),
574 }
575 }
576
577 fn selfGreaterThan(_: void, left: View, right: View) bool {
578 if (left.self_ns != right.self_ns) return left.self_ns > right.self_ns;
579 return totalGreaterThan({}, left, right);
580 }
581
582 fn totalGreaterThan(_: void, left: View, right: View) bool {
583 if (left.total_ns != right.total_ns) return left.total_ns > right.total_ns;
584 return fileLessThan({}, left, right);
585 }
586
587 fn meanGreaterThan(_: void, left: View, right: View) bool {
588 if (left.mean_ns != right.mean_ns) return left.mean_ns > right.mean_ns;
589 return totalGreaterThan({}, left, right);
590 }
591
592 fn countGreaterThan(_: void, left: View, right: View) bool {
593 if (left.count != right.count) return left.count > right.count;
594 return totalGreaterThan({}, left, right);
595 }
596
597 fn fileLessThan(_: void, left: View, right: View) bool {
598 const file_cmp = std.mem.order(u8, left.file, right.file);
599 if (file_cmp != .eq) return file_cmp == .lt;
600 if (left.line != right.line) return left.line < right.line;
601 if (left.column != right.column) return left.column < right.column;
602 const name_cmp = std.mem.order(u8, left.name, right.name);
603 return name_cmp == .lt;
604 }
605
606 fn nameLessThan(_: void, left: View, right: View) bool {
607 const name_cmp = std.mem.order(u8, left.name, right.name);
608 if (name_cmp != .eq) return name_cmp == .lt;
609 const file_cmp = std.mem.order(u8, left.file, right.file);
610 if (file_cmp != .eq) return file_cmp == .lt;
611 if (left.line != right.line) return left.line < right.line;
612 return left.column < right.column;
613 }
614
615 test "source annotates hot locations with local context" {
616 var tmp = std.testing.tmpDir(.{});
617 defer tmp.cleanup();
618 const root = try tmp.parent_dir.realPathFileAlloc(sys.fs.debugIo(), tmp.sub_path[0..], std.testing.allocator);
619 defer std.testing.allocator.free(root);
620 const source_path = try std.fs.path.join(std.testing.allocator, &.{ root, "phase.zig" });
621 defer std.testing.allocator.free(source_path);
622 try sys.fs.cwd().writeFile(sys.fs.debugIo(), .{
623 .sub_path = source_path,
624 .data = "const x = 1;\nconst y = 2;\nfn work() void {}\nconst z = 4;\n",
625 });
626
627 var trace_bytes = std.Io.Writer.Allocating.init(std.testing.allocator);
628 defer trace_bytes.deinit();
629 try (event.TraceEvent{ .seq = 1, .kind = .zone_begin, .time_ns = 100, .thread = 1, .id = 1, .name = "work", .file = "phase.zig", .function = "work", .line = 3 }).writeJsonLine(&trace_bytes.writer);
630 try (event.TraceEvent{ .seq = 2, .kind = .zone_end, .time_ns = 180, .thread = 1, .id = 1 }).writeJsonLine(&trace_bytes.writer);
631 try (event.TraceEvent{ .seq = 3, .kind = .zone_begin, .time_ns = 190, .thread = 1, .id = 2, .name = "work", .file = "phase.zig", .function = "work", .line = 3 }).writeJsonLine(&trace_bytes.writer);
632 try (event.TraceEvent{ .seq = 4, .kind = .zone_end, .time_ns = 230, .thread = 1, .id = 2 }).writeJsonLine(&trace_bytes.writer);
633
634 var trace = tree.Analyzer.init(std.testing.allocator);
635 defer trace.deinit();
636 try trace.ingestJsonlBytes(trace_bytes.written());
637
638 var analyzer = Analyzer.init(std.testing.allocator);
639 defer analyzer.deinit();
640 const options = Options{ .root = root, .context = 1, .context_back = 1 };
641 try analyzer.ingestTree(&trace, options);
642
643 var out = std.Io.Writer.Allocating.init(std.testing.allocator);
644 defer out.deinit();
645 try writeText(std.testing.allocator, &analyzer, &out.writer, options);
646 const text = out.written();
647 try std.testing.expect(std.mem.indexOf(u8, text, "tracy source rows=1 spans=2 matched_spans=2") != null);
648 try std.testing.expect(std.mem.indexOf(u8, text, "source_state=found") != null);
649 try std.testing.expect(std.mem.indexOf(u8, text, "count=2 total_ns=120 self_ns=120") != null);
650 try std.testing.expect(std.mem.indexOf(u8, text, "excerpt=\"2:const y = 2;\\n3:fn work() void {}\\n4:const z = 4;\\n\"") != null);
651 }
652
653 test "source jsonl reports out of range source lines" {
654 var tmp = std.testing.tmpDir(.{});
655 defer tmp.cleanup();
656 const root = try tmp.parent_dir.realPathFileAlloc(sys.fs.debugIo(), tmp.sub_path[0..], std.testing.allocator);
657 defer std.testing.allocator.free(root);
658 const source_path = try std.fs.path.join(std.testing.allocator, &.{ root, "short.zig" });
659 defer std.testing.allocator.free(source_path);
660 try sys.fs.cwd().writeFile(sys.fs.debugIo(), .{ .sub_path = source_path, .data = "const only = 1;\n" });
661
662 var trace_bytes = std.Io.Writer.Allocating.init(std.testing.allocator);
663 defer trace_bytes.deinit();
664 try (event.TraceEvent{ .seq = 1, .kind = .zone_begin, .time_ns = 100, .thread = 1, .id = 1, .name = "short", .file = "short.zig", .function = "short", .line = 8 }).writeJsonLine(&trace_bytes.writer);
665 try (event.TraceEvent{ .seq = 2, .kind = .zone_end, .time_ns = 140, .thread = 1, .id = 1 }).writeJsonLine(&trace_bytes.writer);
666
667 var trace = tree.Analyzer.init(std.testing.allocator);
668 defer trace.deinit();
669 try trace.ingestJsonlBytes(trace_bytes.written());
670
671 var analyzer = Analyzer.init(std.testing.allocator);
672 defer analyzer.deinit();
673 const options = Options{ .root = root };
674 try analyzer.ingestTree(&trace, options);
675
676 var out = std.Io.Writer.Allocating.init(std.testing.allocator);
677 defer out.deinit();
678 try writeJsonl(std.testing.allocator, &analyzer, &out.writer, options);
679 const text = out.written();
680 try std.testing.expect(std.mem.indexOf(u8, text, "\"schema\":\"tracy.source/v0\"") != null);
681 try std.testing.expect(std.mem.indexOf(u8, text, "\"source_state\":\"out-of-range\"") != null);
682 try std.testing.expect(std.mem.indexOf(u8, text, "\"total_lines\":1") != null);
683 try std.testing.expect(std.mem.indexOf(u8, text, "\"excerpt\":\"\"") == null);
684 }