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 }