lib/choir/src/profiling/versus/core/jsonl.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const pretty_json = @import("pretty").json;
3
4 pub const schema = "choir.versus/v2";
5
6 pub const Unit = enum {
7 ns,
8 count,
9 bytes,
10
11 pub fn name(self: Unit) []const u8 {
12 return @tagName(self);
13 }
14 };
15
16 pub const Metric = enum {
17 kernel_ns,
18 compile_ns,
19 compile_ir_ns,
20 compile_pass_ns,
21 compile_jit_ns,
22 compile_alloc_count,
23 compile_alloc_bytes,
24 compile_ir_ops,
25 compile_ir_blocks,
26 compile_ir_values,
27 compile_pass_count,
28 code_bytes,
29 inst_count,
30
31 pub fn name(self: Metric) []const u8 {
32 return switch (self) {
33 .kernel_ns => "kernel_ns",
34 .compile_ns => "compile_ns",
35 .compile_ir_ns => "compile_ir_ns",
36 .compile_pass_ns => "compile_pass_ns",
37 .compile_jit_ns => "compile_jit_ns",
38 .compile_alloc_count => "compile_alloc_count",
39 .compile_alloc_bytes => "compile_alloc_bytes",
40 .compile_ir_ops => "compile_ir_ops",
41 .compile_ir_blocks => "compile_ir_blocks",
42 .compile_ir_values => "compile_ir_values",
43 .compile_pass_count => "compile_pass_count",
44 .code_bytes => "code_bytes",
45 .inst_count => "inst_count",
46 };
47 }
48
49 pub fn unit(self: Metric) Unit {
50 return switch (self) {
51 .kernel_ns,
52 .compile_ns,
53 .compile_ir_ns,
54 .compile_pass_ns,
55 .compile_jit_ns,
56 => .ns,
57 .compile_alloc_bytes,
58 .code_bytes,
59 => .bytes,
60 .compile_alloc_count,
61 .compile_ir_ops,
62 .compile_ir_blocks,
63 .compile_ir_values,
64 .compile_pass_count,
65 .inst_count,
66 => .count,
67 };
68 }
69
70 pub fn valueField(self: Metric) ?[]const u8 {
71 return switch (self) {
72 .compile_ir_ns => "compile_ir_ns",
73 .compile_pass_ns => "compile_pass_ns",
74 .compile_jit_ns => "compile_jit_ns",
75 .compile_alloc_count => "compile_alloc_count",
76 .compile_alloc_bytes => "compile_alloc_bytes",
77 .compile_ir_ops => "compile_ir_ops",
78 .compile_ir_blocks => "compile_ir_blocks",
79 .compile_ir_values => "compile_ir_values",
80 .compile_pass_count => "compile_pass_count",
81 .code_bytes => "code_bytes",
82 .inst_count => "inst_count",
83 .kernel_ns,
84 .compile_ns,
85 => null,
86 };
87 }
88
89 pub fn p10Field(self: Metric) ?[]const u8 {
90 return switch (self) {
91 .compile_ir_ns => "compile_ir_p10_ns",
92 .compile_pass_ns => "compile_pass_p10_ns",
93 .compile_jit_ns => "compile_jit_p10_ns",
94 .compile_alloc_count => "compile_alloc_count_p10",
95 .compile_alloc_bytes => "compile_alloc_bytes_p10",
96 else => null,
97 };
98 }
99
100 pub fn p90Field(self: Metric) ?[]const u8 {
101 return switch (self) {
102 .compile_ir_ns => "compile_ir_p90_ns",
103 .compile_pass_ns => "compile_pass_p90_ns",
104 .compile_jit_ns => "compile_jit_p90_ns",
105 .compile_alloc_count => "compile_alloc_count_p90",
106 .compile_alloc_bytes => "compile_alloc_bytes_p90",
107 else => null,
108 };
109 }
110
111 pub fn parse(text: []const u8) ?Metric {
112 inline for (std.meta.tags(Metric)) |value| {
113 if (std.mem.eql(u8, text, value.name())) return value;
114 }
115 return null;
116 }
117 };
118
119 pub const Row = struct {
120 system: []const u8,
121 workload: []const u8,
122 metric: Metric,
123 samples: u32,
124 median_ns: u64,
125 p10_ns: u64,
126 p90_ns: u64,
127 flops: u64,
128 moved_bytes: u64,
129 instructions: u64 = 0,
130 cycles: u64 = 0,
131 code_bytes: u64 = 0,
132 inst_count: u64 = 0,
133 mca_rthroughput: f64 = 0,
134 compile_ir_ns: u64 = 0,
135 compile_ir_p10_ns: u64 = 0,
136 compile_ir_p90_ns: u64 = 0,
137 compile_pass_ns: u64 = 0,
138 compile_pass_p10_ns: u64 = 0,
139 compile_pass_p90_ns: u64 = 0,
140 compile_jit_ns: u64 = 0,
141 compile_jit_p10_ns: u64 = 0,
142 compile_jit_p90_ns: u64 = 0,
143 compile_alloc_count: u64 = 0,
144 compile_alloc_count_p10: u64 = 0,
145 compile_alloc_count_p90: u64 = 0,
146 compile_alloc_bytes: u64 = 0,
147 compile_alloc_bytes_p10: u64 = 0,
148 compile_alloc_bytes_p90: u64 = 0,
149 compile_ir_ops: u64 = 0,
150 compile_ir_blocks: u64 = 0,
151 compile_ir_values: u64 = 0,
152 compile_pass_count: u64 = 0,
153 note: ?[]const u8 = null,
154 };
155
156 pub fn writeRow(writer: *std.Io.Writer, row: Row) !void {
157 var stream = pretty_json.Writer.init(writer, .minified);
158 const object = try stream.object();
159 try object.field("schema", schema);
160 try object.field("system", row.system);
161 try object.field("workload", row.workload);
162 try object.field("metric", row.metric.name());
163 try object.field("unit", row.metric.unit().name());
164 try object.field("samples", row.samples);
165 try object.field("median_ns", row.median_ns);
166 try object.field("p10_ns", row.p10_ns);
167 try object.field("p90_ns", row.p90_ns);
168 try object.field("flops", row.flops);
169 try object.field("moved_bytes", row.moved_bytes);
170 try object.field("instructions", row.instructions);
171 try object.field("cycles", row.cycles);
172 try object.field("code_bytes", row.code_bytes);
173 try object.field("inst_count", row.inst_count);
174 try writeCompileFields(object, row);
175 if (row.mca_rthroughput > 0) {
176 try object.print("mca_rthroughput", "{d:.2}", .{row.mca_rthroughput});
177 }
178 if (row.note) |note| try object.field("note", note);
179 try object.endLine();
180 }
181
182 pub fn writeMeta(writer: *std.Io.Writer, system: []const u8, host: []const u8, detail: []const u8) !void {
183 var stream = pretty_json.Writer.init(writer, .minified);
184 const object = try stream.object();
185 try object.field("schema", schema);
186 try object.field("system", system);
187 try object.field("meta", true);
188 try object.field("host", host);
189 try object.field("detail", detail);
190 try object.endLine();
191 }
192
193 pub const CompileTrace = struct {
194 system: []const u8,
195 workload: []const u8,
196 sample: usize,
197 total_ns: u64,
198 pass_ordinal: u64,
199 pass_name: []const u8,
200 pass_ns: u64,
201 modified: bool,
202 op_count_before: u64 = 0,
203 op_count_after: u64 = 0,
204 op_count_delta: i128 = 0,
205 alloc_count: u64 = 0,
206 free_count: u64 = 0,
207 alloc_bytes: u64 = 0,
208 };
209
210 pub fn writeCompileTrace(writer: *std.Io.Writer, trace: CompileTrace) !void {
211 var stream = pretty_json.Writer.init(writer, .minified);
212 const object = try stream.object();
213 try object.field("schema", schema);
214 try object.field("event", "compile_trace");
215 try object.field("system", trace.system);
216 try object.field("workload", trace.workload);
217 try object.field("sample", trace.sample);
218 try object.field("total_ns", trace.total_ns);
219 try object.field("pass_ordinal", trace.pass_ordinal);
220 try object.field("pass_name", trace.pass_name);
221 try object.field("pass_ns", trace.pass_ns);
222 try object.field("modified", trace.modified);
223 try object.field("op_count_before", trace.op_count_before);
224 try object.field("op_count_after", trace.op_count_after);
225 try object.field("op_count_delta", trace.op_count_delta);
226 try object.field("alloc_count", trace.alloc_count);
227 try object.field("free_count", trace.free_count);
228 try object.field("alloc_bytes", trace.alloc_bytes);
229 try object.endLine();
230 }
231
232 pub fn writeProfilingMetrics(writer: *std.Io.Writer, row: Row) !void {
233 if (row.metric.unit() == .ns) {
234 try writeProfilingTiming(writer, row, row.metric, row.median_ns, row.p10_ns, row.p90_ns);
235 }
236 if (row.metric != .compile_ns) return;
237
238 inline for (std.meta.tags(Metric)) |metric| {
239 if (metric.valueField()) |value_field| {
240 const value = valueFromRow(row, value_field);
241 if (value != 0) {
242 switch (metric.unit()) {
243 .ns => try writeProfilingTiming(
244 writer,
245 row,
246 metric,
247 value,
248 if (metric.p10Field()) |p10_field| valueFromRow(row, p10_field) else value,
249 if (metric.p90Field()) |p90_field| valueFromRow(row, p90_field) else value,
250 ),
251 .bytes, .count => {
252 if (metric == .compile_alloc_bytes or metric == .compile_alloc_count) {
253 try writeProfilingMemory(writer, row, metric, value);
254 }
255 },
256 }
257 }
258 }
259 }
260 }
261
262 fn writeProfilingTiming(writer: *std.Io.Writer, row: Row, metric: Metric, median: u64, p10: u64, p90: u64) !void {
263 var stream = pretty_json.Writer.init(writer, .minified);
264 const object = try stream.object();
265 try object.field("schema", "tiny.profiling.metric/v1");
266 try object.field("family", "timing");
267 try object.field("metric", metric.name());
268 try object.field("unit", "ns");
269 try object.field("workload", row.workload);
270 try object.field("system", row.system);
271 try object.stringParts("name", &.{ row.system, "/", row.workload, "/", metric.name() });
272 try object.field("sample_count", row.samples);
273 try object.field("median_ns", median);
274 try object.field("p10_ns", p10);
275 try object.field("p90_ns", p90);
276 try object.endLine();
277 }
278
279 fn writeProfilingMemory(writer: *std.Io.Writer, row: Row, metric: Metric, value: u64) !void {
280 var stream = pretty_json.Writer.init(writer, .minified);
281 const object = try stream.object();
282 try object.field("schema", "tiny.profiling.metric/v1");
283 try object.field("family", "memory");
284 try object.field("metric", metric.name());
285 try object.field("unit", metric.unit().name());
286 try object.field("workload", row.workload);
287 try object.field("system", row.system);
288 try object.stringParts("name", &.{ row.system, "/", row.workload, "/", metric.name() });
289 try object.field("sample_count", row.samples);
290 try object.field("value", value);
291 try object.endLine();
292 }
293
294 fn writeCompileFields(object: pretty_json.Object, row: Row) !void {
295 try object.field("compile_ir_ns", row.compile_ir_ns);
296 try object.field("compile_ir_p10_ns", row.compile_ir_p10_ns);
297 try object.field("compile_ir_p90_ns", row.compile_ir_p90_ns);
298 try object.field("compile_pass_ns", row.compile_pass_ns);
299 try object.field("compile_pass_p10_ns", row.compile_pass_p10_ns);
300 try object.field("compile_pass_p90_ns", row.compile_pass_p90_ns);
301 try object.field("compile_jit_ns", row.compile_jit_ns);
302 try object.field("compile_jit_p10_ns", row.compile_jit_p10_ns);
303 try object.field("compile_jit_p90_ns", row.compile_jit_p90_ns);
304 try object.field("compile_alloc_count", row.compile_alloc_count);
305 try object.field("compile_alloc_count_p10", row.compile_alloc_count_p10);
306 try object.field("compile_alloc_count_p90", row.compile_alloc_count_p90);
307 try object.field("compile_alloc_bytes", row.compile_alloc_bytes);
308 try object.field("compile_alloc_bytes_p10", row.compile_alloc_bytes_p10);
309 try object.field("compile_alloc_bytes_p90", row.compile_alloc_bytes_p90);
310 try object.field("compile_ir_ops", row.compile_ir_ops);
311 try object.field("compile_ir_blocks", row.compile_ir_blocks);
312 try object.field("compile_ir_values", row.compile_ir_values);
313 try object.field("compile_pass_count", row.compile_pass_count);
314 }
315
316 fn valueFromRow(row: Row, field: []const u8) u64 {
317 if (std.mem.eql(u8, field, "compile_ir_ns")) return row.compile_ir_ns;
318 if (std.mem.eql(u8, field, "compile_ir_p10_ns")) return row.compile_ir_p10_ns;
319 if (std.mem.eql(u8, field, "compile_ir_p90_ns")) return row.compile_ir_p90_ns;
320 if (std.mem.eql(u8, field, "compile_pass_ns")) return row.compile_pass_ns;
321 if (std.mem.eql(u8, field, "compile_pass_p10_ns")) return row.compile_pass_p10_ns;
322 if (std.mem.eql(u8, field, "compile_pass_p90_ns")) return row.compile_pass_p90_ns;
323 if (std.mem.eql(u8, field, "compile_jit_ns")) return row.compile_jit_ns;
324 if (std.mem.eql(u8, field, "compile_jit_p10_ns")) return row.compile_jit_p10_ns;
325 if (std.mem.eql(u8, field, "compile_jit_p90_ns")) return row.compile_jit_p90_ns;
326 if (std.mem.eql(u8, field, "compile_alloc_count")) return row.compile_alloc_count;
327 if (std.mem.eql(u8, field, "compile_alloc_count_p10")) return row.compile_alloc_count_p10;
328 if (std.mem.eql(u8, field, "compile_alloc_count_p90")) return row.compile_alloc_count_p90;
329 if (std.mem.eql(u8, field, "compile_alloc_bytes")) return row.compile_alloc_bytes;
330 if (std.mem.eql(u8, field, "compile_alloc_bytes_p10")) return row.compile_alloc_bytes_p10;
331 if (std.mem.eql(u8, field, "compile_alloc_bytes_p90")) return row.compile_alloc_bytes_p90;
332 if (std.mem.eql(u8, field, "compile_ir_ops")) return row.compile_ir_ops;
333 if (std.mem.eql(u8, field, "compile_ir_blocks")) return row.compile_ir_blocks;
334 if (std.mem.eql(u8, field, "compile_ir_values")) return row.compile_ir_values;
335 if (std.mem.eql(u8, field, "compile_pass_count")) return row.compile_pass_count;
336 if (std.mem.eql(u8, field, "code_bytes")) return row.code_bytes;
337 if (std.mem.eql(u8, field, "inst_count")) return row.inst_count;
338 return 0;
339 }
340
341 pub const Samples = struct {
342 sorted: []u64,
343
344 pub fn init(values: []u64) Samples {
345 std.mem.sort(u64, values, {}, std.sort.asc(u64));
346 return .{ .sorted = values };
347 }
348
349 pub fn median(self: Samples) u64 {
350 return self.sorted[self.sorted.len / 2];
351 }
352
353 pub fn p10(self: Samples) u64 {
354 return self.sorted[self.sorted.len / 10];
355 }
356
357 pub fn p90(self: Samples) u64 {
358 return self.sorted[(self.sorted.len * 9) / 10];
359 }
360 };
361
362 test "metric names round-trip" {
363 inline for (std.meta.tags(Metric)) |value| {
364 try std.testing.expectEqual(value, Metric.parse(value.name()).?);
365 }
366 try std.testing.expect(Metric.parse("bogus") == null);
367 }
368
369 test "samples report order statistics" {
370 var values = [_]u64{ 5, 1, 9, 3, 7, 2, 8, 4, 6, 10 };
371 const samples = Samples.init(values[0..]);
372 try std.testing.expectEqual(@as(u64, 6), samples.median());
373 try std.testing.expectEqual(@as(u64, 2), samples.p10());
374 try std.testing.expectEqual(@as(u64, 10), samples.p90());
375 }
376
377 test "writeRow emits one parsable json line" {
378 var buffer: [4096]u8 = undefined;
379 var writer = std.Io.Writer.fixed(buffer[0..]);
380 try writeRow(&writer, .{
381 .system = "choir",
382 .workload = "saxpy_f32_1m",
383 .metric = .kernel_ns,
384 .samples = 20,
385 .median_ns = 100,
386 .p10_ns = 90,
387 .p90_ns = 110,
388 .flops = 2097152,
389 .moved_bytes = 12582912,
390 .instructions = 5000,
391 .cycles = 4000,
392 .code_bytes = 96,
393 .inst_count = 24,
394 .compile_ir_ns = 10,
395 .compile_ir_p10_ns = 9,
396 .compile_ir_p90_ns = 11,
397 .compile_pass_ns = 20,
398 .compile_pass_p10_ns = 18,
399 .compile_pass_p90_ns = 22,
400 .compile_jit_ns = 70,
401 .compile_jit_p10_ns = 66,
402 .compile_jit_p90_ns = 77,
403 .compile_alloc_count = 12,
404 .compile_alloc_bytes = 4096,
405 .compile_ir_ops = 30,
406 .compile_ir_blocks = 7,
407 .compile_ir_values = 36,
408 .compile_pass_count = 11,
409 .note = "pipeline=\"choir\"\ncleanup",
410 });
411 const written = writer.buffered();
412 try std.testing.expect(std.mem.endsWith(u8, written, "}\n"));
413
414 const parsed = try std.json.parseFromSlice(std.json.Value, std.testing.allocator, written[0 .. written.len - 1], .{});
415 defer parsed.deinit();
416 try std.testing.expectEqualStrings("choir", parsed.value.object.get("system").?.string);
417 try std.testing.expectEqual(@as(i64, 24), parsed.value.object.get("inst_count").?.integer);
418 try std.testing.expectEqual(@as(i64, 10), parsed.value.object.get("compile_ir_ns").?.integer);
419 try std.testing.expectEqual(@as(i64, 4096), parsed.value.object.get("compile_alloc_bytes").?.integer);
420 try std.testing.expectEqualStrings(
421 "pipeline=\"choir\"\ncleanup",
422 parsed.value.object.get("note").?.string,
423 );
424 }
425
426 test "writeProfilingMetrics emits strict profile metric rows" {
427 var buffer: [8192]u8 = undefined;
428 var writer = std.Io.Writer.fixed(buffer[0..]);
429 try writeProfilingMetrics(&writer, .{
430 .system = "choir",
431 .workload = "compile_arith_1x32",
432 .metric = .compile_ns,
433 .samples = 5,
434 .median_ns = 1000,
435 .p10_ns = 900,
436 .p90_ns = 1100,
437 .flops = 0,
438 .moved_bytes = 0,
439 .compile_ir_ns = 100,
440 .compile_ir_p10_ns = 90,
441 .compile_ir_p90_ns = 110,
442 .compile_pass_ns = 200,
443 .compile_pass_p10_ns = 180,
444 .compile_pass_p90_ns = 220,
445 .compile_jit_ns = 700,
446 .compile_jit_p10_ns = 690,
447 .compile_jit_p90_ns = 710,
448 .compile_alloc_count = 12,
449 .compile_alloc_bytes = 4096,
450 });
451 const written = writer.buffered();
452 try std.testing.expect(std.mem.indexOf(u8, written, "\"schema\":\"tiny.profiling.metric/v1\"") != null);
453 try std.testing.expect(std.mem.indexOf(u8, written, "\"metric\":\"compile_ir_ns\"") != null);
454 try std.testing.expect(std.mem.indexOf(u8, written, "\"family\":\"memory\"") != null);
455 try std.testing.expect(std.mem.indexOf(u8, written, "\"metric\":\"compile_alloc_bytes\"") != null);
456 }