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 }