lib/trace/src/replay.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const pretty_json = @import("pretty").json;
3 const event = @import("event.zig");
4
5 const timepoint_string_capacity = 54;
6
7 pub const Frame = struct {
8 depth: usize = 0,
9 function_id: u64,
10 site_id: u64,
11 stack_map_id: u64,
12 entered_at: event.Timepoint,
13 last_timepoint: event.Timepoint,
14 synthetic: bool = false,
15
16 pub fn writeJsonLine(self: Frame, writer: *std.Io.Writer) !void {
17 var stream = pretty_json.Writer.init(writer, .minified);
18 const object = try stream.object();
19 try object.field("event", "frame");
20 try object.field("depth", self.depth);
21 try object.field("function_id", self.function_id);
22 try object.field("site_id", self.site_id);
23 try object.field("stack_map_id", self.stack_map_id);
24 try writeTimepointField(object, "entered_at", self.entered_at);
25 try writeTimepointField(object, "last_timepoint", self.last_timepoint);
26 try object.field("synthetic", self.synthetic);
27 try object.endLine();
28 }
29 };
30
31 pub const TimepointEntry = struct {
32 index: usize,
33 timepoint: event.Timepoint,
34 kind: event.EventKind,
35 function_id: u64 = 0,
36 site_id: u64 = 0,
37 stack_map_id: u64 = 0,
38 object_id: u64 = 0,
39 label: ?[]const u8 = null,
40 operation: ?[]const u8 = null,
41
42 pub fn fromEvent(index: usize, item: event.Event) TimepointEntry {
43 return .{
44 .index = index,
45 .timepoint = item.timepoint,
46 .kind = item.kind,
47 .function_id = item.function_id,
48 .site_id = item.site_id,
49 .stack_map_id = item.stack_map_id,
50 .object_id = item.object_id,
51 .label = item.label,
52 .operation = item.operation,
53 };
54 }
55
56 pub fn writeJsonLine(self: TimepointEntry, writer: *std.Io.Writer) !void {
57 var stream = pretty_json.Writer.init(writer, .minified);
58 const object = try stream.object();
59 try object.field("event", "timepoint");
60 try object.field("index", self.index);
61 try writeTimepointField(object, "timepoint", self.timepoint);
62 try object.field("epoch", self.timepoint.epoch);
63 try object.field("thread", self.timepoint.thread_id);
64 try object.field("seq", self.timepoint.seq);
65 try object.field("kind", self.kind.tag());
66 try object.field("function_id", self.function_id);
67 try object.field("site_id", self.site_id);
68 try object.field("stack_map_id", self.stack_map_id);
69 try object.field("object_id", self.object_id);
70 if (self.label) |label| try object.field("label", label);
71 if (self.operation) |operation| try object.field("operation", operation);
72 try object.endLine();
73 }
74 };
75
76 pub const UserEventFilter = struct {
77 label: ?[]const u8 = null,
78 protocol: ?[]const u8 = null,
79 payload_filters: []const UserPayloadFilter = &.{},
80 };
81
82 pub const UserPayloadFilter = struct {
83 field: []const u8,
84 value: []const u8,
85 };
86
87 pub const UserEventStatSort = enum {
88 total,
89 count,
90 min,
91 max,
92 mean,
93 group,
94
95 pub fn parse(text: []const u8) ?UserEventStatSort {
96 if (std.mem.eql(u8, text, "total")) return .total;
97 if (std.mem.eql(u8, text, "count")) return .count;
98 if (std.mem.eql(u8, text, "min")) return .min;
99 if (std.mem.eql(u8, text, "max")) return .max;
100 if (std.mem.eql(u8, text, "mean")) return .mean;
101 if (std.mem.eql(u8, text, "group")) return .group;
102 return null;
103 }
104 };
105
106 pub const UserEventEntry = struct {
107 index: usize,
108 timepoint: event.Timepoint,
109 label: ?[]const u8 = null,
110 data: ?[]const u8 = null,
111
112 pub fn fromEvent(index: usize, item: event.Event) UserEventEntry {
113 return .{
114 .index = index,
115 .timepoint = item.timepoint,
116 .label = item.label,
117 .data = item.data,
118 };
119 }
120
121 pub fn writeJsonLine(
122 self: UserEventEntry,
123 allocator: std.mem.Allocator,
124 writer: *std.Io.Writer,
125 ) !void {
126 var stream = pretty_json.Writer.init(writer, .minified);
127 const object = try stream.object();
128 try object.field("event", "user");
129 try object.field("index", self.index);
130 try writeTimepointField(object, "timepoint", self.timepoint);
131 try object.field("epoch", self.timepoint.epoch);
132 try object.field("thread", self.timepoint.thread_id);
133 try object.field("seq", self.timepoint.seq);
134 if (self.label) |label| try object.field("label", label);
135 if (try userPayloadProtocolAlloc(allocator, self.data)) |protocol| {
136 defer allocator.free(protocol);
137 try object.field("protocol", protocol);
138 }
139 if (self.data) |bytes| {
140 try object.hexString("data_hex", bytes);
141 if (try payloadIsJson(allocator, bytes)) try object.raw("payload", bytes);
142 }
143 try object.endLine();
144 }
145 };
146
147 pub const UserEventStatEntry = struct {
148 group: []u8,
149 count: usize = 0,
150 first_index: ?usize = null,
151 last_index: ?usize = null,
152 first_timepoint: ?event.Timepoint = null,
153 last_timepoint: ?event.Timepoint = null,
154 total: u64 = 0,
155 min: u64 = 0,
156 max: u64 = 0,
157
158 fn deinit(self: *UserEventStatEntry, allocator: std.mem.Allocator) void {
159 allocator.free(self.group);
160 self.* = undefined;
161 }
162
163 fn record(self: *UserEventStatEntry, index: usize, timepoint: event.Timepoint, value: u64) void {
164 self.count += 1;
165 if (self.first_index == null) {
166 self.first_index = index;
167 self.first_timepoint = timepoint;
168 }
169 self.last_index = index;
170 self.last_timepoint = timepoint;
171 self.total +|= value;
172 self.min = if (self.count == 1) value else @min(self.min, value);
173 self.max = @max(self.max, value);
174 }
175
176 fn mean(self: UserEventStatEntry) u64 {
177 return if (self.count == 0) 0 else self.total / self.count;
178 }
179
180 fn sortValue(self: UserEventStatEntry, sort: UserEventStatSort) u64 {
181 return switch (sort) {
182 .total => self.total,
183 .count => @intCast(self.count),
184 .min => self.min,
185 .max => self.max,
186 .mean => self.mean(),
187 .group => 0,
188 };
189 }
190
191 fn writeJsonLine(
192 self: UserEventStatEntry,
193 group_field: []const u8,
194 metric_field: []const u8,
195 sort: UserEventStatSort,
196 writer: *std.Io.Writer,
197 ) !void {
198 var stream = pretty_json.Writer.init(writer, .minified);
199 const object = try stream.object();
200 try object.field("event", "user_event_stat");
201 try object.field("group_field", group_field);
202 try object.field("group", self.group);
203 try object.field("metric_field", metric_field);
204 try object.field("sort_field", @tagName(sort));
205 try object.field("first_index", self.first_index);
206 try object.field("last_index", self.last_index);
207 try writeOptionalTimepointField(object, "first_timepoint", self.first_timepoint);
208 try writeOptionalTimepointField(object, "last_timepoint", self.last_timepoint);
209 try object.field("count", self.count);
210 try object.field("total", self.total);
211 try object.field("min", self.min);
212 try object.field("max", self.max);
213 try object.field("mean", self.mean());
214 try object.endLine();
215 }
216 };
217
218 pub const SummaryEntry = struct {
219 event_count: usize = 0,
220 first_timepoint: ?event.Timepoint = null,
221 last_timepoint: ?event.Timepoint = null,
222 thread_count: usize = 0,
223 session_status: ?i64 = null,
224 session_start_count: usize = 0,
225 session_end_count: usize = 0,
226 function_enter_count: usize = 0,
227 function_exit_count: usize = 0,
228 safepoint_count: usize = 0,
229 boundary_count: usize = 0,
230 allocation_count: usize = 0,
231 free_count: usize = 0,
232 checkpoint_count: usize = 0,
233 checkpoint_restore_count: usize = 0,
234 user_count: usize = 0,
235 object_count: usize = 0,
236 live_object_count: usize = 0,
237 freed_object_count: usize = 0,
238 free_without_allocation_count: usize = 0,
239 allocated_bytes: u64 = 0,
240 live_bytes: u64 = 0,
241
242 pub fn record(self: *SummaryEntry, item: event.Event) void {
243 if (self.event_count == 0) self.first_timepoint = item.timepoint;
244 self.last_timepoint = item.timepoint;
245 self.event_count += 1;
246 switch (item.kind) {
247 .session_start => self.session_start_count += 1,
248 .session_end => {
249 self.session_end_count += 1;
250 self.session_status = item.status;
251 },
252 .function_enter => self.function_enter_count += 1,
253 .function_exit => self.function_exit_count += 1,
254 .safepoint => self.safepoint_count += 1,
255 .boundary => self.boundary_count += 1,
256 .allocation => {
257 self.allocation_count += 1;
258 self.allocated_bytes += item.size;
259 },
260 .free => self.free_count += 1,
261 .checkpoint => self.checkpoint_count += 1,
262 .checkpoint_restore => self.checkpoint_restore_count += 1,
263 .user => self.user_count += 1,
264 }
265 }
266
267 pub fn writeJsonLine(self: SummaryEntry, writer: *std.Io.Writer) !void {
268 var stream = pretty_json.Writer.init(writer, .minified);
269 const object = try stream.object();
270 try object.field("event", "summary");
271 try object.field("event_count", self.event_count);
272 try writeOptionalTimepointField(object, "first_timepoint", self.first_timepoint);
273 try writeOptionalTimepointField(object, "last_timepoint", self.last_timepoint);
274 try object.field("thread_count", self.thread_count);
275 try object.field("session_status", self.session_status);
276 try object.field("session_start_count", self.session_start_count);
277 try object.field("session_end_count", self.session_end_count);
278 try object.field("function_enter_count", self.function_enter_count);
279 try object.field("function_exit_count", self.function_exit_count);
280 try object.field("safepoint_count", self.safepoint_count);
281 try object.field("boundary_count", self.boundary_count);
282 try object.field("allocation_count", self.allocation_count);
283 try object.field("free_count", self.free_count);
284 try object.field("checkpoint_count", self.checkpoint_count);
285 try object.field("checkpoint_restore_count", self.checkpoint_restore_count);
286 try object.field("user_count", self.user_count);
287 try object.field("object_count", self.object_count);
288 try object.field("live_object_count", self.live_object_count);
289 try object.field("freed_object_count", self.freed_object_count);
290 try object.field(
291 "free_without_allocation_count",
292 self.free_without_allocation_count,
293 );
294 try object.field("allocated_bytes", self.allocated_bytes);
295 try object.field("live_bytes", self.live_bytes);
296 try object.endLine();
297 }
298 };
299
300 pub const ThreadEntry = struct {
301 thread_id: event.ThreadId,
302 first_index: usize,
303 last_index: usize,
304 first_timepoint: event.Timepoint,
305 last_timepoint: event.Timepoint,
306 event_count: usize = 0,
307 session_start_count: usize = 0,
308 session_end_count: usize = 0,
309 function_enter_count: usize = 0,
310 function_exit_count: usize = 0,
311 safepoint_count: usize = 0,
312 boundary_count: usize = 0,
313 allocation_count: usize = 0,
314 free_count: usize = 0,
315 checkpoint_count: usize = 0,
316 checkpoint_restore_count: usize = 0,
317 user_count: usize = 0,
318
319 pub fn init(index: usize, item: event.Event) ThreadEntry {
320 var entry = ThreadEntry{
321 .thread_id = item.timepoint.thread_id,
322 .first_index = index,
323 .last_index = index,
324 .first_timepoint = item.timepoint,
325 .last_timepoint = item.timepoint,
326 };
327 entry.record(item, index);
328 return entry;
329 }
330
331 pub fn record(self: *ThreadEntry, item: event.Event, index: usize) void {
332 self.last_index = index;
333 self.last_timepoint = item.timepoint;
334 self.event_count += 1;
335 switch (item.kind) {
336 .session_start => self.session_start_count += 1,
337 .session_end => self.session_end_count += 1,
338 .function_enter => self.function_enter_count += 1,
339 .function_exit => self.function_exit_count += 1,
340 .safepoint => self.safepoint_count += 1,
341 .boundary => self.boundary_count += 1,
342 .allocation => self.allocation_count += 1,
343 .free => self.free_count += 1,
344 .checkpoint => self.checkpoint_count += 1,
345 .checkpoint_restore => self.checkpoint_restore_count += 1,
346 .user => self.user_count += 1,
347 }
348 }
349
350 pub fn writeJsonLine(self: ThreadEntry, writer: *std.Io.Writer) !void {
351 var stream = pretty_json.Writer.init(writer, .minified);
352 const object = try stream.object();
353 try object.field("event", "thread");
354 try object.field("thread", self.thread_id);
355 try object.field("first_index", self.first_index);
356 try object.field("last_index", self.last_index);
357 try writeTimepointField(object, "first_timepoint", self.first_timepoint);
358 try writeTimepointField(object, "last_timepoint", self.last_timepoint);
359 try object.field("event_count", self.event_count);
360 try object.field("session_start_count", self.session_start_count);
361 try object.field("session_end_count", self.session_end_count);
362 try object.field("function_enter_count", self.function_enter_count);
363 try object.field("function_exit_count", self.function_exit_count);
364 try object.field("safepoint_count", self.safepoint_count);
365 try object.field("boundary_count", self.boundary_count);
366 try object.field("allocation_count", self.allocation_count);
367 try object.field("free_count", self.free_count);
368 try object.field("checkpoint_count", self.checkpoint_count);
369 try object.field("checkpoint_restore_count", self.checkpoint_restore_count);
370 try object.field("user_count", self.user_count);
371 try object.endLine();
372 }
373 };
374
375 pub const ObjectEntry = struct {
376 object_id: u64,
377 allocation_index: ?usize = null,
378 free_index: ?usize = null,
379 allocated_at: ?event.Timepoint = null,
380 freed_at: ?event.Timepoint = null,
381 size: u64 = 0,
382 alignment: u32 = 0,
383 label: ?[]const u8 = null,
384
385 pub fn fromAllocationAlloc(allocator: std.mem.Allocator, index: usize, item: event.Event) !ObjectEntry {
386 return .{
387 .object_id = item.object_id,
388 .allocation_index = index,
389 .allocated_at = item.timepoint,
390 .size = item.size,
391 .alignment = item.alignment,
392 .label = if (item.label) |label| try allocator.dupe(u8, label) else null,
393 };
394 }
395
396 pub fn fromFree(index: usize, item: event.Event) ObjectEntry {
397 return .{
398 .object_id = item.object_id,
399 .free_index = index,
400 .freed_at = item.timepoint,
401 };
402 }
403
404 pub fn status(self: ObjectEntry) []const u8 {
405 if (self.allocated_at == null) return "free_without_allocation";
406 if (self.freed_at != null) return "freed";
407 return "live";
408 }
409
410 pub fn deinit(self: *ObjectEntry, allocator: std.mem.Allocator) void {
411 if (self.label) |label| allocator.free(label);
412 self.* = undefined;
413 }
414
415 pub fn writeJsonLine(self: ObjectEntry, writer: *std.Io.Writer) !void {
416 var stream = pretty_json.Writer.init(writer, .minified);
417 const object = try stream.object();
418 try object.field("event", "object");
419 try object.field("object_id", self.object_id);
420 try object.field("status", self.status());
421 try object.field("allocation_index", self.allocation_index);
422 try object.field("free_index", self.free_index);
423 try writeOptionalTimepointField(object, "allocated_at", self.allocated_at);
424 try writeOptionalTimepointField(object, "freed_at", self.freed_at);
425 try object.field("size", self.size);
426 try object.field("alignment", self.alignment);
427 if (self.label) |label| try object.field("label", label);
428 try object.endLine();
429 }
430 };
431
432 pub fn writeTimepointsJsonl(source: event.Source, writer: *std.Io.Writer) !void {
433 var index: usize = 0;
434 while (try source.peek()) |item| {
435 try TimepointEntry.fromEvent(index, item.*).writeJsonLine(writer);
436 source.advance();
437 index += 1;
438 }
439 }
440
441 pub fn writeUserEventsJsonl(
442 allocator: std.mem.Allocator,
443 source: event.Source,
444 filter: UserEventFilter,
445 writer: *std.Io.Writer,
446 ) !void {
447 var index: usize = 0;
448 while (try source.peek()) |item| {
449 if (item.kind == .user and try userEventMatches(allocator, item.*, filter)) {
450 try UserEventEntry.fromEvent(index, item.*).writeJsonLine(allocator, writer);
451 }
452 source.advance();
453 index += 1;
454 }
455 }
456
457 pub fn writeUserEventStatsJsonl(
458 allocator: std.mem.Allocator,
459 source: event.Source,
460 filter: UserEventFilter,
461 group_field: []const u8,
462 metric_field: []const u8,
463 sort: UserEventStatSort,
464 writer: *std.Io.Writer,
465 ) !void {
466 var stats: std.ArrayListUnmanaged(UserEventStatEntry) = .empty;
467 defer {
468 for (stats.items) |*entry| entry.deinit(allocator);
469 stats.deinit(allocator);
470 }
471
472 var index: usize = 0;
473 while (try source.peek()) |item| {
474 if (item.kind == .user and try userEventMatches(allocator, item.*, filter)) stats: {
475 const data = item.data orelse break :stats;
476 const parsed = std.json.parseFromSlice(std.json.Value, allocator, data, .{}) catch break :stats;
477 defer parsed.deinit();
478 const object = switch (parsed.value) {
479 .object => |object| object,
480 else => break :stats,
481 };
482 const group = object.get(group_field) orelse break :stats;
483 const metric = object.get(metric_field) orelse break :stats;
484 const metric_value = payloadJsonValueToUnsigned(metric) orelse break :stats;
485 const group_text = try payloadJsonValueToTextAlloc(allocator, group);
486 defer allocator.free(group_text);
487 const stat_index = try userEventStatIndex(allocator, &stats, group_text);
488 stats.items[stat_index].record(index, item.timepoint, metric_value);
489 }
490 source.advance();
491 index += 1;
492 }
493
494 std.mem.sort(UserEventStatEntry, stats.items, sort, userEventStatGreater);
495 for (stats.items) |entry| {
496 try entry.writeJsonLine(group_field, metric_field, sort, writer);
497 }
498 }
499
500 pub fn writeSummaryJsonl(
501 allocator: std.mem.Allocator,
502 source: event.Source,
503 writer: *std.Io.Writer,
504 ) !void {
505 var summary = SummaryEntry{};
506 var thread_ids: std.AutoHashMapUnmanaged(event.ThreadId, void) = .empty;
507 defer thread_ids.deinit(allocator);
508 var live_objects: std.AutoHashMapUnmanaged(u64, u64) = .empty;
509 defer live_objects.deinit(allocator);
510
511 while (try source.peek()) |item| {
512 summary.record(item.*);
513 try thread_ids.put(allocator, item.timepoint.thread_id, {});
514 switch (item.kind) {
515 .allocation => {
516 const result = try live_objects.getOrPut(allocator, item.object_id);
517 if (result.found_existing) return error.DuplicateLiveObject;
518 result.value_ptr.* = item.size;
519 summary.object_count += 1;
520 summary.live_bytes += item.size;
521 },
522 .free => if (live_objects.fetchRemove(item.object_id)) |removed| {
523 summary.freed_object_count += 1;
524 summary.live_bytes -= removed.value;
525 } else {
526 summary.object_count += 1;
527 summary.free_without_allocation_count += 1;
528 },
529 else => {},
530 }
531 source.advance();
532 }
533
534 summary.thread_count = thread_ids.count();
535 summary.live_object_count = live_objects.count();
536 try summary.writeJsonLine(writer);
537 }
538
539 pub fn writeThreadsJsonl(
540 allocator: std.mem.Allocator,
541 source: event.Source,
542 writer: *std.Io.Writer,
543 ) !void {
544 var threads: std.ArrayListUnmanaged(ThreadEntry) = .empty;
545 defer threads.deinit(allocator);
546
547 var index: usize = 0;
548 while (try source.peek()) |item| {
549 if (findThread(threads.items, item.timepoint.thread_id)) |thread_index| {
550 threads.items[thread_index].record(item.*, index);
551 } else {
552 try threads.append(allocator, ThreadEntry.init(index, item.*));
553 }
554 source.advance();
555 index += 1;
556 }
557
558 for (threads.items) |item| {
559 try item.writeJsonLine(writer);
560 }
561 }
562
563 pub fn writeObjectsJsonl(
564 allocator: std.mem.Allocator,
565 source: event.Source,
566 writer: *std.Io.Writer,
567 ) !void {
568 var live: std.AutoHashMapUnmanaged(u64, ObjectEntry) = .empty;
569 defer {
570 var iterator = live.valueIterator();
571 while (iterator.next()) |item| item.deinit(allocator);
572 live.deinit(allocator);
573 }
574
575 var index: usize = 0;
576 while (try source.peek()) |item| {
577 switch (item.kind) {
578 .allocation => {
579 var owned = try ObjectEntry.fromAllocationAlloc(allocator, index, item.*);
580 var inserted = false;
581 errdefer if (!inserted) owned.deinit(allocator);
582 const result = try live.getOrPut(allocator, item.object_id);
583 if (result.found_existing) return error.DuplicateLiveObject;
584 result.value_ptr.* = owned;
585 inserted = true;
586 },
587 .free => if (live.fetchRemove(item.object_id)) |removed| {
588 var completed = removed.value;
589 defer completed.deinit(allocator);
590 completed.free_index = index;
591 completed.freed_at = item.timepoint;
592 try completed.writeJsonLine(writer);
593 } else {
594 try ObjectEntry.fromFree(index, item.*).writeJsonLine(writer);
595 },
596 else => {},
597 }
598 source.advance();
599 index += 1;
600 }
601
602 var iterator = live.valueIterator();
603 while (iterator.next()) |item| try item.writeJsonLine(writer);
604 }
605
606 pub fn nearestCheckpointTimepoint(
607 source: event.Source,
608 target: event.Timepoint,
609 ) !?event.Timepoint {
610 var result: ?event.Timepoint = null;
611 while (try source.peek()) |item| {
612 if (item.kind == .checkpoint and item.timepoint.beforeOrEqual(target) and
613 (result == null or result.?.beforeOrEqual(item.timepoint)))
614 {
615 result = item.timepoint;
616 }
617 source.advance();
618 }
619 return result;
620 }
621
622 fn findThread(entries: []const ThreadEntry, thread_id: event.ThreadId) ?usize {
623 for (entries, 0..) |entry, index| {
624 if (entry.thread_id == thread_id) return index;
625 }
626 return null;
627 }
628
629 fn userEventMatches(
630 allocator: std.mem.Allocator,
631 item: event.Event,
632 filter: UserEventFilter,
633 ) !bool {
634 if (filter.label) |expected| {
635 const actual = item.label orelse return false;
636 if (!std.mem.eql(u8, actual, expected)) return false;
637 }
638 if (filter.protocol) |expected| {
639 const protocol = try userPayloadProtocolAlloc(allocator, item.data) orelse return false;
640 defer allocator.free(protocol);
641 if (!std.mem.eql(u8, protocol, expected)) return false;
642 }
643 for (filter.payload_filters) |payload_filter| {
644 if (!try userPayloadFieldMatches(allocator, item.data, payload_filter.field, payload_filter.value)) return false;
645 }
646 return true;
647 }
648
649 fn userPayloadFieldMatches(
650 allocator: std.mem.Allocator,
651 maybe_data: ?[]const u8,
652 field: []const u8,
653 expected: []const u8,
654 ) !bool {
655 const data = maybe_data orelse return false;
656 const parsed = std.json.parseFromSlice(std.json.Value, allocator, data, .{}) catch return false;
657 defer parsed.deinit();
658 const object = switch (parsed.value) {
659 .object => |object| object,
660 else => return false,
661 };
662 const actual = object.get(field) orelse return false;
663 return payloadJsonValueMatchesText(actual, expected);
664 }
665
666 fn payloadJsonValueMatchesText(value: std.json.Value, expected: []const u8) bool {
667 return switch (value) {
668 .string => |actual| std.mem.eql(u8, actual, expected),
669 .integer => |actual| blk: {
670 const parsed = std.fmt.parseInt(i64, expected, 0) catch break :blk false;
671 break :blk actual == parsed;
672 },
673 .float => |actual| blk: {
674 const parsed = std.fmt.parseFloat(f64, expected) catch break :blk false;
675 break :blk actual == parsed;
676 },
677 .number_string => |actual| std.mem.eql(u8, actual, expected),
678 .bool => |actual| (actual and std.mem.eql(u8, expected, "true")) or
679 (!actual and std.mem.eql(u8, expected, "false")),
680 .null => std.mem.eql(u8, expected, "null"),
681 .array,
682 .object,
683 => false,
684 };
685 }
686
687 fn payloadJsonValueToUnsigned(value: std.json.Value) ?u64 {
688 return switch (value) {
689 .integer => |actual| if (actual >= 0) @intCast(actual) else null,
690 .float => |actual| if (actual >= 0 and @floor(actual) == actual) @intFromFloat(actual) else null,
691 .number_string => |actual| std.fmt.parseUnsigned(u64, actual, 0) catch null,
692 else => null,
693 };
694 }
695
696 fn payloadJsonValueToTextAlloc(
697 allocator: std.mem.Allocator,
698 value: std.json.Value,
699 ) ![]u8 {
700 return switch (value) {
701 .string => |actual| try allocator.dupe(u8, actual),
702 .integer => |actual| try std.fmt.allocPrint(allocator, "{d}", .{actual}),
703 .float => |actual| try std.fmt.allocPrint(allocator, "{d}", .{actual}),
704 .number_string => |actual| try allocator.dupe(u8, actual),
705 .bool => |actual| try allocator.dupe(u8, if (actual) "true" else "false"),
706 .null => try allocator.dupe(u8, "null"),
707 .array,
708 .object,
709 => error.UnsupportedPayloadField,
710 };
711 }
712
713 fn userEventStatIndex(
714 allocator: std.mem.Allocator,
715 stats: *std.ArrayListUnmanaged(UserEventStatEntry),
716 group: []const u8,
717 ) !usize {
718 for (stats.items, 0..) |entry, index| {
719 if (std.mem.eql(u8, entry.group, group)) return index;
720 }
721 const owned_group = try allocator.dupe(u8, group);
722 errdefer allocator.free(owned_group);
723 try stats.append(allocator, .{ .group = owned_group });
724 return stats.items.len - 1;
725 }
726
727 fn userEventStatGreater(sort: UserEventStatSort, left: UserEventStatEntry, right: UserEventStatEntry) bool {
728 if (sort == .group) return std.mem.lessThan(u8, left.group, right.group);
729 const left_value = left.sortValue(sort);
730 const right_value = right.sortValue(sort);
731 if (left_value != right_value) return left_value > right_value;
732 if (left.total != right.total) return left.total > right.total;
733 if (left.max != right.max) return left.max > right.max;
734 return std.mem.lessThan(u8, left.group, right.group);
735 }
736
737 fn userPayloadProtocolAlloc(
738 allocator: std.mem.Allocator,
739 maybe_data: ?[]const u8,
740 ) !?[]u8 {
741 const data = maybe_data orelse return null;
742 const parsed = std.json.parseFromSlice(std.json.Value, allocator, data, .{}) catch return null;
743 defer parsed.deinit();
744 const object = switch (parsed.value) {
745 .object => |object| object,
746 else => return null,
747 };
748 const protocol = switch (object.get("protocol") orelse return null) {
749 .string => |text| text,
750 else => return null,
751 };
752 return try allocator.dupe(u8, protocol);
753 }
754
755 fn payloadIsJson(allocator: std.mem.Allocator, bytes: []const u8) !bool {
756 const parsed = std.json.parseFromSlice(std.json.Value, allocator, bytes, .{}) catch return false;
757 parsed.deinit();
758 return true;
759 }
760
761 pub fn framesAtAlloc(
762 allocator: std.mem.Allocator,
763 source: event.Source,
764 target: event.Timepoint,
765 ) ![]Frame {
766 var stack: std.ArrayListUnmanaged(Frame) = .empty;
767 defer stack.deinit(allocator);
768
769 while (try source.peek()) |item| {
770 if (item.timepoint.thread_id == target.thread_id and item.timepoint.beforeOrEqual(target)) {
771 switch (item.kind) {
772 .function_enter => try pushFrame(allocator, &stack, item.*, false),
773 .safepoint => try recordSafepoint(allocator, &stack, item.*),
774 .function_exit => try popFrame(&stack, item.*),
775 else => {},
776 }
777 }
778 source.advance();
779 }
780
781 const frames = try allocator.alloc(Frame, stack.items.len);
782 for (frames, 0..) |*frame, depth| {
783 frame.* = stack.items[stack.items.len - depth - 1];
784 frame.depth = depth;
785 }
786 return frames;
787 }
788
789 fn pushFrame(
790 allocator: std.mem.Allocator,
791 stack: *std.ArrayListUnmanaged(Frame),
792 item: event.Event,
793 synthetic: bool,
794 ) !void {
795 try stack.append(allocator, .{
796 .function_id = item.function_id,
797 .site_id = item.site_id,
798 .stack_map_id = item.stack_map_id,
799 .entered_at = item.timepoint,
800 .last_timepoint = item.timepoint,
801 .synthetic = synthetic,
802 });
803 }
804
805 fn recordSafepoint(
806 allocator: std.mem.Allocator,
807 stack: *std.ArrayListUnmanaged(Frame),
808 item: event.Event,
809 ) !void {
810 if (stack.items.len == 0) {
811 try pushFrame(allocator, stack, item, true);
812 return;
813 }
814
815 const top = &stack.items[stack.items.len - 1];
816 if (top.function_id != item.function_id) return error.TraceFrameMismatch;
817 top.site_id = item.site_id;
818 top.stack_map_id = item.stack_map_id;
819 top.last_timepoint = item.timepoint;
820 }
821
822 fn popFrame(
823 stack: *std.ArrayListUnmanaged(Frame),
824 item: event.Event,
825 ) !void {
826 if (stack.items.len == 0) return error.TraceFrameMismatch;
827 const top = stack.items[stack.items.len - 1];
828 if (top.function_id != item.function_id) return error.TraceFrameMismatch;
829 _ = stack.pop();
830 }
831
832 fn writeTimepointField(object: pretty_json.Object, name: []const u8, value: event.Timepoint) !void {
833 try object.formattedString(
834 name,
835 timepoint_string_capacity,
836 "{d}:{d}:{d}",
837 .{ value.epoch, value.thread_id, value.seq },
838 );
839 }
840
841 fn writeOptionalTimepointField(
842 object: pretty_json.Object,
843 name: []const u8,
844 value: ?event.Timepoint,
845 ) !void {
846 if (value) |actual| {
847 try writeTimepointField(object, name, actual);
848 } else {
849 try object.field(name, null);
850 }
851 }
852
853 const SliceSource = struct {
854 events: []const event.Event,
855 cursor: usize = 0,
856
857 fn source(self: *SliceSource) event.Source {
858 return .{ .context = self, .peekFn = peek, .advanceFn = advance, .countFn = count };
859 }
860
861 fn peek(context: *anyopaque) !?*const event.Event {
862 const self: *SliceSource = @ptrCast(@alignCast(context));
863 if (self.cursor == self.events.len) return null;
864 return &self.events[self.cursor];
865 }
866
867 fn advance(context: *anyopaque) void {
868 const self: *SliceSource = @ptrCast(@alignCast(context));
869 self.cursor += 1;
870 }
871
872 fn count(context: *anyopaque) u64 {
873 const self: *SliceSource = @ptrCast(@alignCast(context));
874 return self.events.len;
875 }
876 };
877
878 fn writeTimepointsFromSlice(events: []const event.Event, writer: *std.Io.Writer) !void {
879 var stream = SliceSource{ .events = events };
880 try writeTimepointsJsonl(stream.source(), writer);
881 }
882
883 fn writeUserEventsFromSlice(
884 allocator: std.mem.Allocator,
885 events: []const event.Event,
886 filter: UserEventFilter,
887 writer: *std.Io.Writer,
888 ) !void {
889 var stream = SliceSource{ .events = events };
890 try writeUserEventsJsonl(allocator, stream.source(), filter, writer);
891 }
892
893 fn writeUserEventStatsFromSlice(
894 allocator: std.mem.Allocator,
895 events: []const event.Event,
896 filter: UserEventFilter,
897 group_field: []const u8,
898 metric_field: []const u8,
899 sort: UserEventStatSort,
900 writer: *std.Io.Writer,
901 ) !void {
902 var stream = SliceSource{ .events = events };
903 try writeUserEventStatsJsonl(allocator, stream.source(), filter, group_field, metric_field, sort, writer);
904 }
905
906 fn writeSummaryFromSlice(allocator: std.mem.Allocator, events: []const event.Event, writer: *std.Io.Writer) !void {
907 var stream = SliceSource{ .events = events };
908 try writeSummaryJsonl(allocator, stream.source(), writer);
909 }
910
911 fn writeThreadsFromSlice(allocator: std.mem.Allocator, events: []const event.Event, writer: *std.Io.Writer) !void {
912 var stream = SliceSource{ .events = events };
913 try writeThreadsJsonl(allocator, stream.source(), writer);
914 }
915
916 fn writeObjectsFromSlice(allocator: std.mem.Allocator, events: []const event.Event, writer: *std.Io.Writer) !void {
917 var stream = SliceSource{ .events = events };
918 try writeObjectsJsonl(allocator, stream.source(), writer);
919 }
920
921 fn nearestCheckpointFromSlice(events: []const event.Event, target: event.Timepoint) !?event.Timepoint {
922 var stream = SliceSource{ .events = events };
923 return try nearestCheckpointTimepoint(stream.source(), target);
924 }
925
926 fn framesFromSlice(allocator: std.mem.Allocator, events: []const event.Event, target: event.Timepoint) ![]Frame {
927 var stream = SliceSource{ .events = events };
928 return try framesAtAlloc(allocator, stream.source(), target);
929 }
930
931 test "checkpoint lookup returns nearest predecessor" {
932 const events = [_]event.Event{
933 event.Event.checkpoint(.{ .thread_id = 1, .seq = 2 }, "a", "state-a"),
934 event.Event.user(.{ .thread_id = 1, .seq = 3 }, "note", "ignored"),
935 event.Event.checkpoint(.{ .thread_id = 1, .seq = 8 }, "b", "state-b"),
936 };
937
938 const found = (try nearestCheckpointFromSlice(&events, .{ .thread_id = 1, .seq = 9 })) orelse
939 return error.MissingCheckpoint;
940 try std.testing.expectEqual(@as(u64, 8), found.seq);
941 }
942
943 test "timepoint query writes compact navigation jsonl" {
944 const events = [_]event.Event{
945 event.Event.sessionStart(.{ .thread_id = 1, .seq = 1 }, "run"),
946 event.Event.safepointReached(.{ .thread_id = 1, .seq = 2 }, .{ .function_id = 20, .site_id = 5 }),
947 event.Event.user(.{ .thread_id = 1, .seq = 3 }, "note", "ignored"),
948 };
949
950 var out = std.Io.Writer.Allocating.init(std.testing.allocator);
951 defer out.deinit();
952 try writeTimepointsFromSlice(&events, &out.writer);
953 const bytes = try out.toOwnedSlice();
954 defer std.testing.allocator.free(bytes);
955
956 try std.testing.expect(std.mem.indexOf(u8, bytes, "\"event\":\"timepoint\"") != null);
957 try std.testing.expect(std.mem.indexOf(u8, bytes, "\"timepoint\":\"0:1:2\"") != null);
958 try std.testing.expect(std.mem.indexOf(u8, bytes, "\"kind\":\"safepoint\"") != null);
959 try std.testing.expect(std.mem.indexOf(u8, bytes, "\"label\":\"run\"") != null);
960 }
961
962 test "user event query filters labels and payload protocols" {
963 const events = [_]event.Event{
964 event.Event.user(
965 .{ .thread_id = 1, .seq = 1 },
966 "compiler.query.miss",
967 "{\"protocol\":\"tiny.compiler.query/v1\",\"phase\":\"analysis\",\"product_kind\":\"function_summary\",\"product_identity\":7}",
968 ),
969 event.Event.user(.{ .thread_id = 1, .seq = 2 }, "process.argv", "tiny\x00compile"),
970 event.Event.user(
971 .{ .thread_id = 2, .seq = 3 },
972 "compiler.query.hit",
973 "{\"protocol\":\"tiny.compiler.query/v1\",\"phase\":\"source_choir\",\"product_kind\":\"source_choir_module\",\"product_identity\":8}",
974 ),
975 };
976
977 var out = std.Io.Writer.Allocating.init(std.testing.allocator);
978 defer out.deinit();
979 try writeUserEventsFromSlice(
980 std.testing.allocator,
981 &events,
982 .{ .protocol = "tiny.compiler.query/v1" },
983 &out.writer,
984 );
985 const bytes = try out.toOwnedSlice();
986 defer std.testing.allocator.free(bytes);
987
988 try std.testing.expect(std.mem.indexOf(u8, bytes, "\"event\":\"user\"") != null);
989 try std.testing.expect(std.mem.indexOf(u8, bytes, "\"label\":\"compiler.query.miss\"") != null);
990 try std.testing.expect(std.mem.indexOf(u8, bytes, "\"label\":\"compiler.query.hit\"") != null);
991 try std.testing.expect(std.mem.indexOf(u8, bytes, "\"label\":\"process.argv\"") == null);
992 try std.testing.expect(std.mem.indexOf(u8, bytes, "\"protocol\":\"tiny.compiler.query/v1\"") != null);
993 try std.testing.expect(std.mem.indexOf(u8, bytes, "\"payload\":{\"protocol\":\"tiny.compiler.query/v1\",\"phase\":\"analysis\"") != null);
994
995 var label_out = std.Io.Writer.Allocating.init(std.testing.allocator);
996 defer label_out.deinit();
997 try writeUserEventsFromSlice(
998 std.testing.allocator,
999 &events,
1000 .{ .label = "compiler.query.hit" },
1001 &label_out.writer,
1002 );
1003 const label_bytes = try label_out.toOwnedSlice();
1004 defer std.testing.allocator.free(label_bytes);
1005
1006 try std.testing.expect(std.mem.indexOf(u8, label_bytes, "\"label\":\"compiler.query.hit\"") != null);
1007 try std.testing.expect(std.mem.indexOf(u8, label_bytes, "\"label\":\"compiler.query.miss\"") == null);
1008
1009 var payload_out = std.Io.Writer.Allocating.init(std.testing.allocator);
1010 defer payload_out.deinit();
1011 const product_kind_filters = [_]UserPayloadFilter{.{
1012 .field = "product_kind",
1013 .value = "function_summary",
1014 }};
1015 try writeUserEventsFromSlice(
1016 std.testing.allocator,
1017 &events,
1018 .{
1019 .protocol = "tiny.compiler.query/v1",
1020 .payload_filters = &product_kind_filters,
1021 },
1022 &payload_out.writer,
1023 );
1024 const payload_bytes = try payload_out.toOwnedSlice();
1025 defer std.testing.allocator.free(payload_bytes);
1026
1027 try std.testing.expect(std.mem.indexOf(u8, payload_bytes, "\"product_kind\":\"function_summary\"") != null);
1028 try std.testing.expect(std.mem.indexOf(u8, payload_bytes, "\"product_kind\":\"source_choir_module\"") == null);
1029
1030 var identity_out = std.Io.Writer.Allocating.init(std.testing.allocator);
1031 defer identity_out.deinit();
1032 const identity_filters = [_]UserPayloadFilter{.{
1033 .field = "product_identity",
1034 .value = "8",
1035 }};
1036 try writeUserEventsFromSlice(
1037 std.testing.allocator,
1038 &events,
1039 .{ .payload_filters = &identity_filters },
1040 &identity_out.writer,
1041 );
1042 const identity_bytes = try identity_out.toOwnedSlice();
1043 defer std.testing.allocator.free(identity_bytes);
1044
1045 try std.testing.expect(std.mem.indexOf(u8, identity_bytes, "\"product_identity\":8") != null);
1046 try std.testing.expect(std.mem.indexOf(u8, identity_bytes, "\"product_identity\":7") == null);
1047
1048 var combined_out = std.Io.Writer.Allocating.init(std.testing.allocator);
1049 defer combined_out.deinit();
1050 const combined_filters = [_]UserPayloadFilter{
1051 .{ .field = "product_kind", .value = "source_choir_module" },
1052 .{ .field = "product_identity", .value = "8" },
1053 };
1054 try writeUserEventsFromSlice(
1055 std.testing.allocator,
1056 &events,
1057 .{
1058 .protocol = "tiny.compiler.query/v1",
1059 .payload_filters = &combined_filters,
1060 },
1061 &combined_out.writer,
1062 );
1063 const combined_bytes = try combined_out.toOwnedSlice();
1064 defer std.testing.allocator.free(combined_bytes);
1065
1066 try std.testing.expect(std.mem.indexOf(u8, combined_bytes, "\"product_kind\":\"source_choir_module\"") != null);
1067 try std.testing.expect(std.mem.indexOf(u8, combined_bytes, "\"product_kind\":\"function_summary\"") == null);
1068 try std.testing.expect(std.mem.indexOf(u8, combined_bytes, "\"product_identity\":8") != null);
1069 }
1070
1071 test "user event stats groups by payload and sums numeric metrics" {
1072 const events = [_]event.Event{
1073 event.Event.user(
1074 .{ .thread_id = 1, .seq = 1 },
1075 "compiler.query.finish",
1076 "{\"protocol\":\"tiny.compiler.query/v1\",\"product_kind\":\"function_summary\",\"elapsed_ns\":7}",
1077 ),
1078 event.Event.user(
1079 .{ .thread_id = 1, .seq = 2 },
1080 "compiler.query.finish",
1081 "{\"protocol\":\"tiny.compiler.query/v1\",\"product_kind\":\"partial_evaluation\",\"elapsed_ns\":11}",
1082 ),
1083 event.Event.user(
1084 .{ .thread_id = 1, .seq = 3 },
1085 "compiler.query.finish",
1086 "{\"protocol\":\"tiny.compiler.query/v1\",\"product_kind\":\"function_summary\",\"elapsed_ns\":13}",
1087 ),
1088 event.Event.user(.{ .thread_id = 1, .seq = 4 }, "process.argv", "tiny\x00compile"),
1089 };
1090
1091 var out = std.Io.Writer.Allocating.init(std.testing.allocator);
1092 defer out.deinit();
1093 try writeUserEventStatsFromSlice(
1094 std.testing.allocator,
1095 &events,
1096 .{
1097 .label = "compiler.query.finish",
1098 .protocol = "tiny.compiler.query/v1",
1099 },
1100 "product_kind",
1101 "elapsed_ns",
1102 .total,
1103 &out.writer,
1104 );
1105 const bytes = try out.toOwnedSlice();
1106 defer std.testing.allocator.free(bytes);
1107
1108 const function_index = std.mem.indexOf(u8, bytes, "\"group\":\"function_summary\"") orelse
1109 return error.TestExpectedResult;
1110 const partial_index = std.mem.indexOf(u8, bytes, "\"group\":\"partial_evaluation\"") orelse
1111 return error.TestExpectedResult;
1112 try std.testing.expect(function_index < partial_index);
1113 try std.testing.expect(std.mem.indexOf(u8, bytes, "\"count\":2") != null);
1114 try std.testing.expect(std.mem.indexOf(u8, bytes, "\"sort_field\":\"total\"") != null);
1115 try std.testing.expect(std.mem.indexOf(u8, bytes, "\"first_index\":0") != null);
1116 try std.testing.expect(std.mem.indexOf(u8, bytes, "\"last_index\":2") != null);
1117 try std.testing.expect(std.mem.indexOf(u8, bytes, "\"first_timepoint\":\"0:1:1\"") != null);
1118 try std.testing.expect(std.mem.indexOf(u8, bytes, "\"last_timepoint\":\"0:1:3\"") != null);
1119 try std.testing.expect(std.mem.indexOf(u8, bytes, "\"total\":20") != null);
1120 try std.testing.expect(std.mem.indexOf(u8, bytes, "\"min\":7") != null);
1121 try std.testing.expect(std.mem.indexOf(u8, bytes, "\"max\":13") != null);
1122 try std.testing.expect(std.mem.indexOf(u8, bytes, "\"mean\":10") != null);
1123 try std.testing.expect(std.mem.indexOf(u8, bytes, "\"total\":11") != null);
1124 try std.testing.expect(std.mem.indexOf(u8, bytes, "\"mean\":11") != null);
1125 }
1126
1127 test "user event stats can sort by mean metric" {
1128 const events = [_]event.Event{
1129 event.Event.user(
1130 .{ .thread_id = 1, .seq = 1 },
1131 "compiler.query.finish",
1132 "{\"protocol\":\"tiny.compiler.query/v1\",\"product_kind\":\"function_summary\",\"elapsed_ns\":7}",
1133 ),
1134 event.Event.user(
1135 .{ .thread_id = 1, .seq = 2 },
1136 "compiler.query.finish",
1137 "{\"protocol\":\"tiny.compiler.query/v1\",\"product_kind\":\"partial_evaluation\",\"elapsed_ns\":11}",
1138 ),
1139 event.Event.user(
1140 .{ .thread_id = 1, .seq = 3 },
1141 "compiler.query.finish",
1142 "{\"protocol\":\"tiny.compiler.query/v1\",\"product_kind\":\"function_summary\",\"elapsed_ns\":13}",
1143 ),
1144 };
1145
1146 var out = std.Io.Writer.Allocating.init(std.testing.allocator);
1147 defer out.deinit();
1148 try writeUserEventStatsFromSlice(
1149 std.testing.allocator,
1150 &events,
1151 .{
1152 .label = "compiler.query.finish",
1153 .protocol = "tiny.compiler.query/v1",
1154 },
1155 "product_kind",
1156 "elapsed_ns",
1157 .mean,
1158 &out.writer,
1159 );
1160 const bytes = try out.toOwnedSlice();
1161 defer std.testing.allocator.free(bytes);
1162
1163 const partial_index = std.mem.indexOf(u8, bytes, "\"group\":\"partial_evaluation\"") orelse
1164 return error.TestExpectedResult;
1165 const function_index = std.mem.indexOf(u8, bytes, "\"group\":\"function_summary\"") orelse
1166 return error.TestExpectedResult;
1167 try std.testing.expect(partial_index < function_index);
1168 try std.testing.expect(std.mem.indexOf(u8, bytes, "\"sort_field\":\"mean\"") != null);
1169 }
1170
1171 test "summary query writes trace-level totals as jsonl" {
1172 const events = [_]event.Event{
1173 event.Event.sessionStart(.{ .thread_id = 1, .seq = 1 }, "run"),
1174 event.Event.allocation(.{ .thread_id = 1, .seq = 2 }, 7, 64, 8, "buffer"),
1175 event.Event.boundaryBytes(.{ .thread_id = 2, .seq = 3 }, "env.TEST", "value"),
1176 event.Event.free(.{ .thread_id = 1, .seq = 4 }, 7),
1177 event.Event.free(.{ .thread_id = 1, .seq = 5 }, 99),
1178 event.Event.sessionEnd(.{ .thread_id = 2, .seq = 6 }, 0),
1179 };
1180
1181 var out = std.Io.Writer.Allocating.init(std.testing.allocator);
1182 defer out.deinit();
1183 try writeSummaryFromSlice(std.testing.allocator, &events, &out.writer);
1184 const bytes = try out.toOwnedSlice();
1185 defer std.testing.allocator.free(bytes);
1186
1187 try std.testing.expect(std.mem.indexOf(u8, bytes, "\"event\":\"summary\"") != null);
1188 try std.testing.expect(std.mem.indexOf(u8, bytes, "\"event_count\":6") != null);
1189 try std.testing.expect(std.mem.indexOf(u8, bytes, "\"first_timepoint\":\"0:1:1\"") != null);
1190 try std.testing.expect(std.mem.indexOf(u8, bytes, "\"last_timepoint\":\"0:2:6\"") != null);
1191 try std.testing.expect(std.mem.indexOf(u8, bytes, "\"thread_count\":2") != null);
1192 try std.testing.expect(std.mem.indexOf(u8, bytes, "\"session_status\":0") != null);
1193 try std.testing.expect(std.mem.indexOf(u8, bytes, "\"boundary_count\":1") != null);
1194 try std.testing.expect(std.mem.indexOf(u8, bytes, "\"object_count\":2") != null);
1195 try std.testing.expect(std.mem.indexOf(u8, bytes, "\"freed_object_count\":1") != null);
1196 try std.testing.expect(std.mem.indexOf(u8, bytes, "\"free_without_allocation_count\":1") != null);
1197 try std.testing.expect(std.mem.indexOf(u8, bytes, "\"allocated_bytes\":64") != null);
1198 try std.testing.expect(std.mem.indexOf(u8, bytes, "\"live_bytes\":0") != null);
1199 }
1200
1201 test "thread query writes per-thread event ranges and counts" {
1202 const events = [_]event.Event{
1203 event.Event.sessionStart(.{ .thread_id = 1, .seq = 1 }, "run"),
1204 event.Event.functionEnter(.{ .thread_id = 1, .seq = 2 }, .{ .function_id = 20, .site_id = 1 }),
1205 event.Event.safepointReached(.{ .thread_id = 2, .seq = 3 }, .{ .function_id = 30, .site_id = 5 }),
1206 event.Event.boundaryBytes(.{ .thread_id = 1, .seq = 4 }, "env.TEST", "value"),
1207 event.Event.sessionEnd(.{ .thread_id = 2, .seq = 5 }, 0),
1208 };
1209
1210 var out = std.Io.Writer.Allocating.init(std.testing.allocator);
1211 defer out.deinit();
1212 try writeThreadsFromSlice(std.testing.allocator, &events, &out.writer);
1213 const bytes = try out.toOwnedSlice();
1214 defer std.testing.allocator.free(bytes);
1215
1216 try std.testing.expect(std.mem.indexOf(u8, bytes, "\"event\":\"thread\"") != null);
1217 try std.testing.expect(std.mem.indexOf(u8, bytes, "\"thread\":1") != null);
1218 try std.testing.expect(std.mem.indexOf(u8, bytes, "\"first_timepoint\":\"0:1:1\"") != null);
1219 try std.testing.expect(std.mem.indexOf(u8, bytes, "\"last_timepoint\":\"0:1:4\"") != null);
1220 try std.testing.expect(std.mem.indexOf(u8, bytes, "\"event_count\":3") != null);
1221 try std.testing.expect(std.mem.indexOf(u8, bytes, "\"function_enter_count\":1") != null);
1222 try std.testing.expect(std.mem.indexOf(u8, bytes, "\"boundary_count\":1") != null);
1223 try std.testing.expect(std.mem.indexOf(u8, bytes, "\"thread\":2") != null);
1224 try std.testing.expect(std.mem.indexOf(u8, bytes, "\"safepoint_count\":1") != null);
1225 try std.testing.expect(std.mem.indexOf(u8, bytes, "\"session_end_count\":1") != null);
1226 }
1227
1228 test "object query writes allocation lifetimes as jsonl" {
1229 const events = [_]event.Event{
1230 event.Event.allocation(.{ .thread_id = 1, .seq = 1 }, 7, 64, 8, "buffer"),
1231 event.Event.safepointReached(.{ .thread_id = 1, .seq = 2 }, .{ .function_id = 20, .site_id = 5 }),
1232 event.Event.free(.{ .thread_id = 1, .seq = 3 }, 7),
1233 event.Event.allocation(.{ .thread_id = 1, .seq = 4 }, 8, 16, 4, null),
1234 event.Event.free(.{ .thread_id = 1, .seq = 5 }, 99),
1235 };
1236
1237 var out = std.Io.Writer.Allocating.init(std.testing.allocator);
1238 defer out.deinit();
1239 try writeObjectsFromSlice(std.testing.allocator, &events, &out.writer);
1240 const bytes = try out.toOwnedSlice();
1241 defer std.testing.allocator.free(bytes);
1242
1243 try std.testing.expect(std.mem.indexOf(u8, bytes, "\"event\":\"object\"") != null);
1244 try std.testing.expect(std.mem.indexOf(u8, bytes, "\"object_id\":7") != null);
1245 try std.testing.expect(std.mem.indexOf(u8, bytes, "\"status\":\"freed\"") != null);
1246 try std.testing.expect(std.mem.indexOf(u8, bytes, "\"allocated_at\":\"0:1:1\"") != null);
1247 try std.testing.expect(std.mem.indexOf(u8, bytes, "\"freed_at\":\"0:1:3\"") != null);
1248 try std.testing.expect(std.mem.indexOf(u8, bytes, "\"label\":\"buffer\"") != null);
1249 try std.testing.expect(std.mem.indexOf(u8, bytes, "\"object_id\":8") != null);
1250 try std.testing.expect(std.mem.indexOf(u8, bytes, "\"status\":\"live\"") != null);
1251 try std.testing.expect(std.mem.indexOf(u8, bytes, "\"object_id\":99") != null);
1252 try std.testing.expect(std.mem.indexOf(u8, bytes, "\"status\":\"free_without_allocation\"") != null);
1253 }
1254
1255 test "frame query returns active stack at safepoint" {
1256 const events = [_]event.Event{
1257 event.Event.functionEnter(.{ .thread_id = 1, .seq = 1 }, .{ .function_id = 10, .site_id = 1 }),
1258 event.Event.functionEnter(.{ .thread_id = 1, .seq = 2 }, .{ .function_id = 20, .site_id = 1 }),
1259 event.Event.safepointReached(.{ .thread_id = 1, .seq = 3 }, .{ .function_id = 20, .site_id = 9, .stack_map_id = 7 }),
1260 event.Event.functionExit(.{ .thread_id = 1, .seq = 4 }, .{ .function_id = 20, .site_id = 10 }),
1261 };
1262
1263 const frames = try framesFromSlice(std.testing.allocator, &events, .{ .thread_id = 1, .seq = 3 });
1264 defer std.testing.allocator.free(frames);
1265
1266 try std.testing.expectEqual(@as(usize, 2), frames.len);
1267 try std.testing.expectEqual(@as(usize, 0), frames[0].depth);
1268 try std.testing.expectEqual(@as(u64, 20), frames[0].function_id);
1269 try std.testing.expectEqual(@as(u64, 9), frames[0].site_id);
1270 try std.testing.expectEqual(@as(u64, 7), frames[0].stack_map_id);
1271 try std.testing.expectEqual(@as(u64, 3), frames[0].last_timepoint.seq);
1272 try std.testing.expectEqual(@as(usize, 1), frames[1].depth);
1273 try std.testing.expectEqual(@as(u64, 10), frames[1].function_id);
1274 }
1275
1276 test "frame query drops exited frames by target timepoint" {
1277 const events = [_]event.Event{
1278 event.Event.functionEnter(.{ .thread_id = 1, .seq = 1 }, .{ .function_id = 10, .site_id = 1 }),
1279 event.Event.functionEnter(.{ .thread_id = 1, .seq = 2 }, .{ .function_id = 20, .site_id = 1 }),
1280 event.Event.functionExit(.{ .thread_id = 1, .seq = 3 }, .{ .function_id = 20, .site_id = 2 }),
1281 };
1282
1283 const frames = try framesFromSlice(std.testing.allocator, &events, .{ .thread_id = 1, .seq = 3 });
1284 defer std.testing.allocator.free(frames);
1285
1286 try std.testing.expectEqual(@as(usize, 1), frames.len);
1287 try std.testing.expectEqual(@as(u64, 10), frames[0].function_id);
1288 }
1289
1290 test "frame query represents safepoint-only traces" {
1291 const events = [_]event.Event{
1292 event.Event.safepointReached(.{ .thread_id = 1, .seq = 1 }, .{ .function_id = 20, .site_id = 5 }),
1293 };
1294
1295 const frames = try framesFromSlice(std.testing.allocator, &events, .{ .thread_id = 1, .seq = 1 });
1296 defer std.testing.allocator.free(frames);
1297
1298 try std.testing.expectEqual(@as(usize, 1), frames.len);
1299 try std.testing.expectEqual(@as(u64, 20), frames[0].function_id);
1300 try std.testing.expect(frames[0].synthetic);
1301 }
1302
1303 test "frame query ignores other threads" {
1304 const events = [_]event.Event{
1305 event.Event.functionEnter(.{ .thread_id = 1, .seq = 1 }, .{ .function_id = 10, .site_id = 1 }),
1306 event.Event.functionEnter(.{ .thread_id = 2, .seq = 2 }, .{ .function_id = 20, .site_id = 1 }),
1307 };
1308
1309 const frames = try framesFromSlice(std.testing.allocator, &events, .{ .thread_id = 1, .seq = 2 });
1310 defer std.testing.allocator.free(frames);
1311
1312 try std.testing.expectEqual(@as(usize, 1), frames.len);
1313 try std.testing.expectEqual(@as(u64, 10), frames[0].function_id);
1314 }