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 }