lib/tracy/src/timeline.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 const tree = @import("tree.zig");
7
8 pub const schema = "tracy.timeline/v0";
9
10 pub const Kind = enum {
11 all,
12 zone,
13 message,
14 app_info,
15 frame,
16 plot,
17 alloc,
18 free,
19 thread,
20 fiber,
21 gpu,
22 sample,
23 context,
24 lock,
25 system,
26 metadata,
27
28 pub fn fromName(text: []const u8) ?Kind {
29 if (std.mem.eql(u8, text, "all")) return .all;
30 if (std.mem.eql(u8, text, "zone")) return .zone;
31 if (std.mem.eql(u8, text, "message")) return .message;
32 if (std.mem.eql(u8, text, "app-info")) return .app_info;
33 if (std.mem.eql(u8, text, "frame")) return .frame;
34 if (std.mem.eql(u8, text, "plot")) return .plot;
35 if (std.mem.eql(u8, text, "alloc")) return .alloc;
36 if (std.mem.eql(u8, text, "free")) return .free;
37 if (std.mem.eql(u8, text, "thread")) return .thread;
38 if (std.mem.eql(u8, text, "fiber")) return .fiber;
39 if (std.mem.eql(u8, text, "gpu")) return .gpu;
40 if (std.mem.eql(u8, text, "sample")) return .sample;
41 if (std.mem.eql(u8, text, "context")) return .context;
42 if (std.mem.eql(u8, text, "lock")) return .lock;
43 if (std.mem.eql(u8, text, "system")) return .system;
44 if (std.mem.eql(u8, text, "metadata")) return .metadata;
45 return null;
46 }
47
48 fn tag(self: Kind) []const u8 {
49 return switch (self) {
50 .all => "all",
51 .zone => "zone",
52 .message => "message",
53 .app_info => "app-info",
54 .frame => "frame",
55 .plot => "plot",
56 .alloc => "alloc",
57 .free => "free",
58 .thread => "thread",
59 .fiber => "fiber",
60 .gpu => "gpu",
61 .sample => "sample",
62 .context => "context",
63 .lock => "lock",
64 .system => "system",
65 .metadata => "metadata",
66 };
67 }
68 };
69
70 pub const Sort = enum {
71 time,
72 duration,
73
74 pub fn fromName(text: []const u8) ?Sort {
75 if (std.mem.eql(u8, text, "time")) return .time;
76 if (std.mem.eql(u8, text, "duration")) return .duration;
77 return null;
78 }
79
80 fn tag(self: Sort) []const u8 {
81 return switch (self) {
82 .time => "time",
83 .duration => "duration",
84 };
85 }
86 };
87
88 pub const Options = struct {
89 top: usize = 100,
90 kind: Kind = .all,
91 sort: Sort = .time,
92 thread: ?u64 = null,
93 since_ns: ?u64 = null,
94 until_ns: ?u64 = null,
95 min_duration_ns: u64 = 0,
96 match: ?[]const u8 = null,
97 };
98
99 pub const Counters = struct {
100 events: u64 = 0,
101 instants: u64 = 0,
102 metadata: u64 = 0,
103 };
104
105 const Instant = struct {
106 kind: Kind,
107 event: []const u8,
108 time_ns: u64,
109 thread: u64,
110 id: u64 = 0,
111 name: ?[]u8 = null,
112 text: ?[]u8 = null,
113 file: ?[]u8 = null,
114 function: ?[]u8 = null,
115 line: u32 = 0,
116 column: u32 = 0,
117 color: ?u32 = null,
118 value_u64: ?u64 = null,
119 value_i64: ?i64 = null,
120 value_f64: ?f64 = null,
121 address: u64 = 0,
122 size: u64 = 0,
123 depth: u32 = 0,
124 cpu: ?u32 = null,
125 prev_thread: u64 = 0,
126 next_thread: u64 = 0,
127 target_thread: u64 = 0,
128 pid: u64 = 0,
129 previous_cstate: ?u32 = null,
130 prev_priority: ?i64 = null,
131 next_priority: ?i64 = null,
132 group_hint: ?i64 = null,
133 gpu_context: ?u32 = null,
134 gpu_query: ?u32 = null,
135 gpu_time: ?i64 = null,
136 gpu_period: ?f64 = null,
137 gpu_cpu_delta_ns: ?i64 = null,
138 gpu_context_type: ?[]u8 = null,
139 gpu_has_calibration: bool = false,
140 gpu_serial: bool = false,
141 gpu_annotation_id: ?i64 = null,
142 cpu_package: ?u32 = null,
143 cpu_die: ?u32 = null,
144 cpu_core: ?u32 = null,
145 power_delta_uj: ?u64 = null,
146 hw_sample_kind: ?[]u8 = null,
147 plot_kind: ?[]u8 = null,
148 plot_unit: ?[]u8 = null,
149 plot_step: bool = false,
150 plot_fill: bool = false,
151 lock_kind: ?[]u8 = null,
152 sample_kind: ?[]u8 = null,
153 context_reason: ?[]u8 = null,
154 context_state: ?[]u8 = null,
155
156 fn deinit(self: *Instant, allocator: std.mem.Allocator) void {
157 if (self.name) |name| allocator.free(name);
158 if (self.text) |text| allocator.free(text);
159 if (self.file) |file| allocator.free(file);
160 if (self.function) |function| allocator.free(function);
161 if (self.plot_kind) |plot_kind| allocator.free(plot_kind);
162 if (self.plot_unit) |plot_unit| allocator.free(plot_unit);
163 if (self.lock_kind) |lock_kind| allocator.free(lock_kind);
164 if (self.sample_kind) |sample_kind| allocator.free(sample_kind);
165 if (self.gpu_context_type) |gpu_context_type| allocator.free(gpu_context_type);
166 if (self.hw_sample_kind) |hw_sample_kind| allocator.free(hw_sample_kind);
167 if (self.context_reason) |context_reason| allocator.free(context_reason);
168 if (self.context_state) |context_state| allocator.free(context_state);
169 self.* = undefined;
170 }
171 };
172
173 const Row = struct {
174 kind: Kind,
175 event: []const u8,
176 time_ns: u64,
177 end_ns: u64 = 0,
178 duration_ns: u64 = 0,
179 thread: u64 = 0,
180 depth: usize = 0,
181 id: u64 = 0,
182 state: tree.SpanState = .complete,
183 duration_valid: bool = false,
184 name: ?[]const u8 = null,
185 text: ?[]const u8 = null,
186 file: ?[]const u8 = null,
187 function: ?[]const u8 = null,
188 line: u32 = 0,
189 column: u32 = 0,
190 color: ?u32 = null,
191 value_u64: ?u64 = null,
192 value_i64: ?i64 = null,
193 value_f64: ?f64 = null,
194 address: u64 = 0,
195 size: u64 = 0,
196 sample_depth: u32 = 0,
197 cpu: ?u32 = null,
198 prev_thread: u64 = 0,
199 next_thread: u64 = 0,
200 target_thread: u64 = 0,
201 pid: u64 = 0,
202 previous_cstate: ?u32 = null,
203 prev_priority: ?i64 = null,
204 next_priority: ?i64 = null,
205 group_hint: ?i64 = null,
206 gpu_context: ?u32 = null,
207 gpu_query: ?u32 = null,
208 gpu_time: ?i64 = null,
209 gpu_period: ?f64 = null,
210 gpu_cpu_delta_ns: ?i64 = null,
211 gpu_context_type: ?[]const u8 = null,
212 gpu_has_calibration: bool = false,
213 gpu_serial: bool = false,
214 gpu_annotation_id: ?i64 = null,
215 cpu_package: ?u32 = null,
216 cpu_die: ?u32 = null,
217 cpu_core: ?u32 = null,
218 power_delta_uj: ?u64 = null,
219 hw_sample_kind: ?[]const u8 = null,
220 plot_kind: ?[]const u8 = null,
221 plot_unit: ?[]const u8 = null,
222 plot_step: bool = false,
223 plot_fill: bool = false,
224 lock_kind: ?[]const u8 = null,
225 sample_kind: ?[]const u8 = null,
226 context_reason: ?[]const u8 = null,
227 context_state: ?[]const u8 = null,
228 };
229
230 pub const Analyzer = struct {
231 allocator: std.mem.Allocator,
232 zones: tree.Analyzer,
233 instants: std.ArrayListUnmanaged(Instant) = .empty,
234 counters: Counters = .{},
235 start_ns: ?u64 = null,
236 end_ns: ?u64 = null,
237
238 pub fn init(allocator: std.mem.Allocator) Analyzer {
239 return .{
240 .allocator = allocator,
241 .zones = tree.Analyzer.init(allocator),
242 };
243 }
244
245 pub fn deinit(self: *Analyzer) void {
246 for (self.instants.items) |*instant| instant.deinit(self.allocator);
247 self.instants.deinit(self.allocator);
248 self.zones.deinit();
249 self.* = undefined;
250 }
251
252 pub fn ingestJsonlBytes(self: *Analyzer, bytes: []const u8) !void {
253 var lines = std.mem.splitScalar(u8, bytes, '\n');
254 while (lines.next()) |line| try self.ingestJsonLine(line);
255 }
256
257 pub fn ingestJsonLine(self: *Analyzer, line: []const u8) !void {
258 const text = std.mem.trim(u8, line, " \t\r\n");
259 if (text.len == 0) return;
260 var parsed = try record_mod.parseLine(self.allocator, text);
261 defer parsed.deinit();
262 switch (parsed) {
263 .event => |value| try self.ingest(value),
264 .flight => |report_value| self.zones.recordFlightReport(report_value),
265 }
266 }
267
268 pub fn ingest(self: *Analyzer, parsed: event.Parsed) !void {
269 self.counters.events += 1;
270 if (self.start_ns == null and parsed.time_ns != 0) self.start_ns = parsed.time_ns;
271 if (parsed.time_ns != 0) self.end_ns = parsed.time_ns;
272 try self.zones.ingest(parsed);
273 switch (parsed.kind) {
274 .message => try self.recordInstant(.message, parsed),
275 .app_info => try self.recordInstant(.app_info, parsed),
276 .frame => try self.recordInstant(.frame, parsed),
277 .plot_config, .plot => try self.recordInstant(.plot, parsed),
278 .alloc => try self.recordInstant(.alloc, parsed),
279 .free => try self.recordInstant(.free, parsed),
280 .thread_name => try self.recordInstant(.thread, parsed),
281 .fiber_name, .fiber_enter, .fiber_leave => try self.recordInstant(.fiber, parsed),
282 .gpu_context,
283 .gpu_context_name,
284 .gpu_zone_begin,
285 .gpu_zone_end,
286 .gpu_time,
287 .gpu_calibration,
288 .gpu_time_sync,
289 .gpu_annotation,
290 .gpu_annotation_name,
291 => try self.recordInstant(.gpu, parsed),
292 .sample, .sample_frame => try self.recordInstant(.sample, parsed),
293 .context_switch, .thread_wakeup => try self.recordInstant(.context, parsed),
294 .lock_announce,
295 .lock_terminate,
296 .lock_wait,
297 .lock_obtain,
298 .lock_release,
299 .lock_shared_wait,
300 .lock_shared_obtain,
301 .lock_shared_release,
302 .lock_mark,
303 .lock_name,
304 => try self.recordInstant(.lock, parsed),
305 .thread_context,
306 .thread_pid,
307 .cpu_topology,
308 .sys_time,
309 .sys_power,
310 .hw_sample,
311 => try self.recordInstant(.system, parsed),
312 .zone_text, .zone_name, .zone_color, .zone_value => try self.recordInstant(.metadata, parsed),
313 else => {},
314 }
315 }
316
317 pub fn collectRows(self: *Analyzer, allocator: std.mem.Allocator, options: Options) !std.ArrayListUnmanaged(Row) {
318 var rows: std.ArrayListUnmanaged(Row) = .empty;
319 var spans = try self.zones.collectSpans(allocator);
320 defer spans.deinit(allocator);
321 for (spans.items) |span| {
322 const end_ns = if (span.state == .open)
323 self.end_ns orelse span.start_ns
324 else
325 span.end_ns;
326 const row = Row{
327 .kind = .zone,
328 .event = "zone",
329 .time_ns = span.start_ns,
330 .end_ns = end_ns,
331 .duration_ns = if (span.state == .open and end_ns >= span.start_ns)
332 end_ns - span.start_ns
333 else
334 span.total_ns,
335 .thread = span.thread,
336 .depth = span.depth,
337 .id = span.id,
338 .state = span.state,
339 .duration_valid = span.duration_valid,
340 .name = span.name,
341 .text = span.text,
342 .file = span.file,
343 .function = span.function,
344 .line = span.line,
345 .column = span.column,
346 .color = span.color,
347 .value_u64 = span.value,
348 };
349 if (!rowMatches(row, options)) continue;
350 try rows.append(allocator, row);
351 }
352 for (self.instants.items) |instant| {
353 const row = instantRow(instant);
354 if (!rowMatches(row, options)) continue;
355 try rows.append(allocator, row);
356 }
357 sortRows(rows.items, options.sort);
358 return rows;
359 }
360
361 pub fn durationNs(self: Analyzer) u64 {
362 const start_ns = self.start_ns orelse return 0;
363 const end_ns = self.end_ns orelse return 0;
364 if (end_ns <= start_ns) return 0;
365 return end_ns - start_ns;
366 }
367
368 fn recordInstant(self: *Analyzer, kind: Kind, parsed: event.Parsed) !void {
369 try self.instants.append(self.allocator, .{
370 .kind = kind,
371 .event = parsed.kind.tag(),
372 .time_ns = parsed.time_ns,
373 .thread = parsed.thread,
374 .id = parsed.id,
375 .name = try dupeOptional(self.allocator, parsed.name),
376 .text = try dupeOptional(self.allocator, parsed.text),
377 .file = try dupeOptional(self.allocator, parsed.file),
378 .function = try dupeOptional(self.allocator, parsed.function),
379 .line = parsed.line,
380 .column = parsed.column,
381 .color = parsed.color,
382 .value_u64 = parsed.value_u64,
383 .value_i64 = parsed.value_i64,
384 .value_f64 = parsed.value_f64,
385 .address = parsed.address,
386 .size = parsed.size,
387 .depth = parsed.depth,
388 .cpu = parsed.cpu,
389 .prev_thread = parsed.prev_thread,
390 .next_thread = parsed.next_thread,
391 .target_thread = parsed.target_thread,
392 .pid = parsed.pid,
393 .previous_cstate = parsed.previous_cstate,
394 .prev_priority = parsed.prev_priority,
395 .next_priority = parsed.next_priority,
396 .group_hint = parsed.group_hint,
397 .gpu_context = parsed.gpu_context,
398 .gpu_query = parsed.gpu_query,
399 .gpu_time = parsed.gpu_time,
400 .gpu_period = parsed.gpu_period,
401 .gpu_cpu_delta_ns = parsed.gpu_cpu_delta_ns,
402 .gpu_context_type = try dupeOptional(self.allocator, parsed.gpu_context_type),
403 .gpu_has_calibration = parsed.gpu_has_calibration,
404 .gpu_serial = parsed.gpu_serial,
405 .gpu_annotation_id = parsed.gpu_annotation_id,
406 .cpu_package = parsed.cpu_package,
407 .cpu_die = parsed.cpu_die,
408 .cpu_core = parsed.cpu_core,
409 .power_delta_uj = parsed.power_delta_uj,
410 .hw_sample_kind = try dupeOptional(self.allocator, parsed.hw_sample_kind),
411 .plot_kind = try dupeOptional(self.allocator, parsed.plot_kind),
412 .plot_unit = try dupeOptional(self.allocator, parsed.plot_unit),
413 .plot_step = parsed.plot_step,
414 .plot_fill = parsed.plot_fill,
415 .lock_kind = try dupeOptional(self.allocator, parsed.lock_kind),
416 .sample_kind = try dupeOptional(self.allocator, parsed.sample_kind),
417 .context_reason = try dupeOptional(self.allocator, parsed.context_reason),
418 .context_state = try dupeOptional(self.allocator, parsed.context_state),
419 });
420 self.counters.instants += 1;
421 if (kind == .metadata) self.counters.metadata += 1;
422 }
423 };
424
425 pub fn writeTextFromJsonlPath(
426 allocator: std.mem.Allocator,
427 path: []const u8,
428 writer: *std.Io.Writer,
429 options: Options,
430 ) !void {
431 return report.writeFromJsonlPath(Analyzer, writeText, allocator, path, writer, options);
432 }
433
434 pub fn writeJsonlFromJsonlPath(
435 allocator: std.mem.Allocator,
436 path: []const u8,
437 writer: *std.Io.Writer,
438 options: Options,
439 ) !void {
440 return report.writeFromJsonlPath(Analyzer, writeJsonl, allocator, path, writer, options);
441 }
442
443 pub fn ingestPath(analyzer: *Analyzer, path: []const u8) !void {
444 return report.ingestJsonlPath(analyzer, path);
445 }
446
447 fn writeText(
448 allocator: std.mem.Allocator,
449 analyzer: *Analyzer,
450 writer: *std.Io.Writer,
451 options: Options,
452 ) !void {
453 var rows = try analyzer.collectRows(allocator, options);
454 defer rows.deinit(allocator);
455 try writeTextSummary(analyzer, writer, options, rows.items.len);
456 try analyzer.zones.evidence().writeText(writer);
457 const limit = @min(options.top, rows.items.len);
458 for (rows.items[0..limit]) |row| try writeTextRow(writer, row);
459 }
460
461 fn writeTextSummary(
462 analyzer: *Analyzer,
463 writer: *std.Io.Writer,
464 options: Options,
465 rows: usize,
466 ) !void {
467 try writer.print(
468 "tracy timeline rows={d} events={d} zones={d} completed_zones={d} " ++
469 "open_zones={d} instants={d} metadata={d} duration_ns={d} " ++
470 "kind={s} sort={s}\n",
471 .{
472 rows,
473 analyzer.counters.events,
474 analyzer.zones.counters.started_zones,
475 analyzer.zones.counters.completed_zones,
476 analyzer.zones.active.count(),
477 analyzer.counters.instants,
478 analyzer.counters.metadata,
479 analyzer.durationNs(),
480 options.kind.tag(),
481 options.sort.tag(),
482 },
483 );
484 }
485
486 fn writeTextRow(writer: *std.Io.Writer, row: Row) !void {
487 try writer.print(
488 "timeline kind={s} event={s} time_ns={d}",
489 .{ row.kind.tag(), row.event, row.time_ns },
490 );
491 if (row.kind == .zone) {
492 try writer.print(
493 " end_ns={d} duration_ns={d} state={s} depth={d}",
494 .{ row.end_ns, row.duration_ns, row.state.tag(), row.depth },
495 );
496 }
497 try writer.print(" thread={d}", .{row.thread});
498 if (row.id != 0) try writer.print(" id={d}", .{row.id});
499 if (row.name) |name| {
500 try writer.writeAll(" name=");
501 try pretty_json.writeString(writer, name);
502 }
503 if (row.text) |text| {
504 try writer.writeAll(" text=");
505 try pretty_json.writeString(writer, text);
506 }
507 if (row.file) |file| try writer.print(" file={s}:{d}", .{ file, row.line });
508 if (row.function) |function| {
509 try writer.writeAll(" function=");
510 try pretty_json.writeString(writer, function);
511 }
512 try writeValueText(writer, row);
513 try writeTextDetails(writer, row);
514 try writer.writeByte('\n');
515 }
516
517 fn writeTextDetails(writer: *std.Io.Writer, row: Row) !void {
518 if (row.color) |color| try writer.print(" color={d}", .{color});
519 if (row.address != 0) try writer.print(" address={d}", .{row.address});
520 if (row.size != 0) try writer.print(" size={d}", .{row.size});
521 if (std.mem.eql(u8, row.event, "sample.frame")) {
522 try writer.print(" depth={d}", .{row.sample_depth});
523 }
524 if (row.cpu) |cpu| try writer.print(" cpu={d}", .{cpu});
525 if (row.prev_thread != 0) try writer.print(" prev_thread={d}", .{row.prev_thread});
526 if (row.next_thread != 0) try writer.print(" next_thread={d}", .{row.next_thread});
527 if (row.target_thread != 0) try writer.print(" target_thread={d}", .{row.target_thread});
528 if (row.pid != 0) try writer.print(" pid={d}", .{row.pid});
529 if (row.previous_cstate) |value| try writer.print(" previous_cstate={d}", .{value});
530 if (row.prev_priority) |value| try writer.print(" prev_priority={d}", .{value});
531 if (row.next_priority) |value| try writer.print(" next_priority={d}", .{value});
532 if (row.group_hint) |value| try writer.print(" group_hint={d}", .{value});
533 try writeGpuFieldsText(writer, row);
534 try writeSystemFieldsText(writer, row);
535 if (row.plot_kind) |plot_kind| {
536 try writer.writeAll(" plot_kind=");
537 try pretty_json.writeString(writer, plot_kind);
538 }
539 try writePlotFieldsText(writer, row);
540 try writeTextLabels(writer, row);
541 if (row.kind == .zone) try writer.print(" duration_valid={}", .{row.duration_valid});
542 }
543
544 fn writeTextLabels(writer: *std.Io.Writer, row: Row) !void {
545 if (row.lock_kind) |value| {
546 try writer.writeAll(" lock_kind=");
547 try pretty_json.writeString(writer, value);
548 }
549 if (row.sample_kind) |value| {
550 try writer.writeAll(" sample_kind=");
551 try pretty_json.writeString(writer, value);
552 }
553 if (row.context_reason) |value| {
554 try writer.writeAll(" context_reason=");
555 try pretty_json.writeString(writer, value);
556 }
557 if (row.context_state) |value| {
558 try writer.writeAll(" context_state=");
559 try pretty_json.writeString(writer, value);
560 }
561 }
562
563 fn writeJsonl(
564 allocator: std.mem.Allocator,
565 analyzer: *Analyzer,
566 writer: *std.Io.Writer,
567 options: Options,
568 ) !void {
569 var rows = try analyzer.collectRows(allocator, options);
570 defer rows.deinit(allocator);
571 try writeJsonSummary(analyzer, writer, options, rows.items.len);
572 const limit = @min(options.top, rows.items.len);
573 for (rows.items[0..limit]) |row| try writeJsonRow(writer, row);
574 }
575
576 fn writeJsonSummary(
577 analyzer: *Analyzer,
578 writer: *std.Io.Writer,
579 options: Options,
580 rows: usize,
581 ) !void {
582 var stream = pretty_json.Writer.init(writer, .minified);
583 const object = try stream.object();
584 try object.field("schema", schema);
585 try object.field("kind", "summary");
586 try object.field("rows", rows);
587 try object.field("events", analyzer.counters.events);
588 try object.field("zones", analyzer.zones.counters.started_zones);
589 try object.field("completed_zones", analyzer.zones.counters.completed_zones);
590 try object.field("open_zones", analyzer.zones.active.count());
591 try object.field("instants", analyzer.counters.instants);
592 try object.field("metadata", analyzer.counters.metadata);
593 try object.field("duration_ns", analyzer.durationNs());
594 try object.field("filter_kind", options.kind.tag());
595 try object.field("sort", options.sort.tag());
596 try analyzer.zones.evidence().writeFields(object);
597 try object.endLine();
598 }
599
600 fn writeJsonRow(writer: *std.Io.Writer, row: Row) !void {
601 var stream = pretty_json.Writer.init(writer, .minified);
602 const object = try stream.object();
603 try object.field("schema", schema);
604 try object.field("kind", row.kind.tag());
605 try object.field("event", row.event);
606 try object.field("time_ns", row.time_ns);
607 try object.field("thread", row.thread);
608 if (row.kind == .zone) {
609 try object.field("end_ns", row.end_ns);
610 try object.field("duration_ns", row.duration_ns);
611 try object.field("state", row.state.tag());
612 try object.field("duration_valid", row.duration_valid);
613 try object.field("depth", row.depth);
614 }
615 if (row.id != 0) try object.field("id", row.id);
616 if (row.name) |name| try object.field("name", name);
617 if (row.text) |value| try object.field("text", value);
618 if (row.file) |value| {
619 try object.field("file", value);
620 try object.field("line", row.line);
621 }
622 if (row.function) |value| try object.field("function", value);
623 try writeValueField(object, row);
624 try writeFields(object, row);
625 try object.endLine();
626 }
627
628 fn writeFields(object: pretty_json.Object, row: Row) !void {
629 if (row.color) |value| try object.field("color", value);
630 if (row.address != 0) try object.field("address", row.address);
631 if (row.size != 0) try object.field("size", row.size);
632 if (std.mem.eql(u8, row.event, "sample.frame")) try object.field("depth", row.sample_depth);
633 if (row.cpu) |value| try object.field("cpu", value);
634 if (row.prev_thread != 0) try object.field("prev_thread", row.prev_thread);
635 if (row.next_thread != 0) try object.field("next_thread", row.next_thread);
636 if (row.target_thread != 0) try object.field("target_thread", row.target_thread);
637 if (row.pid != 0) try object.field("pid", row.pid);
638 if (row.previous_cstate) |value| try object.field("previous_cstate", value);
639 if (row.prev_priority) |value| try object.field("prev_priority", value);
640 if (row.next_priority) |value| try object.field("next_priority", value);
641 if (row.group_hint) |value| try object.field("group_hint", value);
642 try writeGpuFields(object, row);
643 try writeSystemFields(object, row);
644 if (row.plot_kind) |value| try object.field("plot_kind", value);
645 try writePlotFields(object, row);
646 try writeLabels(object, row);
647 }
648
649 fn writeLabels(object: pretty_json.Object, row: Row) !void {
650 if (row.lock_kind) |value| try object.field("lock_kind", value);
651 if (row.sample_kind) |value| try object.field("sample_kind", value);
652 if (row.context_reason) |value| try object.field("context_reason", value);
653 if (row.context_state) |value| try object.field("context_state", value);
654 }
655
656 fn rowMatches(row: Row, options: Options) bool {
657 if (options.kind != .all and row.kind != options.kind) return false;
658 if (options.thread) |thread| {
659 if (row.thread != thread) return false;
660 }
661 if (row.kind == .zone and row.duration_ns < options.min_duration_ns) return false;
662 if (options.since_ns) |since_ns| {
663 const row_end = if (row.kind == .zone) row.end_ns else row.time_ns;
664 if (row_end < since_ns) return false;
665 }
666 if (options.until_ns) |until_ns| {
667 if (row.time_ns > until_ns) return false;
668 }
669 if (options.match) |needle| {
670 if (!rowContains(row, needle)) return false;
671 }
672 return true;
673 }
674
675 fn rowContains(row: Row, needle: []const u8) bool {
676 if (contains(row.kind.tag(), needle)) return true;
677 if (contains(row.event, needle)) return true;
678 if (row.name) |name| if (contains(name, needle)) return true;
679 if (row.text) |text| if (contains(text, needle)) return true;
680 if (row.file) |file| if (contains(file, needle)) return true;
681 if (row.function) |function| if (contains(function, needle)) return true;
682 if (row.plot_kind) |plot_kind| if (contains(plot_kind, needle)) return true;
683 if (row.plot_unit) |plot_unit| if (contains(plot_unit, needle)) return true;
684 if (row.lock_kind) |lock_kind| if (contains(lock_kind, needle)) return true;
685 if (row.sample_kind) |sample_kind| if (contains(sample_kind, needle)) return true;
686 if (row.gpu_context_type) |context_type| if (contains(context_type, needle)) return true;
687 if (row.hw_sample_kind) |sample_kind| if (contains(sample_kind, needle)) return true;
688 if (row.context_reason) |context_reason| if (contains(context_reason, needle)) return true;
689 if (row.context_state) |context_state| if (contains(context_state, needle)) return true;
690 return false;
691 }
692
693 fn contains(haystack: []const u8, needle: []const u8) bool {
694 return std.mem.indexOf(u8, haystack, needle) != null;
695 }
696
697 fn instantRow(instant: Instant) Row {
698 return .{
699 .kind = instant.kind,
700 .event = instant.event,
701 .time_ns = instant.time_ns,
702 .thread = instant.thread,
703 .id = instant.id,
704 .name = instant.name,
705 .text = instant.text,
706 .file = instant.file,
707 .function = instant.function,
708 .line = instant.line,
709 .column = instant.column,
710 .color = instant.color,
711 .value_u64 = instant.value_u64,
712 .value_i64 = instant.value_i64,
713 .value_f64 = instant.value_f64,
714 .address = instant.address,
715 .size = instant.size,
716 .sample_depth = instant.depth,
717 .cpu = instant.cpu,
718 .prev_thread = instant.prev_thread,
719 .next_thread = instant.next_thread,
720 .target_thread = instant.target_thread,
721 .pid = instant.pid,
722 .previous_cstate = instant.previous_cstate,
723 .prev_priority = instant.prev_priority,
724 .next_priority = instant.next_priority,
725 .group_hint = instant.group_hint,
726 .gpu_context = instant.gpu_context,
727 .gpu_query = instant.gpu_query,
728 .gpu_time = instant.gpu_time,
729 .gpu_period = instant.gpu_period,
730 .gpu_cpu_delta_ns = instant.gpu_cpu_delta_ns,
731 .gpu_context_type = instant.gpu_context_type,
732 .gpu_has_calibration = instant.gpu_has_calibration,
733 .gpu_serial = instant.gpu_serial,
734 .gpu_annotation_id = instant.gpu_annotation_id,
735 .cpu_package = instant.cpu_package,
736 .cpu_die = instant.cpu_die,
737 .cpu_core = instant.cpu_core,
738 .power_delta_uj = instant.power_delta_uj,
739 .hw_sample_kind = instant.hw_sample_kind,
740 .plot_kind = instant.plot_kind,
741 .plot_unit = instant.plot_unit,
742 .plot_step = instant.plot_step,
743 .plot_fill = instant.plot_fill,
744 .lock_kind = instant.lock_kind,
745 .sample_kind = instant.sample_kind,
746 .context_reason = instant.context_reason,
747 .context_state = instant.context_state,
748 };
749 }
750
751 fn sortRows(items: []Row, sort: Sort) void {
752 std.mem.sort(Row, items, sort, rowLessThan);
753 }
754
755 fn rowLessThan(sort: Sort, left: Row, right: Row) bool {
756 return switch (sort) {
757 .time => rowTimeLessThan({}, left, right),
758 .duration => rowDurationGreaterThan({}, left, right),
759 };
760 }
761
762 fn rowTimeLessThan(_: void, left: Row, right: Row) bool {
763 if (left.time_ns != right.time_ns) return left.time_ns < right.time_ns;
764 if (left.duration_ns != right.duration_ns) return left.duration_ns > right.duration_ns;
765 return std.mem.lessThan(u8, left.event, right.event);
766 }
767
768 fn rowDurationGreaterThan(_: void, left: Row, right: Row) bool {
769 if (left.duration_ns != right.duration_ns) return left.duration_ns > right.duration_ns;
770 return rowTimeLessThan({}, left, right);
771 }
772
773 fn dupeOptional(allocator: std.mem.Allocator, text: ?[]const u8) !?[]u8 {
774 const actual = text orelse return null;
775 return try allocator.dupe(u8, actual);
776 }
777
778 fn writeValueText(writer: *std.Io.Writer, row: Row) !void {
779 if (row.value_f64) |value| {
780 try writer.print(" value={d}", .{value});
781 } else if (row.value_i64) |value| {
782 try writer.print(" value={d}", .{value});
783 } else if (row.value_u64) |value| {
784 try writer.print(" value={d}", .{value});
785 }
786 }
787
788 fn writeValueField(object: pretty_json.Object, row: Row) !void {
789 if (row.value_f64) |value| {
790 try object.field("value", value);
791 } else if (row.value_i64) |value| {
792 try object.field("value", value);
793 } else if (row.value_u64) |value| {
794 try object.field("value", value);
795 }
796 }
797
798 fn writePlotFieldsText(writer: *std.Io.Writer, row: Row) !void {
799 if (row.plot_unit) |plot_unit| {
800 try writer.writeAll(" plot_unit=");
801 try pretty_json.writeString(writer, plot_unit);
802 }
803 if (std.mem.eql(u8, row.event, "plot.config")) {
804 try writer.print(" plot_step={} plot_fill={}", .{ row.plot_step, row.plot_fill });
805 }
806 }
807
808 fn writePlotFields(object: pretty_json.Object, row: Row) !void {
809 if (row.plot_unit) |plot_unit| try object.field("plot_unit", plot_unit);
810 if (std.mem.eql(u8, row.event, "plot.config")) {
811 try object.field("plot_step", row.plot_step);
812 try object.field("plot_fill", row.plot_fill);
813 }
814 }
815
816 fn writeGpuFieldsText(writer: *std.Io.Writer, row: Row) !void {
817 if (row.gpu_context) |context| try writer.print(" gpu_context={d}", .{context});
818 if (row.gpu_query) |query| try writer.print(" gpu_query={d}", .{query});
819 if (row.gpu_time) |gpu_time| try writer.print(" gpu_time={d}", .{gpu_time});
820 if (row.gpu_period) |period| try writer.print(" gpu_period={d}", .{period});
821 if (row.gpu_cpu_delta_ns) |delta| try writer.print(" gpu_cpu_delta_ns={d}", .{delta});
822 if (row.gpu_context_type) |context_type| {
823 try writer.writeAll(" gpu_context_type=");
824 try pretty_json.writeString(writer, context_type);
825 }
826 if (row.gpu_has_calibration) try writer.writeAll(" gpu_has_calibration=true");
827 if (row.gpu_serial) try writer.writeAll(" gpu_serial=true");
828 if (row.gpu_annotation_id) |annotation_id| try writer.print(" gpu_annotation_id={d}", .{annotation_id});
829 }
830
831 fn writeGpuFields(object: pretty_json.Object, row: Row) !void {
832 if (row.gpu_context) |context| try object.field("gpu_context", context);
833 if (row.gpu_query) |query| try object.field("gpu_query", query);
834 if (row.gpu_time) |gpu_time| try object.field("gpu_time", gpu_time);
835 if (row.gpu_period) |period| try object.field("gpu_period", period);
836 if (row.gpu_cpu_delta_ns) |delta| try object.field("gpu_cpu_delta_ns", delta);
837 if (row.gpu_context_type) |context_type| try object.field("gpu_context_type", context_type);
838 if (row.gpu_has_calibration) try object.field("gpu_has_calibration", true);
839 if (row.gpu_serial) try object.field("gpu_serial", true);
840 if (row.gpu_annotation_id) |annotation_id| try object.field("gpu_annotation_id", annotation_id);
841 }
842
843 fn writeSystemFieldsText(writer: *std.Io.Writer, row: Row) !void {
844 if (row.cpu_package) |package| try writer.print(" cpu_package={d}", .{package});
845 if (row.cpu_die) |die| try writer.print(" cpu_die={d}", .{die});
846 if (row.cpu_core) |core| try writer.print(" cpu_core={d}", .{core});
847 if (row.power_delta_uj) |delta| try writer.print(" power_delta_uj={d}", .{delta});
848 if (row.hw_sample_kind) |kind| {
849 try writer.writeAll(" hw_sample_kind=");
850 try pretty_json.writeString(writer, kind);
851 }
852 }
853
854 fn writeSystemFields(object: pretty_json.Object, row: Row) !void {
855 if (row.cpu_package) |package| try object.field("cpu_package", package);
856 if (row.cpu_die) |die| try object.field("cpu_die", die);
857 if (row.cpu_core) |core| try object.field("cpu_core", core);
858 if (row.power_delta_uj) |delta| try object.field("power_delta_uj", delta);
859 if (row.hw_sample_kind) |kind| try object.field("hw_sample_kind", kind);
860 }
861
862 test "timeline combines spans and instant events by time" {
863 var trace = std.Io.Writer.Allocating.init(std.testing.allocator);
864 defer trace.deinit();
865 try (event.TraceEvent{ .seq = 1, .kind = .start, .time_ns = 90, .thread = 1, .name = "test" }).writeJsonLine(&trace.writer);
866 try (event.TraceEvent{ .seq = 2, .kind = .thread_name, .time_ns = 95, .thread = 1, .name = "main" }).writeJsonLine(&trace.writer);
867 try (event.TraceEvent{ .seq = 3, .kind = .zone_begin, .time_ns = 100, .thread = 1, .id = 1, .name = "root", .file = "root.zig", .line = 7 }).writeJsonLine(&trace.writer);
868 try (event.TraceEvent{ .seq = 4, .kind = .zone_begin, .time_ns = 120, .thread = 1, .id = 2, .name = "child" }).writeJsonLine(&trace.writer);
869 try (event.TraceEvent{ .seq = 5, .kind = .message, .time_ns = 125, .thread = 1, .text = "inside child" }).writeJsonLine(&trace.writer);
870 try (event.TraceEvent{ .seq = 6, .kind = .zone_text, .time_ns = 130, .thread = 1, .id = 2, .text = "child detail" }).writeJsonLine(&trace.writer);
871 try (event.TraceEvent{ .seq = 7, .kind = .zone_end, .time_ns = 150, .thread = 1, .id = 2 }).writeJsonLine(&trace.writer);
872 try (event.TraceEvent{ .seq = 8, .kind = .plot, .time_ns = 155, .thread = 1, .name = "latency.ns", .value_i64 = 30, .plot_kind = "int" }).writeJsonLine(&trace.writer);
873 try (event.TraceEvent{ .seq = 9, .kind = .alloc, .time_ns = 170, .thread = 1, .name = "arena", .address = 4096, .size = 64 }).writeJsonLine(&trace.writer);
874 try (event.TraceEvent{ .seq = 10, .kind = .zone_end, .time_ns = 210, .thread = 1, .id = 1 }).writeJsonLine(&trace.writer);
875 try (event.TraceEvent{ .seq = 11, .kind = .stop, .time_ns = 220, .thread = 1 }).writeJsonLine(&trace.writer);
876
877 var analyzer = Analyzer.init(std.testing.allocator);
878 defer analyzer.deinit();
879 try analyzer.ingestJsonlBytes(trace.written());
880
881 var out = std.Io.Writer.Allocating.init(std.testing.allocator);
882 defer out.deinit();
883 try writeText(std.testing.allocator, &analyzer, &out.writer, .{ .top = 8, .sort = .time });
884 const text = out.written();
885 try std.testing.expect(std.mem.indexOf(u8, text, "tracy timeline rows=7 events=11 zones=2 completed_zones=2") != null);
886 try std.testing.expect(std.mem.indexOf(u8, text, "timeline kind=zone event=zone time_ns=100 end_ns=210 duration_ns=110 state=complete depth=0 thread=1 id=1 name=\"root\"") != null);
887 try std.testing.expect(std.mem.indexOf(u8, text, "timeline kind=zone event=zone time_ns=120 end_ns=150 duration_ns=30 state=complete depth=1 thread=1 id=2 name=\"child\" text=\"child detail\"") != null);
888 try std.testing.expect(std.mem.indexOf(u8, text, "timeline kind=message event=message time_ns=125 thread=1 text=\"inside child\"") != null);
889 try std.testing.expect(std.mem.indexOf(u8, text, "timeline kind=plot event=plot time_ns=155 thread=1 name=\"latency.ns\" value=30 plot_kind=\"int\"") != null);
890 try std.testing.expect(std.mem.indexOf(u8, text, "timeline kind=alloc event=alloc time_ns=170 thread=1 name=\"arena\" address=4096 size=64") != null);
891 }
892
893 test "timeline jsonl filters by window kind and match text" {
894 var trace = std.Io.Writer.Allocating.init(std.testing.allocator);
895 defer trace.deinit();
896 try (event.TraceEvent{ .seq = 1, .kind = .zone_begin, .time_ns = 100, .thread = 1, .id = 1, .name = "root" }).writeJsonLine(&trace.writer);
897 try (event.TraceEvent{ .seq = 2, .kind = .message, .time_ns = 120, .thread = 1, .text = "queue ready" }).writeJsonLine(&trace.writer);
898 try (event.TraceEvent{ .seq = 3, .kind = .message, .time_ns = 180, .thread = 2, .text = "other thread" }).writeJsonLine(&trace.writer);
899 try (event.TraceEvent{ .seq = 4, .kind = .zone_text, .time_ns = 190, .thread = 1, .id = 1, .text = "root detail" }).writeJsonLine(&trace.writer);
900 try (event.TraceEvent{ .seq = 5, .kind = .zone_end, .time_ns = 240, .thread = 1, .id = 1 }).writeJsonLine(&trace.writer);
901
902 var analyzer = Analyzer.init(std.testing.allocator);
903 defer analyzer.deinit();
904 try analyzer.ingestJsonlBytes(trace.written());
905
906 var out = std.Io.Writer.Allocating.init(std.testing.allocator);
907 defer out.deinit();
908 try writeJsonl(std.testing.allocator, &analyzer, &out.writer, .{ .top = 4, .kind = .message, .since_ns = 110, .until_ns = 140, .match = "queue" });
909 const text = out.written();
910 try std.testing.expect(std.mem.indexOf(u8, text, "\"schema\":\"tracy.timeline/v0\"") != null);
911 try std.testing.expect(std.mem.indexOf(u8, text, "\"kind\":\"summary\",\"rows\":1") != null);
912 try std.testing.expect(std.mem.indexOf(u8, text, "\"kind\":\"message\"") != null);
913 try std.testing.expect(std.mem.indexOf(u8, text, "\"text\":\"queue ready\"") != null);
914 try std.testing.expect(std.mem.indexOf(u8, text, "\"other thread\"") == null);
915 }
916
917 test "timeline includes sample instants and frames" {
918 var trace = std.Io.Writer.Allocating.init(std.testing.allocator);
919 defer trace.deinit();
920 try (event.TraceEvent{ .seq = 1, .kind = .sample, .time_ns = 100, .thread = 1, .id = 9, .name = "tick", .sample_kind = "cpu", .file = "hot.zig", .line = 4 }).writeJsonLine(&trace.writer);
921 try (event.TraceEvent{ .seq = 2, .kind = .sample_frame, .time_ns = 100, .thread = 1, .id = 9, .depth = 1, .name = "hot", .sample_kind = "cpu" }).writeJsonLine(&trace.writer);
922
923 var analyzer = Analyzer.init(std.testing.allocator);
924 defer analyzer.deinit();
925 try analyzer.ingestJsonlBytes(trace.written());
926
927 var out = std.Io.Writer.Allocating.init(std.testing.allocator);
928 defer out.deinit();
929 try writeText(std.testing.allocator, &analyzer, &out.writer, .{ .top = 4, .kind = .sample });
930 const text = out.written();
931 try std.testing.expect(std.mem.indexOf(u8, text, "tracy timeline rows=2 events=2") != null);
932 try std.testing.expect(std.mem.indexOf(u8, text, "timeline kind=sample event=sample time_ns=100 thread=1 id=9 name=\"tick\" file=hot.zig:4 sample_kind=\"cpu\"") != null);
933 try std.testing.expect(std.mem.indexOf(u8, text, "timeline kind=sample event=sample.frame time_ns=100 thread=1 id=9 name=\"hot\" depth=1 sample_kind=\"cpu\"") != null);
934 }
935
936 test "timeline includes context switches and wakeups" {
937 var trace = std.Io.Writer.Allocating.init(std.testing.allocator);
938 defer trace.deinit();
939 try (event.TraceEvent{ .seq = 1, .kind = .context_switch, .time_ns = 100, .thread = 1, .cpu = 0, .prev_thread = 10, .next_thread = 11, .context_reason = "WrPreempted", .context_state = "Ready" }).writeJsonLine(&trace.writer);
940 try (event.TraceEvent{ .seq = 2, .kind = .thread_wakeup, .time_ns = 120, .thread = 1, .target_thread = 10, .cpu = 0, .context_reason = "Unwait" }).writeJsonLine(&trace.writer);
941
942 var analyzer = Analyzer.init(std.testing.allocator);
943 defer analyzer.deinit();
944 try analyzer.ingestJsonlBytes(trace.written());
945
946 var out = std.Io.Writer.Allocating.init(std.testing.allocator);
947 defer out.deinit();
948 try writeText(std.testing.allocator, &analyzer, &out.writer, .{ .top = 4, .kind = .context });
949 const text = out.written();
950 try std.testing.expect(std.mem.indexOf(u8, text, "tracy timeline rows=2 events=2") != null);
951 try std.testing.expect(std.mem.indexOf(u8, text, "timeline kind=context event=context.switch time_ns=100 thread=1 cpu=0 prev_thread=10 next_thread=11 context_reason=\"WrPreempted\" context_state=\"Ready\"") != null);
952 try std.testing.expect(std.mem.indexOf(u8, text, "timeline kind=context event=thread.wakeup time_ns=120 thread=1 cpu=0 target_thread=10 context_reason=\"Unwait\"") != null);
953 }
954
955 test "timeline includes fiber instants" {
956 var trace = std.Io.Writer.Allocating.init(std.testing.allocator);
957 defer trace.deinit();
958 try (event.TraceEvent{ .seq = 1, .kind = .fiber_name, .time_ns = 90, .thread = 10, .id = 7, .name = "job.parse", .group_hint = 3 }).writeJsonLine(&trace.writer);
959 try (event.TraceEvent{ .seq = 2, .kind = .fiber_enter, .time_ns = 100, .thread = 10, .id = 7, .name = "job.parse", .group_hint = 3 }).writeJsonLine(&trace.writer);
960 try (event.TraceEvent{ .seq = 3, .kind = .fiber_leave, .time_ns = 140, .thread = 10, .id = 7 }).writeJsonLine(&trace.writer);
961
962 var analyzer = Analyzer.init(std.testing.allocator);
963 defer analyzer.deinit();
964 try analyzer.ingestJsonlBytes(trace.written());
965
966 var out = std.Io.Writer.Allocating.init(std.testing.allocator);
967 defer out.deinit();
968 try writeText(std.testing.allocator, &analyzer, &out.writer, .{ .top = 4, .kind = .fiber });
969 const text = out.written();
970 try std.testing.expect(std.mem.indexOf(u8, text, "tracy timeline rows=3 events=3") != null);
971 try std.testing.expect(std.mem.indexOf(u8, text, "timeline kind=fiber event=fiber.name time_ns=90 thread=10 id=7 name=\"job.parse\" group_hint=3") != null);
972 try std.testing.expect(std.mem.indexOf(u8, text, "timeline kind=fiber event=fiber.enter time_ns=100 thread=10 id=7 name=\"job.parse\" group_hint=3") != null);
973 try std.testing.expect(std.mem.indexOf(u8, text, "timeline kind=fiber event=fiber.leave time_ns=140 thread=10 id=7") != null);
974 }
975
976 test "timeline includes gpu instants" {
977 var trace = std.Io.Writer.Allocating.init(std.testing.allocator);
978 defer trace.deinit();
979 try (event.TraceEvent{ .seq = 1, .kind = .gpu_context, .time_ns = 90, .thread = 10, .gpu_context = 2, .name = "render", .gpu_period = 1.0, .gpu_context_type = "vulkan" }).writeJsonLine(&trace.writer);
980 try (event.TraceEvent{ .seq = 2, .kind = .gpu_zone_begin, .time_ns = 100, .thread = 10, .gpu_context = 2, .gpu_query = 11, .name = "draw" }).writeJsonLine(&trace.writer);
981 try (event.TraceEvent{ .seq = 3, .kind = .gpu_time, .time_ns = 140, .thread = 10, .gpu_context = 2, .gpu_query = 11, .gpu_time = 55 }).writeJsonLine(&trace.writer);
982
983 var analyzer = Analyzer.init(std.testing.allocator);
984 defer analyzer.deinit();
985 try analyzer.ingestJsonlBytes(trace.written());
986
987 var out = std.Io.Writer.Allocating.init(std.testing.allocator);
988 defer out.deinit();
989 try writeText(std.testing.allocator, &analyzer, &out.writer, .{ .top = 4, .kind = .gpu });
990 const text = out.written();
991 try std.testing.expect(std.mem.indexOf(u8, text, "tracy timeline rows=3 events=3") != null);
992 try std.testing.expect(std.mem.indexOf(u8, text, "timeline kind=gpu event=gpu.context time_ns=90 thread=10 name=\"render\" gpu_context=2 gpu_period=1 gpu_context_type=\"vulkan\"") != null);
993 try std.testing.expect(std.mem.indexOf(u8, text, "timeline kind=gpu event=gpu.zone.begin time_ns=100 thread=10 name=\"draw\" gpu_context=2 gpu_query=11") != null);
994 try std.testing.expect(std.mem.indexOf(u8, text, "timeline kind=gpu event=gpu.time time_ns=140 thread=10 gpu_context=2 gpu_query=11 gpu_time=55") != null);
995 }
996
997 test "timeline includes system instants" {
998 var trace = std.Io.Writer.Allocating.init(std.testing.allocator);
999 defer trace.deinit();
1000 try (event.TraceEvent{ .seq = 1, .kind = .thread_pid, .time_ns = 90, .thread = 42, .pid = 900 }).writeJsonLine(&trace.writer);
1001 try (event.TraceEvent{ .seq = 2, .kind = .cpu_topology, .time_ns = 100, .thread = 1, .cpu = 7, .cpu_package = 1, .cpu_die = 0, .cpu_core = 3 }).writeJsonLine(&trace.writer);
1002 try (event.TraceEvent{ .seq = 3, .kind = .hw_sample, .time_ns = 110, .thread = 42, .cpu = 7, .address = 0xabc, .hw_sample_kind = "cache-miss" }).writeJsonLine(&trace.writer);
1003
1004 var analyzer = Analyzer.init(std.testing.allocator);
1005 defer analyzer.deinit();
1006 try analyzer.ingestJsonlBytes(trace.written());
1007
1008 var out = std.Io.Writer.Allocating.init(std.testing.allocator);
1009 defer out.deinit();
1010 try writeText(std.testing.allocator, &analyzer, &out.writer, .{ .top = 4, .kind = .system });
1011 const text = out.written();
1012 try std.testing.expect(std.mem.indexOf(u8, text, "tracy timeline rows=3 events=3") != null);
1013 try std.testing.expect(std.mem.indexOf(u8, text, "timeline kind=system event=thread.pid time_ns=90 thread=42 pid=900") != null);
1014 try std.testing.expect(std.mem.indexOf(u8, text, "timeline kind=system event=cpu.topology time_ns=100 thread=1 cpu=7 cpu_package=1 cpu_die=0 cpu_core=3") != null);
1015 try std.testing.expect(std.mem.indexOf(u8, text, "timeline kind=system event=hw.sample time_ns=110 thread=42 address=2748 cpu=7 hw_sample_kind=\"cache-miss\"") != null);
1016 }
1017
1018 test "timeline preserves plot configuration metadata" {
1019 var trace = std.Io.Writer.Allocating.init(std.testing.allocator);
1020 defer trace.deinit();
1021 try (event.TraceEvent{
1022 .seq = 1,
1023 .kind = .plot_config,
1024 .time_ns = 100,
1025 .thread = 1,
1026 .name = "heap.live",
1027 .color = 7,
1028 .plot_unit = "bytes",
1029 .plot_step = true,
1030 .plot_fill = true,
1031 }).writeJsonLine(&trace.writer);
1032
1033 var analyzer = Analyzer.init(std.testing.allocator);
1034 defer analyzer.deinit();
1035 try analyzer.ingestJsonlBytes(trace.written());
1036
1037 var text_out = std.Io.Writer.Allocating.init(std.testing.allocator);
1038 defer text_out.deinit();
1039 try writeText(std.testing.allocator, &analyzer, &text_out.writer, .{ .kind = .plot });
1040 const text_metadata =
1041 "name=\"heap.live\" color=7 plot_unit=\"bytes\" plot_step=true plot_fill=true";
1042 try std.testing.expect(std.mem.indexOf(u8, text_out.written(), text_metadata) != null);
1043
1044 var json_out = std.Io.Writer.Allocating.init(std.testing.allocator);
1045 defer json_out.deinit();
1046 try writeJsonl(std.testing.allocator, &analyzer, &json_out.writer, .{ .kind = .plot });
1047 const json = json_out.written();
1048 try std.testing.expect(std.mem.indexOf(u8, json, "\"event\":\"plot.config\"") != null);
1049 try std.testing.expect(std.mem.indexOf(u8, json, "\"plot_unit\":\"bytes\"") != null);
1050 try std.testing.expect(std.mem.indexOf(u8, json, "\"plot_step\":true") != null);
1051 try std.testing.expect(std.mem.indexOf(u8, json, "\"plot_fill\":true") != null);
1052 }