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 }