lib/tracy/src/stats.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const std = @import("std");
  2 const pretty_json = @import("pretty").json;
  3 const event = @import("event.zig");
  4 const tree = @import("tree.zig");
  5 
  6 pub const schema = "tracy.stats/v0";
  7 
  8 pub const Sort = enum {
  9     self,
 10     total,
 11     mean,
 12     count,
 13     threads,
 14     name,
 15 
 16     pub fn fromName(text: []const u8) ?Sort {
 17         if (std.mem.eql(u8, text, "self")) return .self;
 18         if (std.mem.eql(u8, text, "total")) return .total;
 19         if (std.mem.eql(u8, text, "mean")) return .mean;
 20         if (std.mem.eql(u8, text, "count")) return .count;
 21         if (std.mem.eql(u8, text, "threads")) return .threads;
 22         if (std.mem.eql(u8, text, "name")) return .name;
 23         return null;
 24     }
 25 
 26     fn tag(self: Sort) []const u8 {
 27         return switch (self) {
 28             .self => "self",
 29             .total => "total",
 30             .mean => "mean",
 31             .count => "count",
 32             .threads => "threads",
 33             .name => "name",
 34         };
 35     }
 36 };
 37 
 38 pub const Options = struct {
 39     top: usize = 20,
 40     sort: Sort = .self,
 41     min_total_ns: u64 = 0,
 42     min_self_ns: u64 = 0,
 43     thread: ?u64 = null,
 44 };
 45 
 46 const Row = struct {
 47     name: []u8,
 48     file: ?[]u8 = null,
 49     function: ?[]u8 = null,
 50     line: u32 = 0,
 51     column: u32 = 0,
 52     count: u64 = 0,
 53     total_ns: u64 = 0,
 54     self_ns: u64 = 0,
 55     min_ns: u64 = std.math.maxInt(u64),
 56     max_ns: u64 = 0,
 57     min_self_ns: u64 = std.math.maxInt(u64),
 58     max_self_ns: u64 = 0,
 59     max_depth: usize = 0,
 60     threads: std.ArrayListUnmanaged(u64) = .empty,
 61 
 62     fn deinit(self: *Row, allocator: std.mem.Allocator) void {
 63         allocator.free(self.name);
 64         if (self.file) |file| allocator.free(file);
 65         if (self.function) |function| allocator.free(function);
 66         self.threads.deinit(allocator);
 67         self.* = undefined;
 68     }
 69 
 70     fn meanNs(self: Row) u64 {
 71         if (self.count == 0) return 0;
 72         return self.total_ns / self.count;
 73     }
 74 
 75     fn meanSelfNs(self: Row) u64 {
 76         if (self.count == 0) return 0;
 77         return self.self_ns / self.count;
 78     }
 79 
 80     fn threadCount(self: Row) u64 {
 81         return @intCast(self.threads.items.len);
 82     }
 83 };
 84 
 85 const View = struct {
 86     name: []const u8,
 87     file: ?[]const u8,
 88     function: ?[]const u8,
 89     line: u32,
 90     column: u32,
 91     count: u64,
 92     total_ns: u64,
 93     self_ns: u64,
 94     min_ns: u64,
 95     max_ns: u64,
 96     min_self_ns: u64,
 97     max_self_ns: u64,
 98     mean_ns: u64,
 99     mean_self_ns: u64,
100     max_depth: usize,
101     threads: u64,
102 };
103 
104 const Analyzer = struct {
105     allocator: std.mem.Allocator,
106     rows: std.StringHashMapUnmanaged(Row) = .{},
107     spans: u64 = 0,
108     matched_spans: u64 = 0,
109     open_spans: u64 = 0,
110     superseded_spans: u64 = 0,
111     invalid_duration_spans: u64 = 0,
112     zero_spans: u64 = 0,
113     duration_ns: u64 = 0,
114     evidence: ?tree.Evidence = null,
115 
116     fn init(allocator: std.mem.Allocator) Analyzer {
117         return .{ .allocator = allocator };
118     }
119 
120     fn deinit(self: *Analyzer) void {
121         var iter = self.rows.iterator();
122         while (iter.next()) |entry| {
123             self.allocator.free(entry.key_ptr.*);
124             entry.value_ptr.deinit(self.allocator);
125         }
126         self.rows.deinit(self.allocator);
127         self.* = undefined;
128     }
129 
130     fn ingestTree(self: *Analyzer, trace: *tree.Analyzer, options: Options) !void {
131         self.duration_ns = trace.durationNs();
132         self.evidence = trace.evidence();
133         var spans = try trace.collectSpans(self.allocator);
134         defer spans.deinit(self.allocator);
135         self.spans = @intCast(spans.items.len);
136         for (spans.items) |span| {
137             switch (span.state) {
138                 .open => {
139                     self.open_spans += 1;
140                     continue;
141                 },
142                 .superseded => {
143                     self.superseded_spans += 1;
144                     continue;
145                 },
146                 .complete => {},
147             }
148             if (!span.duration_valid) {
149                 self.invalid_duration_spans += 1;
150                 continue;
151             }
152             if (span.total_ns == 0) {
153                 self.zero_spans += 1;
154                 continue;
155             }
156             if (options.thread) |thread| {
157                 if (span.thread != thread) continue;
158             }
159             try self.record(span);
160         }
161     }
162 
163     fn record(self: *Analyzer, span: tree.Span) !void {
164         var key_writer = std.Io.Writer.Allocating.init(self.allocator);
165         defer key_writer.deinit();
166         try writeKey(&key_writer.writer, span);
167         const key_text = key_writer.written();
168         const entry = try self.rows.getOrPut(self.allocator, key_text);
169         if (!entry.found_existing) {
170             const owned_key = try self.allocator.dupe(u8, key_text);
171             entry.key_ptr.* = owned_key;
172             entry.value_ptr.* = .{
173                 .name = try self.allocator.dupe(u8, span.name),
174                 .file = try dupeOptional(self.allocator, span.file),
175                 .function = try dupeOptional(self.allocator, span.function),
176                 .line = span.line,
177                 .column = span.column,
178             };
179         }
180         const row = entry.value_ptr;
181         row.count += 1;
182         row.total_ns +|= span.total_ns;
183         row.self_ns +|= span.self_ns;
184         row.min_ns = @min(row.min_ns, span.total_ns);
185         row.max_ns = @max(row.max_ns, span.total_ns);
186         row.min_self_ns = @min(row.min_self_ns, span.self_ns);
187         row.max_self_ns = @max(row.max_self_ns, span.self_ns);
188         row.max_depth = @max(row.max_depth, span.depth);
189         try appendThread(self.allocator, &row.threads, span.thread);
190         self.matched_spans += 1;
191     }
192 
193     fn collect(self: *Analyzer, options: Options) !std.ArrayListUnmanaged(View) {
194         var views: std.ArrayListUnmanaged(View) = .empty;
195         var iter = self.rows.valueIterator();
196         while (iter.next()) |row| {
197             if (row.total_ns < options.min_total_ns) continue;
198             if (row.self_ns < options.min_self_ns) continue;
199             try views.append(self.allocator, .{
200                 .name = row.name,
201                 .file = row.file,
202                 .function = row.function,
203                 .line = row.line,
204                 .column = row.column,
205                 .count = row.count,
206                 .total_ns = row.total_ns,
207                 .self_ns = row.self_ns,
208                 .min_ns = if (row.min_ns == std.math.maxInt(u64)) 0 else row.min_ns,
209                 .max_ns = row.max_ns,
210                 .min_self_ns = if (row.min_self_ns == std.math.maxInt(u64)) 0 else row.min_self_ns,
211                 .max_self_ns = row.max_self_ns,
212                 .mean_ns = row.meanNs(),
213                 .mean_self_ns = row.meanSelfNs(),
214                 .max_depth = row.max_depth,
215                 .threads = row.threadCount(),
216             });
217         }
218         sortViews(views.items, options.sort);
219         return views;
220     }
221 };
222 
223 pub fn writeTextFromJsonlPath(
224     allocator: std.mem.Allocator,
225     path: []const u8,
226     writer: *std.Io.Writer,
227     options: Options,
228 ) !void {
229     var trace = tree.Analyzer.init(allocator);
230     defer trace.deinit();
231     try tree.ingestPath(&trace, path);
232     var analyzer = Analyzer.init(allocator);
233     defer analyzer.deinit();
234     try analyzer.ingestTree(&trace, options);
235     try writeText(allocator, &analyzer, writer, options);
236 }
237 
238 pub fn writeJsonlFromJsonlPath(
239     allocator: std.mem.Allocator,
240     path: []const u8,
241     writer: *std.Io.Writer,
242     options: Options,
243 ) !void {
244     var trace = tree.Analyzer.init(allocator);
245     defer trace.deinit();
246     try tree.ingestPath(&trace, path);
247     var analyzer = Analyzer.init(allocator);
248     defer analyzer.deinit();
249     try analyzer.ingestTree(&trace, options);
250     try writeJsonl(allocator, &analyzer, writer, options);
251 }
252 
253 fn writeText(
254     allocator: std.mem.Allocator,
255     analyzer: *Analyzer,
256     writer: *std.Io.Writer,
257     options: Options,
258 ) !void {
259     var views = try analyzer.collect(options);
260     defer views.deinit(allocator);
261     try writer.print(
262         "tracy stats rows={d} spans={d} matched_spans={d} open_spans={d} " ++
263             "superseded_spans={d} invalid_duration_spans={d} zero_spans={d} " ++
264             "duration_ns={d} sort={s}\n",
265         .{
266             views.items.len,
267             analyzer.spans,
268             analyzer.matched_spans,
269             analyzer.open_spans,
270             analyzer.superseded_spans,
271             analyzer.invalid_duration_spans,
272             analyzer.zero_spans,
273             analyzer.duration_ns,
274             options.sort.tag(),
275         },
276     );
277     try analyzer.evidence.?.writeText(writer);
278     const limit = @min(options.top, views.items.len);
279     for (views.items[0..limit]) |view| {
280         try writer.writeAll("stat name=");
281         try pretty_json.writeString(writer, view.name);
282         try writer.print(
283             " count={d} total_ns={d} self_ns={d} mean_ns={d} mean_self_ns={d} min_ns={d} max_ns={d} min_self_ns={d} max_self_ns={d} threads={d} max_depth={d}",
284             .{
285                 view.count,
286                 view.total_ns,
287                 view.self_ns,
288                 view.mean_ns,
289                 view.mean_self_ns,
290                 view.min_ns,
291                 view.max_ns,
292                 view.min_self_ns,
293                 view.max_self_ns,
294                 view.threads,
295                 view.max_depth,
296             },
297         );
298         if (view.file) |file| try writer.print(" file={s}:{d}", .{ file, view.line });
299         if (view.function) |function| {
300             try writer.writeAll(" function=");
301             try pretty_json.writeString(writer, function);
302         }
303         try writer.writeByte('\n');
304     }
305 }
306 
307 fn writeJsonl(
308     allocator: std.mem.Allocator,
309     analyzer: *Analyzer,
310     writer: *std.Io.Writer,
311     options: Options,
312 ) !void {
313     var views = try analyzer.collect(options);
314     defer views.deinit(allocator);
315     var summary_stream = pretty_json.Writer.init(writer, .minified);
316     const summary = try summary_stream.object();
317     try summary.field("schema", schema);
318     try summary.field("kind", "summary");
319     try summary.field("rows", views.items.len);
320     try summary.field("spans", analyzer.spans);
321     try summary.field("matched_spans", analyzer.matched_spans);
322     try summary.field("open_spans", analyzer.open_spans);
323     try summary.field("superseded_spans", analyzer.superseded_spans);
324     try summary.field("invalid_duration_spans", analyzer.invalid_duration_spans);
325     try summary.field("zero_spans", analyzer.zero_spans);
326     try summary.field("duration_ns", analyzer.duration_ns);
327     try summary.field("sort", options.sort.tag());
328     try analyzer.evidence.?.writeFields(summary);
329     try summary.endLine();
330     const limit = @min(options.top, views.items.len);
331     for (views.items[0..limit]) |view| {
332         var stream = pretty_json.Writer.init(writer, .minified);
333         const object = try stream.object();
334         try object.field("schema", schema);
335         try object.field("kind", "stat");
336         try object.field("name", view.name);
337         try object.field("count", view.count);
338         try object.field("total_ns", view.total_ns);
339         try object.field("self_ns", view.self_ns);
340         try object.field("mean_ns", view.mean_ns);
341         try object.field("mean_self_ns", view.mean_self_ns);
342         try object.field("min_ns", view.min_ns);
343         try object.field("max_ns", view.max_ns);
344         try object.field("min_self_ns", view.min_self_ns);
345         try object.field("max_self_ns", view.max_self_ns);
346         try object.field("threads", view.threads);
347         try object.field("max_depth", view.max_depth);
348         if (view.file) |file| {
349             try object.field("file", file);
350             try object.field("line", view.line);
351         }
352         if (view.function) |function| try object.field("function", function);
353         try object.endLine();
354     }
355 }
356 
357 fn writeKey(writer: *std.Io.Writer, span: tree.Span) !void {
358     try writer.writeAll(span.name);
359     try writer.writeByte('\x1f');
360     if (span.file) |file| try writer.writeAll(file);
361     try writer.writeByte('\x1f');
362     if (span.function) |function| try writer.writeAll(function);
363     try writer.writeByte('\x1f');
364     try writer.print("{d}:{d}", .{ span.line, span.column });
365 }
366 
367 fn appendThread(allocator: std.mem.Allocator, threads: *std.ArrayListUnmanaged(u64), thread: u64) !void {
368     for (threads.items) |existing| {
369         if (existing == thread) return;
370     }
371     try threads.append(allocator, thread);
372 }
373 
374 fn sortViews(items: []View, sort: Sort) void {
375     switch (sort) {
376         .self => std.mem.sort(View, items, {}, viewSelfGreaterThan),
377         .total => std.mem.sort(View, items, {}, viewTotalGreaterThan),
378         .mean => std.mem.sort(View, items, {}, viewMeanGreaterThan),
379         .count => std.mem.sort(View, items, {}, viewCountGreaterThan),
380         .threads => std.mem.sort(View, items, {}, viewThreadsGreaterThan),
381         .name => std.mem.sort(View, items, {}, viewNameLessThan),
382     }
383 }
384 
385 fn viewSelfGreaterThan(_: void, left: View, right: View) bool {
386     if (left.self_ns != right.self_ns) return left.self_ns > right.self_ns;
387     return viewTotalGreaterThan({}, left, right);
388 }
389 
390 fn viewTotalGreaterThan(_: void, left: View, right: View) bool {
391     if (left.total_ns != right.total_ns) return left.total_ns > right.total_ns;
392     return viewNameLessThan({}, left, right);
393 }
394 
395 fn viewMeanGreaterThan(_: void, left: View, right: View) bool {
396     if (left.mean_ns != right.mean_ns) return left.mean_ns > right.mean_ns;
397     return viewTotalGreaterThan({}, left, right);
398 }
399 
400 fn viewCountGreaterThan(_: void, left: View, right: View) bool {
401     if (left.count != right.count) return left.count > right.count;
402     return viewTotalGreaterThan({}, left, right);
403 }
404 
405 fn viewThreadsGreaterThan(_: void, left: View, right: View) bool {
406     if (left.threads != right.threads) return left.threads > right.threads;
407     return viewTotalGreaterThan({}, left, right);
408 }
409 
410 fn viewNameLessThan(_: void, left: View, right: View) bool {
411     const name_cmp = std.mem.order(u8, left.name, right.name);
412     if (name_cmp != .eq) return name_cmp == .lt;
413     const left_file = left.file orelse "";
414     const right_file = right.file orelse "";
415     const file_cmp = std.mem.order(u8, left_file, right_file);
416     if (file_cmp != .eq) return file_cmp == .lt;
417     return left.line < right.line;
418 }
419 
420 fn dupeOptional(allocator: std.mem.Allocator, text: ?[]const u8) !?[]u8 {
421     const actual = text orelse return null;
422     return try allocator.dupe(u8, actual);
423 }
424 
425 test "stats aggregates inclusive and self time by source location" {
426     var trace_bytes = std.Io.Writer.Allocating.init(std.testing.allocator);
427     defer trace_bytes.deinit();
428     try (event.TraceEvent{ .seq = 1, .kind = .zone_begin, .time_ns = 100, .thread = 1, .id = 1, .name = "root", .file = "root.zig", .line = 1 }).writeJsonLine(&trace_bytes.writer);
429     try (event.TraceEvent{ .seq = 2, .kind = .zone_begin, .time_ns = 120, .thread = 1, .id = 2, .name = "child", .file = "child.zig", .line = 2 }).writeJsonLine(&trace_bytes.writer);
430     try (event.TraceEvent{ .seq = 3, .kind = .zone_end, .time_ns = 150, .thread = 1, .id = 2 }).writeJsonLine(&trace_bytes.writer);
431     try (event.TraceEvent{ .seq = 4, .kind = .zone_end, .time_ns = 200, .thread = 1, .id = 1 }).writeJsonLine(&trace_bytes.writer);
432     try (event.TraceEvent{ .seq = 5, .kind = .zone_begin, .time_ns = 210, .thread = 2, .id = 3, .name = "child", .file = "child.zig", .line = 2 }).writeJsonLine(&trace_bytes.writer);
433     try (event.TraceEvent{ .seq = 6, .kind = .zone_end, .time_ns = 260, .thread = 2, .id = 3 }).writeJsonLine(&trace_bytes.writer);
434 
435     var trace = tree.Analyzer.init(std.testing.allocator);
436     defer trace.deinit();
437     try trace.ingestJsonlBytes(trace_bytes.written());
438 
439     var analyzer = Analyzer.init(std.testing.allocator);
440     defer analyzer.deinit();
441     try analyzer.ingestTree(&trace, .{});
442 
443     var out = std.Io.Writer.Allocating.init(std.testing.allocator);
444     defer out.deinit();
445     try writeText(std.testing.allocator, &analyzer, &out.writer, .{ .top = 4, .sort = .self });
446     const text = out.written();
447     try std.testing.expect(std.mem.indexOf(u8, text, "tracy stats rows=2 spans=3 matched_spans=3") != null);
448     try std.testing.expect(std.mem.indexOf(u8, text, "stat name=\"child\" count=2 total_ns=80 self_ns=80 mean_ns=40 mean_self_ns=40") != null);
449     try std.testing.expect(std.mem.indexOf(u8, text, "threads=2") != null);
450     try std.testing.expect(std.mem.indexOf(u8, text, "stat name=\"root\" count=1 total_ns=100 self_ns=70") != null);
451 }
452 
453 test "stats jsonl emits filtered machine rows" {
454     var trace_bytes = std.Io.Writer.Allocating.init(std.testing.allocator);
455     defer trace_bytes.deinit();
456     try (event.TraceEvent{ .seq = 1, .kind = .zone_begin, .time_ns = 100, .thread = 7, .id = 1, .name = "phase", .file = "phase.zig", .line = 9 }).writeJsonLine(&trace_bytes.writer);
457     try (event.TraceEvent{ .seq = 2, .kind = .zone_end, .time_ns = 150, .thread = 7, .id = 1 }).writeJsonLine(&trace_bytes.writer);
458 
459     var trace = tree.Analyzer.init(std.testing.allocator);
460     defer trace.deinit();
461     try trace.ingestJsonlBytes(trace_bytes.written());
462 
463     var analyzer = Analyzer.init(std.testing.allocator);
464     defer analyzer.deinit();
465     try analyzer.ingestTree(&trace, .{ .thread = 7 });
466 
467     var out = std.Io.Writer.Allocating.init(std.testing.allocator);
468     defer out.deinit();
469     try writeJsonl(std.testing.allocator, &analyzer, &out.writer, .{ .top = 2, .sort = .total, .min_total_ns = 10 });
470     const text = out.written();
471     try std.testing.expect(std.mem.indexOf(u8, text, "\"schema\":\"tracy.stats/v0\"") != null);
472     try std.testing.expect(std.mem.indexOf(u8, text, "\"kind\":\"stat\"") != null);
473     try std.testing.expect(std.mem.indexOf(u8, text, "\"name\":\"phase\"") != null);
474     try std.testing.expect(std.mem.indexOf(u8, text, "\"total_ns\":50") != null);
475     try std.testing.expect(std.mem.indexOf(u8, text, "\"threads\":1") != null);
476 }