lib/trace/src/event.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const pretty_json = @import("pretty").json;
3
4 const Allocator = std.mem.Allocator;
5
6 pub const trace_format_version: u32 = 3;
7 pub const ThreadId = u32;
8
9 pub const Mode = enum {
10 off,
11 record,
12 replay,
13 };
14
15 pub const Timepoint = struct {
16 epoch: u64 = 0,
17 thread_id: ThreadId = 0,
18 seq: u64 = 0,
19
20 pub fn beforeOrEqual(self: Timepoint, other: Timepoint) bool {
21 if (self.epoch != other.epoch) return self.epoch < other.epoch;
22 if (self.seq != other.seq) return self.seq <= other.seq;
23 return self.thread_id <= other.thread_id;
24 }
25
26 pub fn eql(left: Timepoint, right: Timepoint) bool {
27 return left.epoch == right.epoch and
28 left.thread_id == right.thread_id and
29 left.seq == right.seq;
30 }
31
32 pub fn write(self: Timepoint, writer: *std.Io.Writer) !void {
33 try writer.print("{d}:{d}:{d}", .{ self.epoch, self.thread_id, self.seq });
34 }
35 };
36
37 pub const Safepoint = struct {
38 function_id: u64,
39 site_id: u64,
40 stack_map_id: u64 = 0,
41 };
42
43 pub const EventKind = enum {
44 session_start,
45 session_end,
46 function_enter,
47 function_exit,
48 safepoint,
49 boundary,
50 allocation,
51 free,
52 checkpoint,
53 checkpoint_restore,
54 user,
55
56 pub fn tag(self: EventKind) []const u8 {
57 return switch (self) {
58 .session_start => "session.start",
59 .session_end => "session.end",
60 .function_enter => "function.enter",
61 .function_exit => "function.exit",
62 .safepoint => "safepoint",
63 .boundary => "boundary",
64 .allocation => "allocation",
65 .free => "free",
66 .checkpoint => "checkpoint",
67 .checkpoint_restore => "checkpoint.restore",
68 .user => "user",
69 };
70 }
71
72 pub fn fromTag(text: []const u8) ?EventKind {
73 inline for (
74 @typeInfo(EventKind).@"enum".field_names,
75 @typeInfo(EventKind).@"enum".field_values,
76 ) |field_name, field_name_value| {
77 const field = .{ .name = field_name, .value = field_name_value };
78 const kind: EventKind = @fromBackingInt(@intCast(field.value));
79 if (std.mem.eql(u8, text, kind.tag())) return kind;
80 }
81 return null;
82 }
83 };
84
85 pub const Event = struct {
86 kind: EventKind,
87 timepoint: Timepoint,
88 function_id: u64 = 0,
89 site_id: u64 = 0,
90 stack_map_id: u64 = 0,
91 object_id: u64 = 0,
92 size: u64 = 0,
93 alignment: u32 = 0,
94 status: i64 = 0,
95 operation: ?[]const u8 = null,
96 label: ?[]const u8 = null,
97 data: ?[]const u8 = null,
98
99 pub fn sessionStart(timepoint: Timepoint, label: []const u8) Event {
100 return .{ .kind = .session_start, .timepoint = timepoint, .label = label };
101 }
102
103 pub fn sessionEnd(timepoint: Timepoint, status: i64) Event {
104 return .{ .kind = .session_end, .timepoint = timepoint, .status = status };
105 }
106
107 pub fn functionEnter(timepoint: Timepoint, safepoint: Safepoint) Event {
108 return functionEvent(.function_enter, timepoint, safepoint);
109 }
110
111 pub fn functionExit(timepoint: Timepoint, safepoint: Safepoint) Event {
112 return functionEvent(.function_exit, timepoint, safepoint);
113 }
114
115 pub fn safepointReached(timepoint: Timepoint, safepoint: Safepoint) Event {
116 return functionEvent(.safepoint, timepoint, safepoint);
117 }
118
119 pub fn boundaryBytes(timepoint: Timepoint, operation: []const u8, bytes: []const u8) Event {
120 return .{
121 .kind = .boundary,
122 .timepoint = timepoint,
123 .operation = operation,
124 .data = bytes,
125 };
126 }
127
128 pub fn allocation(
129 timepoint: Timepoint,
130 object_id: u64,
131 size: u64,
132 alignment: u32,
133 label: ?[]const u8,
134 ) Event {
135 return .{
136 .kind = .allocation,
137 .timepoint = timepoint,
138 .object_id = object_id,
139 .size = size,
140 .alignment = alignment,
141 .label = label,
142 };
143 }
144
145 pub fn free(timepoint: Timepoint, object_id: u64) Event {
146 return .{ .kind = .free, .timepoint = timepoint, .object_id = object_id };
147 }
148
149 pub fn checkpoint(timepoint: Timepoint, label: []const u8, bytes: []const u8) Event {
150 return .{
151 .kind = .checkpoint,
152 .timepoint = timepoint,
153 .label = label,
154 .data = bytes,
155 };
156 }
157
158 pub fn checkpointRestore(timepoint: Timepoint, label: []const u8) Event {
159 return .{ .kind = .checkpoint_restore, .timepoint = timepoint, .label = label };
160 }
161
162 pub fn user(timepoint: Timepoint, label: []const u8, bytes: []const u8) Event {
163 return .{
164 .kind = .user,
165 .timepoint = timepoint,
166 .label = label,
167 .data = bytes,
168 };
169 }
170
171 pub fn cloneAlloc(self: Event, allocator: Allocator) !Event {
172 var cloned = self;
173 cloned.operation = if (self.operation) |operation|
174 try allocator.dupe(u8, operation)
175 else
176 null;
177 errdefer if (cloned.operation) |operation| allocator.free(operation);
178
179 cloned.label = if (self.label) |label|
180 try allocator.dupe(u8, label)
181 else
182 null;
183 errdefer if (cloned.label) |label| allocator.free(label);
184
185 cloned.data = if (self.data) |data|
186 try allocator.dupe(u8, data)
187 else
188 null;
189 errdefer if (cloned.data) |data| allocator.free(data);
190
191 return cloned;
192 }
193
194 pub fn deinit(self: *Event, allocator: Allocator) void {
195 if (self.operation) |operation| allocator.free(operation);
196 if (self.label) |label| allocator.free(label);
197 if (self.data) |data| allocator.free(data);
198 self.* = undefined;
199 }
200
201 pub fn eqlForReplay(left: Event, right: Event) bool {
202 return left.kind == right.kind and
203 Timepoint.eql(left.timepoint, right.timepoint) and
204 left.function_id == right.function_id and
205 left.site_id == right.site_id and
206 left.stack_map_id == right.stack_map_id and
207 left.object_id == right.object_id and
208 left.size == right.size and
209 left.alignment == right.alignment and
210 left.status == right.status and
211 optionalBytesEql(left.operation, right.operation) and
212 optionalBytesEql(left.label, right.label) and
213 optionalBytesEql(left.data, right.data);
214 }
215
216 pub fn writeJsonLine(self: Event, writer: *std.Io.Writer) !void {
217 var stream = pretty_json.Writer.init(writer, .minified);
218 const object = try stream.object();
219 try object.field("v", trace_format_version);
220 const timepoint = try object.object("timepoint");
221 try timepoint.field("epoch", self.timepoint.epoch);
222 try timepoint.field("thread", self.timepoint.thread_id);
223 try timepoint.field("seq", self.timepoint.seq);
224 try timepoint.end();
225 try object.field("kind", self.kind.tag());
226 try object.field("function_id", self.function_id);
227 try object.field("site_id", self.site_id);
228 try object.field("stack_map_id", self.stack_map_id);
229 try object.field("object_id", self.object_id);
230 try object.field("size", self.size);
231 try object.field("alignment", self.alignment);
232 try object.field("status", self.status);
233 if (self.operation) |operation| try object.field("operation", operation);
234 if (self.label) |label| try object.field("label", label);
235 if (self.data) |data| try object.hexString("data_hex", data);
236 try object.endLine();
237 }
238
239 pub fn fromJsonLine(allocator: Allocator, line: []const u8) !Event {
240 const parsed = std.json.parseFromSlice(std.json.Value, allocator, line, .{}) catch
241 return error.InvalidEventJson;
242 defer parsed.deinit();
243
244 const object = switch (parsed.value) {
245 .object => |object| object,
246 else => return error.InvalidEventJson,
247 };
248
249 const version = try jsonU64(object.get("v") orelse return error.InvalidEventJson);
250 if (version != trace_format_version) return error.UnsupportedTraceVersion;
251
252 const timepoint_object = switch (object.get("timepoint") orelse return error.InvalidEventJson) {
253 .object => |value| value,
254 else => return error.InvalidEventJson,
255 };
256 const kind_text = try jsonString(object.get("kind") orelse return error.InvalidEventJson);
257 const kind = EventKind.fromTag(kind_text) orelse return error.InvalidEventJson;
258
259 var event = Event{
260 .kind = kind,
261 .timepoint = .{
262 .epoch = try jsonU64(timepoint_object.get("epoch") orelse return error.InvalidEventJson),
263 .thread_id = @intCast(try jsonU64(timepoint_object.get("thread") orelse return error.InvalidEventJson)),
264 .seq = try jsonU64(timepoint_object.get("seq") orelse return error.InvalidEventJson),
265 },
266 .function_id = try jsonU64(object.get("function_id") orelse return error.InvalidEventJson),
267 .site_id = try jsonU64(object.get("site_id") orelse return error.InvalidEventJson),
268 .stack_map_id = try jsonU64(object.get("stack_map_id") orelse return error.InvalidEventJson),
269 .object_id = try jsonU64(object.get("object_id") orelse return error.InvalidEventJson),
270 .size = try jsonU64(object.get("size") orelse return error.InvalidEventJson),
271 .alignment = @intCast(try jsonU64(object.get("alignment") orelse return error.InvalidEventJson)),
272 .status = try jsonI64(object.get("status") orelse return error.InvalidEventJson),
273 };
274 errdefer event.deinit(allocator);
275
276 if (object.get("operation")) |operation| {
277 event.operation = try allocator.dupe(u8, try jsonString(operation));
278 }
279 if (object.get("label")) |label| {
280 event.label = try allocator.dupe(u8, try jsonString(label));
281 }
282 if (object.get("data_hex")) |data_hex| {
283 event.data = try decodeHexAlloc(allocator, try jsonString(data_hex));
284 }
285
286 return event;
287 }
288 };
289
290 pub const Sink = struct {
291 context: *anyopaque,
292 appendFn: *const fn (context: *anyopaque, item: Event) anyerror!void,
293
294 pub fn append(self: Sink, item: Event) !void {
295 try self.appendFn(self.context, item);
296 }
297 };
298
299 pub const Source = struct {
300 context: *anyopaque,
301 peekFn: *const fn (context: *anyopaque) anyerror!?*const Event,
302 advanceFn: *const fn (context: *anyopaque) void,
303 countFn: *const fn (context: *anyopaque) u64,
304
305 pub fn peek(self: Source) !?*const Event {
306 return try self.peekFn(self.context);
307 }
308
309 pub fn advance(self: Source) void {
310 self.advanceFn(self.context);
311 }
312
313 pub fn count(self: Source) u64 {
314 return self.countFn(self.context);
315 }
316 };
317
318 fn functionEvent(kind: EventKind, timepoint: Timepoint, safepoint: Safepoint) Event {
319 return .{
320 .kind = kind,
321 .timepoint = timepoint,
322 .function_id = safepoint.function_id,
323 .site_id = safepoint.site_id,
324 .stack_map_id = safepoint.stack_map_id,
325 };
326 }
327
328 fn optionalBytesEql(left: ?[]const u8, right: ?[]const u8) bool {
329 if (left == null and right == null) return true;
330 if (left == null or right == null) return false;
331 return std.mem.eql(u8, left.?, right.?);
332 }
333
334 fn jsonString(value: std.json.Value) ![]const u8 {
335 return switch (value) {
336 .string => |text| text,
337 else => error.InvalidEventJson,
338 };
339 }
340
341 fn jsonU64(value: std.json.Value) !u64 {
342 return switch (value) {
343 .integer => |integer| if (integer < 0) error.InvalidEventJson else @intCast(integer),
344 .number_string => |text| std.fmt.parseInt(u64, text, 10) catch error.InvalidEventJson,
345 else => error.InvalidEventJson,
346 };
347 }
348
349 fn jsonI64(value: std.json.Value) !i64 {
350 return switch (value) {
351 .integer => |integer| @intCast(integer),
352 else => error.InvalidEventJson,
353 };
354 }
355
356 fn decodeHexAlloc(allocator: Allocator, text: []const u8) ![]u8 {
357 if (text.len % 2 != 0) return error.InvalidEventJson;
358 const bytes = try allocator.alloc(u8, text.len / 2);
359 errdefer allocator.free(bytes);
360 for (bytes, 0..) |*byte, index| {
361 const high = try hexNibble(text[index * 2]);
362 const low = try hexNibble(text[index * 2 + 1]);
363 byte.* = (high << 4) | low;
364 }
365 return bytes;
366 }
367
368 fn hexNibble(byte: u8) !u8 {
369 return switch (byte) {
370 '0'...'9' => byte - '0',
371 'a'...'f' => byte - 'a' + 10,
372 'A'...'F' => byte - 'A' + 10,
373 else => error.InvalidEventJson,
374 };
375 }
376
377 test "event json roundtrip preserves replay identity" {
378 const allocator = std.testing.allocator;
379 const original = Event.boundaryBytes(
380 .{ .epoch = 7, .thread_id = 1, .seq = 42 },
381 "env.TEST",
382 "value\nwith bytes",
383 );
384
385 var out = std.Io.Writer.Allocating.init(allocator);
386 defer out.deinit();
387 try original.writeJsonLine(&out.writer);
388 const bytes = try out.toOwnedSlice();
389 defer allocator.free(bytes);
390
391 var parsed = try Event.fromJsonLine(allocator, std.mem.trimEnd(u8, bytes, "\n"));
392 defer parsed.deinit(allocator);
393
394 try std.testing.expect(original.eqlForReplay(parsed));
395 }
396
397 test "timepoints sort by epoch then sequence" {
398 try std.testing.expect((Timepoint{ .epoch = 0, .thread_id = 9, .seq = 10 }).beforeOrEqual(.{
399 .epoch = 0,
400 .thread_id = 1,
401 .seq = 11,
402 }));
403 try std.testing.expect(!(Timepoint{ .epoch = 1, .thread_id = 0, .seq = 0 }).beforeOrEqual(.{
404 .epoch = 0,
405 .thread_id = 0,
406 .seq = 100,
407 }));
408 }