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