lib/tracy/src/event.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const std = @import("std");
  2 const pretty_json = @import("pretty").json;
  3 
  4 pub const schema = "tracy.event/v0";
  5 pub const format_version: u32 = 0;
  6 
  7 pub const Kind = enum {
  8     start,
  9     stop,
 10     zone_begin,
 11     zone_end,
 12     zone_text,
 13     zone_name,
 14     zone_color,
 15     zone_value,
 16     frame,
 17     message,
 18     app_info,
 19     plot_config,
 20     plot,
 21     alloc,
 22     free,
 23     thread_name,
 24     fiber_enter,
 25     fiber_leave,
 26     fiber_name,
 27     gpu_context,
 28     gpu_context_name,
 29     gpu_zone_begin,
 30     gpu_zone_end,
 31     gpu_time,
 32     gpu_calibration,
 33     gpu_time_sync,
 34     gpu_annotation,
 35     gpu_annotation_name,
 36     thread_context,
 37     thread_pid,
 38     cpu_topology,
 39     sys_time,
 40     sys_power,
 41     hw_sample,
 42     sample,
 43     sample_frame,
 44     context_switch,
 45     thread_wakeup,
 46     lock_announce,
 47     lock_terminate,
 48     lock_wait,
 49     lock_obtain,
 50     lock_release,
 51     lock_shared_wait,
 52     lock_shared_obtain,
 53     lock_shared_release,
 54     lock_mark,
 55     lock_name,
 56 
 57     pub fn tag(self: Kind) []const u8 {
 58         return switch (self) {
 59             .start => "start",
 60             .stop => "stop",
 61             .zone_begin => "zone.begin",
 62             .zone_end => "zone.end",
 63             .zone_text => "zone.text",
 64             .zone_name => "zone.name",
 65             .zone_color => "zone.color",
 66             .zone_value => "zone.value",
 67             .frame => "frame",
 68             .message => "message",
 69             .app_info => "app.info",
 70             .plot_config => "plot.config",
 71             .plot => "plot",
 72             .alloc => "alloc",
 73             .free => "free",
 74             .thread_name => "thread.name",
 75             .fiber_enter => "fiber.enter",
 76             .fiber_leave => "fiber.leave",
 77             .fiber_name => "fiber.name",
 78             .gpu_context => "gpu.context",
 79             .gpu_context_name => "gpu.context.name",
 80             .gpu_zone_begin => "gpu.zone.begin",
 81             .gpu_zone_end => "gpu.zone.end",
 82             .gpu_time => "gpu.time",
 83             .gpu_calibration => "gpu.calibration",
 84             .gpu_time_sync => "gpu.time_sync",
 85             .gpu_annotation => "gpu.annotation",
 86             .gpu_annotation_name => "gpu.annotation.name",
 87             .thread_context => "thread.context",
 88             .thread_pid => "thread.pid",
 89             .cpu_topology => "cpu.topology",
 90             .sys_time => "sys.time",
 91             .sys_power => "sys.power",
 92             .hw_sample => "hw.sample",
 93             .sample => "sample",
 94             .sample_frame => "sample.frame",
 95             .context_switch => "context.switch",
 96             .thread_wakeup => "thread.wakeup",
 97             .lock_announce => "lock.announce",
 98             .lock_terminate => "lock.terminate",
 99             .lock_wait => "lock.wait",
100             .lock_obtain => "lock.obtain",
101             .lock_release => "lock.release",
102             .lock_shared_wait => "lock.shared.wait",
103             .lock_shared_obtain => "lock.shared.obtain",
104             .lock_shared_release => "lock.shared.release",
105             .lock_mark => "lock.mark",
106             .lock_name => "lock.name",
107         };
108     }
109 
110     pub fn fromTag(text: []const u8) ?Kind {
111         if (std.mem.eql(u8, text, "start")) return .start;
112         if (std.mem.eql(u8, text, "stop")) return .stop;
113         if (std.mem.eql(u8, text, "zone.begin")) return .zone_begin;
114         if (std.mem.eql(u8, text, "zone.end")) return .zone_end;
115         if (std.mem.eql(u8, text, "zone.text")) return .zone_text;
116         if (std.mem.eql(u8, text, "zone.name")) return .zone_name;
117         if (std.mem.eql(u8, text, "zone.color")) return .zone_color;
118         if (std.mem.eql(u8, text, "zone.value")) return .zone_value;
119         if (std.mem.eql(u8, text, "frame")) return .frame;
120         if (std.mem.eql(u8, text, "message")) return .message;
121         if (std.mem.eql(u8, text, "app.info")) return .app_info;
122         if (std.mem.eql(u8, text, "plot.config")) return .plot_config;
123         if (std.mem.eql(u8, text, "plot")) return .plot;
124         if (std.mem.eql(u8, text, "alloc")) return .alloc;
125         if (std.mem.eql(u8, text, "free")) return .free;
126         if (std.mem.eql(u8, text, "thread.name")) return .thread_name;
127         if (std.mem.eql(u8, text, "fiber.enter")) return .fiber_enter;
128         if (std.mem.eql(u8, text, "fiber.leave")) return .fiber_leave;
129         if (std.mem.eql(u8, text, "fiber.name")) return .fiber_name;
130         if (std.mem.eql(u8, text, "gpu.context")) return .gpu_context;
131         if (std.mem.eql(u8, text, "gpu.context.name")) return .gpu_context_name;
132         if (std.mem.eql(u8, text, "gpu.zone.begin")) return .gpu_zone_begin;
133         if (std.mem.eql(u8, text, "gpu.zone.end")) return .gpu_zone_end;
134         if (std.mem.eql(u8, text, "gpu.time")) return .gpu_time;
135         if (std.mem.eql(u8, text, "gpu.calibration")) return .gpu_calibration;
136         if (std.mem.eql(u8, text, "gpu.time_sync")) return .gpu_time_sync;
137         if (std.mem.eql(u8, text, "gpu.annotation")) return .gpu_annotation;
138         if (std.mem.eql(u8, text, "gpu.annotation.name")) return .gpu_annotation_name;
139         if (std.mem.eql(u8, text, "thread.context")) return .thread_context;
140         if (std.mem.eql(u8, text, "thread.pid")) return .thread_pid;
141         if (std.mem.eql(u8, text, "cpu.topology")) return .cpu_topology;
142         if (std.mem.eql(u8, text, "sys.time")) return .sys_time;
143         if (std.mem.eql(u8, text, "sys.power")) return .sys_power;
144         if (std.mem.eql(u8, text, "hw.sample")) return .hw_sample;
145         if (std.mem.eql(u8, text, "sample")) return .sample;
146         if (std.mem.eql(u8, text, "sample.frame")) return .sample_frame;
147         if (std.mem.eql(u8, text, "context.switch")) return .context_switch;
148         if (std.mem.eql(u8, text, "thread.wakeup")) return .thread_wakeup;
149         if (std.mem.eql(u8, text, "lock.announce")) return .lock_announce;
150         if (std.mem.eql(u8, text, "lock.terminate")) return .lock_terminate;
151         if (std.mem.eql(u8, text, "lock.wait")) return .lock_wait;
152         if (std.mem.eql(u8, text, "lock.obtain")) return .lock_obtain;
153         if (std.mem.eql(u8, text, "lock.release")) return .lock_release;
154         if (std.mem.eql(u8, text, "lock.shared.wait")) return .lock_shared_wait;
155         if (std.mem.eql(u8, text, "lock.shared.obtain")) return .lock_shared_obtain;
156         if (std.mem.eql(u8, text, "lock.shared.release")) return .lock_shared_release;
157         if (std.mem.eql(u8, text, "lock.mark")) return .lock_mark;
158         if (std.mem.eql(u8, text, "lock.name")) return .lock_name;
159         return null;
160     }
161 };
162 
163 pub const TraceEvent = struct {
164     seq: u64 = 0,
165     kind: Kind,
166     time_ns: u64 = 0,
167     thread: u64 = 0,
168     id: u64 = 0,
169     name: ?[]const u8 = null,
170     text: ?[]const u8 = null,
171     function: ?[]const u8 = null,
172     file: ?[]const u8 = null,
173     line: u32 = 0,
174     column: u32 = 0,
175     color: ?u32 = null,
176     value_u64: ?u64 = null,
177     value_i64: ?i64 = null,
178     value_f64: ?f64 = null,
179     address: u64 = 0,
180     size: u64 = 0,
181     depth: u32 = 0,
182     cpu: ?u32 = null,
183     prev_thread: u64 = 0,
184     next_thread: u64 = 0,
185     target_thread: u64 = 0,
186     pid: u64 = 0,
187     previous_cstate: ?u32 = null,
188     prev_priority: ?i64 = null,
189     next_priority: ?i64 = null,
190     group_hint: ?i64 = null,
191     gpu_context: ?u32 = null,
192     gpu_query: ?u32 = null,
193     gpu_time: ?i64 = null,
194     gpu_period: ?f64 = null,
195     gpu_cpu_delta_ns: ?i64 = null,
196     gpu_context_type: ?[]const u8 = null,
197     gpu_has_calibration: bool = false,
198     gpu_serial: bool = false,
199     gpu_annotation_id: ?i64 = null,
200     cpu_package: ?u32 = null,
201     cpu_die: ?u32 = null,
202     cpu_core: ?u32 = null,
203     power_delta_uj: ?u64 = null,
204     hw_sample_kind: ?[]const u8 = null,
205     plot_kind: ?[]const u8 = null,
206     plot_unit: ?[]const u8 = null,
207     plot_step: bool = false,
208     plot_fill: bool = false,
209     lock_kind: ?[]const u8 = null,
210     sample_kind: ?[]const u8 = null,
211     context_reason: ?[]const u8 = null,
212     context_state: ?[]const u8 = null,
213 
214     pub fn writeJsonLine(self: TraceEvent, writer: *std.Io.Writer) !void {
215         var stream = pretty_json.Writer.init(writer, .minified);
216         const object = try stream.object();
217         try object.field("schema", schema);
218         try object.field("v", format_version);
219         try object.field("seq", self.seq);
220         try object.field("kind", self.kind.tag());
221         try object.field("time_ns", self.time_ns);
222         try object.field("thread", self.thread);
223         if (self.id != 0) try object.field("id", self.id);
224         if (self.name) |name| try object.field("name", name);
225         if (self.text) |text| try object.field("text", text);
226         if (self.function) |function| try object.field("function", function);
227         if (self.file) |file| try object.field("file", file);
228         if (self.line != 0) try object.field("line", self.line);
229         if (self.column != 0) try object.field("column", self.column);
230         if (self.color) |color| try object.field("color", color);
231         if (self.value_u64) |value| try object.field("value_u64", value);
232         if (self.value_i64) |value| try object.field("value_i64", value);
233         if (self.value_f64) |value| {
234             if (std.math.isFinite(value)) try object.field("value_f64", value);
235         }
236         if (self.address != 0) try object.field("address", self.address);
237         if (self.size != 0) try object.field("size", self.size);
238         if (self.depth != 0) try object.field("depth", self.depth);
239         if (self.cpu) |cpu| try object.field("cpu", cpu);
240         if (self.prev_thread != 0) try object.field("prev_thread", self.prev_thread);
241         if (self.next_thread != 0) try object.field("next_thread", self.next_thread);
242         if (self.target_thread != 0) try object.field("target_thread", self.target_thread);
243         if (self.pid != 0) try object.field("pid", self.pid);
244         if (self.previous_cstate) |previous_cstate| try object.field("previous_cstate", previous_cstate);
245         if (self.prev_priority) |prev_priority| try object.field("prev_priority", prev_priority);
246         if (self.next_priority) |next_priority| try object.field("next_priority", next_priority);
247         if (self.group_hint) |group_hint| try object.field("group_hint", group_hint);
248         if (self.gpu_context) |gpu_context| try object.field("gpu_context", gpu_context);
249         if (self.gpu_query) |gpu_query| try object.field("gpu_query", gpu_query);
250         if (self.gpu_time) |gpu_time| try object.field("gpu_time", gpu_time);
251         if (self.gpu_period) |gpu_period| {
252             if (std.math.isFinite(gpu_period)) try object.field("gpu_period", gpu_period);
253         }
254         if (self.gpu_cpu_delta_ns) |value| try object.field("gpu_cpu_delta_ns", value);
255         if (self.gpu_context_type) |value| try object.field("gpu_context_type", value);
256         if (self.gpu_has_calibration) try object.field("gpu_has_calibration", true);
257         if (self.gpu_serial) try object.field("gpu_serial", true);
258         if (self.gpu_annotation_id) |value| try object.field("gpu_annotation_id", value);
259         if (self.cpu_package) |value| try object.field("cpu_package", value);
260         if (self.cpu_die) |value| try object.field("cpu_die", value);
261         if (self.cpu_core) |value| try object.field("cpu_core", value);
262         if (self.power_delta_uj) |value| try object.field("power_delta_uj", value);
263         if (self.hw_sample_kind) |value| try object.field("hw_sample_kind", value);
264         if (self.plot_kind) |value| try object.field("plot_kind", value);
265         if (self.plot_unit) |value| try object.field("plot_unit", value);
266         if (self.plot_step) try object.field("plot_step", true);
267         if (self.plot_fill) try object.field("plot_fill", true);
268         if (self.lock_kind) |value| try object.field("lock_kind", value);
269         if (self.sample_kind) |value| try object.field("sample_kind", value);
270         if (self.context_reason) |value| try object.field("context_reason", value);
271         if (self.context_state) |value| try object.field("context_state", value);
272         try object.endLine();
273     }
274 };
275 
276 pub const Parsed = struct {
277     tree: std.json.Parsed(std.json.Value),
278     kind: Kind,
279     seq: u64 = 0,
280     time_ns: u64 = 0,
281     thread: u64 = 0,
282     id: u64 = 0,
283     name: ?[]const u8 = null,
284     text: ?[]const u8 = null,
285     function: ?[]const u8 = null,
286     file: ?[]const u8 = null,
287     line: u32 = 0,
288     column: u32 = 0,
289     color: ?u32 = null,
290     value_u64: ?u64 = null,
291     value_i64: ?i64 = null,
292     value_f64: ?f64 = null,
293     address: u64 = 0,
294     size: u64 = 0,
295     depth: u32 = 0,
296     cpu: ?u32 = null,
297     prev_thread: u64 = 0,
298     next_thread: u64 = 0,
299     target_thread: u64 = 0,
300     pid: u64 = 0,
301     previous_cstate: ?u32 = null,
302     prev_priority: ?i64 = null,
303     next_priority: ?i64 = null,
304     group_hint: ?i64 = null,
305     gpu_context: ?u32 = null,
306     gpu_query: ?u32 = null,
307     gpu_time: ?i64 = null,
308     gpu_period: ?f64 = null,
309     gpu_cpu_delta_ns: ?i64 = null,
310     gpu_context_type: ?[]const u8 = null,
311     gpu_has_calibration: bool = false,
312     gpu_serial: bool = false,
313     gpu_annotation_id: ?i64 = null,
314     cpu_package: ?u32 = null,
315     cpu_die: ?u32 = null,
316     cpu_core: ?u32 = null,
317     power_delta_uj: ?u64 = null,
318     hw_sample_kind: ?[]const u8 = null,
319     plot_kind: ?[]const u8 = null,
320     plot_unit: ?[]const u8 = null,
321     plot_step: bool = false,
322     plot_fill: bool = false,
323     lock_kind: ?[]const u8 = null,
324     sample_kind: ?[]const u8 = null,
325     context_reason: ?[]const u8 = null,
326     context_state: ?[]const u8 = null,
327 
328     pub fn deinit(self: *Parsed) void {
329         self.tree.deinit();
330         self.* = undefined;
331     }
332 };
333 
334 pub fn parseLine(allocator: std.mem.Allocator, line: []const u8) !Parsed {
335     const text = std.mem.trim(u8, line, " \t\r\n");
336     if (text.len == 0) return error.EmptyEventLine;
337     var tree = std.json.parseFromSlice(std.json.Value, allocator, text, .{}) catch |err| {
338         if (err == error.OutOfMemory) return err;
339         return error.InvalidEventJson;
340     };
341     errdefer tree.deinit();
342     const object = switch (tree.value) {
343         .object => |object| object,
344         else => return error.InvalidEventJson,
345     };
346     if (object.get("schema")) |value| {
347         const actual_schema = try jsonString(value);
348         if (!std.mem.eql(u8, actual_schema, schema)) return error.UnsupportedTraceSchema;
349     }
350     const version = try jsonU64(object.get("v") orelse return error.InvalidEventJson);
351     if (version != format_version) return error.UnsupportedTraceVersion;
352     const kind_text = try jsonString(object.get("kind") orelse return error.InvalidEventJson);
353     return .{
354         .tree = tree,
355         .kind = Kind.fromTag(kind_text) orelse return error.InvalidEventJson,
356         .seq = try jsonU64Default(object, "seq", 0),
357         .time_ns = try jsonU64Default(object, "time_ns", 0),
358         .thread = try jsonU64Default(object, "thread", 0),
359         .id = try jsonU64Default(object, "id", 0),
360         .name = try jsonStringOptional(object, "name"),
361         .text = try jsonStringOptional(object, "text"),
362         .function = try jsonStringOptional(object, "function"),
363         .file = try jsonStringOptional(object, "file"),
364         .line = @intCast(try jsonU64Default(object, "line", 0)),
365         .column = @intCast(try jsonU64Default(object, "column", 0)),
366         .color = try jsonU32Optional(object, "color"),
367         .value_u64 = try jsonU64Optional(object, "value_u64"),
368         .value_i64 = try jsonI64Optional(object, "value_i64"),
369         .value_f64 = try jsonF64Optional(object, "value_f64"),
370         .address = try jsonU64Default(object, "address", 0),
371         .size = try jsonU64Default(object, "size", 0),
372         .depth = @intCast(try jsonU64Default(object, "depth", 0)),
373         .cpu = try jsonU32Optional(object, "cpu"),
374         .prev_thread = try jsonU64Default(object, "prev_thread", 0),
375         .next_thread = try jsonU64Default(object, "next_thread", 0),
376         .target_thread = try jsonU64Default(object, "target_thread", 0),
377         .pid = try jsonU64Default(object, "pid", 0),
378         .previous_cstate = try jsonU32Optional(object, "previous_cstate"),
379         .prev_priority = try jsonI64Optional(object, "prev_priority"),
380         .next_priority = try jsonI64Optional(object, "next_priority"),
381         .group_hint = try jsonI64Optional(object, "group_hint"),
382         .gpu_context = try jsonU32Optional(object, "gpu_context"),
383         .gpu_query = try jsonU32Optional(object, "gpu_query"),
384         .gpu_time = try jsonI64Optional(object, "gpu_time"),
385         .gpu_period = try jsonF64Optional(object, "gpu_period"),
386         .gpu_cpu_delta_ns = try jsonI64Optional(object, "gpu_cpu_delta_ns"),
387         .gpu_context_type = try jsonStringOptional(object, "gpu_context_type"),
388         .gpu_has_calibration = try jsonBoolDefault(object, "gpu_has_calibration", false),
389         .gpu_serial = try jsonBoolDefault(object, "gpu_serial", false),
390         .gpu_annotation_id = try jsonI64Optional(object, "gpu_annotation_id"),
391         .cpu_package = try jsonU32Optional(object, "cpu_package"),
392         .cpu_die = try jsonU32Optional(object, "cpu_die"),
393         .cpu_core = try jsonU32Optional(object, "cpu_core"),
394         .power_delta_uj = try jsonU64Optional(object, "power_delta_uj"),
395         .hw_sample_kind = try jsonStringOptional(object, "hw_sample_kind"),
396         .plot_kind = try jsonStringOptional(object, "plot_kind"),
397         .plot_unit = try jsonStringOptional(object, "plot_unit"),
398         .plot_step = try jsonBoolDefault(object, "plot_step", false),
399         .plot_fill = try jsonBoolDefault(object, "plot_fill", false),
400         .lock_kind = try jsonStringOptional(object, "lock_kind"),
401         .sample_kind = try jsonStringOptional(object, "sample_kind"),
402         .context_reason = try jsonStringOptional(object, "context_reason"),
403         .context_state = try jsonStringOptional(object, "context_state"),
404     };
405 }
406 
407 fn jsonString(value: std.json.Value) ![]const u8 {
408     return switch (value) {
409         .string => |text| text,
410         else => error.InvalidEventJson,
411     };
412 }
413 
414 fn jsonStringOptional(object: std.json.ObjectMap, key: []const u8) !?[]const u8 {
415     const value = object.get(key) orelse return null;
416     return try jsonString(value);
417 }
418 
419 fn jsonU64(value: std.json.Value) !u64 {
420     return switch (value) {
421         .integer => |integer| if (integer < 0) error.InvalidEventJson else @intCast(integer),
422         else => error.InvalidEventJson,
423     };
424 }
425 
426 fn jsonU64Default(object: std.json.ObjectMap, key: []const u8, default: u64) !u64 {
427     const value = object.get(key) orelse return default;
428     return try jsonU64(value);
429 }
430 
431 fn jsonU64Optional(object: std.json.ObjectMap, key: []const u8) !?u64 {
432     const value = object.get(key) orelse return null;
433     return try jsonU64(value);
434 }
435 
436 fn jsonU32Optional(object: std.json.ObjectMap, key: []const u8) !?u32 {
437     const value = object.get(key) orelse return null;
438     return @intCast(try jsonU64(value));
439 }
440 
441 fn jsonI64(value: std.json.Value) !i64 {
442     return switch (value) {
443         .integer => |integer| @intCast(integer),
444         else => error.InvalidEventJson,
445     };
446 }
447 
448 fn jsonI64Optional(object: std.json.ObjectMap, key: []const u8) !?i64 {
449     const value = object.get(key) orelse return null;
450     return try jsonI64(value);
451 }
452 
453 fn jsonF64(value: std.json.Value) !f64 {
454     return switch (value) {
455         .integer => |integer| @floatFromInt(integer),
456         .float => |float| float,
457         else => error.InvalidEventJson,
458     };
459 }
460 
461 fn jsonF64Optional(object: std.json.ObjectMap, key: []const u8) !?f64 {
462     const value = object.get(key) orelse return null;
463     return try jsonF64(value);
464 }
465 
466 fn jsonBool(value: std.json.Value) !bool {
467     return switch (value) {
468         .bool => |boolean| boolean,
469         else => error.InvalidEventJson,
470     };
471 }
472 
473 fn jsonBoolDefault(object: std.json.ObjectMap, key: []const u8, default: bool) !bool {
474     const value = object.get(key) orelse return default;
475     return try jsonBool(value);
476 }
477 
478 test "event json parses written zone begin" {
479     var out = std.Io.Writer.Allocating.init(std.testing.allocator);
480     defer out.deinit();
481 
482     try (TraceEvent{
483         .seq = 7,
484         .kind = .zone_begin,
485         .time_ns = 100,
486         .thread = 2,
487         .id = 9,
488         .name = "phase",
489         .function = "run",
490         .file = "run.zig",
491         .line = 12,
492         .color = 0x44aa88,
493     }).writeJsonLine(&out.writer);
494 
495     var parsed = try parseLine(std.testing.allocator, out.written());
496     defer parsed.deinit();
497     try std.testing.expectEqual(Kind.zone_begin, parsed.kind);
498     try std.testing.expectEqual(@as(u64, 9), parsed.id);
499     try std.testing.expectEqualStrings("phase", parsed.name.?);
500     try std.testing.expectEqualStrings("run.zig", parsed.file.?);
501     try std.testing.expectEqual(@as(?u32, 0x44aa88), parsed.color);
502 }
503 
504 test "event json parses sample frame metadata" {
505     var out = std.Io.Writer.Allocating.init(std.testing.allocator);
506     defer out.deinit();
507 
508     try (TraceEvent{
509         .seq = 8,
510         .kind = .sample_frame,
511         .time_ns = 120,
512         .thread = 3,
513         .id = 4,
514         .depth = 2,
515         .name = "leaf",
516         .function = "run",
517         .file = "run.zig",
518         .line = 14,
519         .sample_kind = "cpu",
520     }).writeJsonLine(&out.writer);
521 
522     var parsed = try parseLine(std.testing.allocator, out.written());
523     defer parsed.deinit();
524     try std.testing.expectEqual(Kind.sample_frame, parsed.kind);
525     try std.testing.expectEqual(@as(u32, 2), parsed.depth);
526     try std.testing.expectEqualStrings("cpu", parsed.sample_kind.?);
527 }
528 
529 test "event json parses context switch metadata" {
530     var out = std.Io.Writer.Allocating.init(std.testing.allocator);
531     defer out.deinit();
532 
533     try (TraceEvent{
534         .seq = 9,
535         .kind = .context_switch,
536         .time_ns = 150,
537         .thread = 3,
538         .cpu = 0,
539         .prev_thread = 10,
540         .next_thread = 11,
541         .previous_cstate = 1,
542         .prev_priority = 8,
543         .next_priority = 9,
544         .context_reason = "WrPreempted",
545         .context_state = "Ready",
546     }).writeJsonLine(&out.writer);
547 
548     var parsed = try parseLine(std.testing.allocator, out.written());
549     defer parsed.deinit();
550     try std.testing.expectEqual(Kind.context_switch, parsed.kind);
551     try std.testing.expectEqual(@as(?u32, 0), parsed.cpu);
552     try std.testing.expectEqual(@as(u64, 10), parsed.prev_thread);
553     try std.testing.expectEqual(@as(u64, 11), parsed.next_thread);
554     try std.testing.expectEqual(@as(?u32, 1), parsed.previous_cstate);
555     try std.testing.expectEqual(@as(?i64, 8), parsed.prev_priority);
556     try std.testing.expectEqual(@as(?i64, 9), parsed.next_priority);
557     try std.testing.expectEqualStrings("WrPreempted", parsed.context_reason.?);
558     try std.testing.expectEqualStrings("Ready", parsed.context_state.?);
559 }
560 
561 test "event json parses fiber metadata" {
562     var out = std.Io.Writer.Allocating.init(std.testing.allocator);
563     defer out.deinit();
564 
565     try (TraceEvent{
566         .seq = 10,
567         .kind = .fiber_enter,
568         .time_ns = 200,
569         .thread = 42,
570         .id = 9,
571         .name = "job.apply",
572         .group_hint = 3,
573     }).writeJsonLine(&out.writer);
574 
575     var parsed = try parseLine(std.testing.allocator, out.written());
576     defer parsed.deinit();
577     try std.testing.expectEqual(Kind.fiber_enter, parsed.kind);
578     try std.testing.expectEqual(@as(u64, 9), parsed.id);
579     try std.testing.expectEqual(@as(u64, 42), parsed.thread);
580     try std.testing.expectEqualStrings("job.apply", parsed.name.?);
581     try std.testing.expectEqual(@as(?i64, 3), parsed.group_hint);
582 }
583 
584 test "event json parses gpu metadata" {
585     var out = std.Io.Writer.Allocating.init(std.testing.allocator);
586     defer out.deinit();
587 
588     try (TraceEvent{
589         .seq = 11,
590         .kind = .gpu_context,
591         .time_ns = 300,
592         .thread = 4,
593         .name = "render",
594         .gpu_context = 2,
595         .gpu_time = 1000,
596         .gpu_period = 2.0,
597         .gpu_context_type = "vulkan",
598         .gpu_has_calibration = true,
599         .gpu_serial = true,
600     }).writeJsonLine(&out.writer);
601 
602     var parsed = try parseLine(std.testing.allocator, out.written());
603     defer parsed.deinit();
604     try std.testing.expectEqual(Kind.gpu_context, parsed.kind);
605     try std.testing.expectEqual(@as(?u32, 2), parsed.gpu_context);
606     try std.testing.expectEqual(@as(?i64, 1000), parsed.gpu_time);
607     try std.testing.expectEqual(@as(?f64, 2.0), parsed.gpu_period);
608     try std.testing.expectEqualStrings("vulkan", parsed.gpu_context_type.?);
609     try std.testing.expect(parsed.gpu_has_calibration);
610     try std.testing.expect(parsed.gpu_serial);
611 }
612 
613 test "event json parses system metadata" {
614     var out = std.Io.Writer.Allocating.init(std.testing.allocator);
615     defer out.deinit();
616 
617     try (TraceEvent{
618         .seq = 12,
619         .kind = .hw_sample,
620         .time_ns = 400,
621         .thread = 9,
622         .cpu = 3,
623         .address = 0x1234,
624         .pid = 77,
625         .cpu_package = 1,
626         .cpu_die = 2,
627         .cpu_core = 4,
628         .power_delta_uj = 500,
629         .hw_sample_kind = "cache-miss",
630     }).writeJsonLine(&out.writer);
631 
632     var parsed = try parseLine(std.testing.allocator, out.written());
633     defer parsed.deinit();
634     try std.testing.expectEqual(Kind.hw_sample, parsed.kind);
635     try std.testing.expectEqual(@as(u64, 9), parsed.thread);
636     try std.testing.expectEqual(@as(?u32, 3), parsed.cpu);
637     try std.testing.expectEqual(@as(u64, 0x1234), parsed.address);
638     try std.testing.expectEqual(@as(u64, 77), parsed.pid);
639     try std.testing.expectEqual(@as(?u32, 1), parsed.cpu_package);
640     try std.testing.expectEqual(@as(?u32, 2), parsed.cpu_die);
641     try std.testing.expectEqual(@as(?u32, 4), parsed.cpu_core);
642     try std.testing.expectEqual(@as(?u64, 500), parsed.power_delta_uj);
643     try std.testing.expectEqualStrings("cache-miss", parsed.hw_sample_kind.?);
644 }