tiny.trace.replay
Defined in tiny.trace.
API (21)
Actions
Public operations.
UserEventEntry.fromEventUserEventEntry.writeJsonLineUserEventStatSort.parseframesAtAllocnearestCheckpointTimepointwriteObjectsJsonlwriteSummaryJsonlwriteThreadsJsonlwriteTimepointsJsonlwriteUserEventStatsJsonlwriteUserEventsJsonl
Types and contracts
Public types and contracts.
FrameObjectEntrySummaryEntryThreadEntryTimepointEntryUserEventEntryUserEventFilterUserEventStatEntryUserEventStatSortUserPayloadFilter
Source
Source: lib/trace/src/replay.zig
zig
const std = @import("std");const pretty_json = @import("pretty").json;const event = @import("event.zig");const timepoint_string_capacity = 54;pub const Frame = struct { depth: usize = 0, function_id: u64, site_id: u64, stack_map_id: u64, entered_at: event.Timepoint, last_timepoint: event.Timepoint, synthetic: bool = false, pub fn writeJsonLine(self: Frame, writer: *std.Io.Writer) !void { var stream = pretty_json.Writer.init(writer, .minified); const object = try stream.object(); try object.field("event", "frame"); try object.field("depth", self.depth); try object.field("function_id", self.function_id); try object.field("site_id", self.site_id); try object.field("stack_map_id", self.stack_map_id); try writeTimepointField(object, "entered_at", self.entered_at); try writeTimepointField(object, "last_timepoint", self.last_timepoint); try object.field("synthetic", self.synthetic); try object.endLine(); }};pub const TimepointEntry = struct { index: usize, timepoint: event.Timepoint, kind: event.EventKind, function_id: u64 = 0, site_id: u64 = 0, stack_map_id: u64 = 0, object_id: u64 = 0, label: ?[]const u8 = null, operation: ?[]const u8 = null, pub fn fromEvent(index: usize, item: event.Event) TimepointEntry { return .{ .index = index, .timepoint = item.timepoint, .kind = item.kind, .function_id = item.function_id, .site_id = item.site_id, .stack_map_id = item.stack_map_id, .object_id = item.object_id, .label = item.label, .operation = item.operation, }; } pub fn writeJsonLine(self: TimepointEntry, writer: *std.Io.Writer) !void { var stream = pretty_json.Writer.init(writer, .minified); const object = try stream.object(); try object.field("event", "timepoint"); try object.field("index", self.index); try writeTimepointField(object, "timepoint", self.timepoint); try object.field("epoch", self.timepoint.epoch); try object.field("thread", self.timepoint.thread_id); try object.field("seq", self.timepoint.seq); try object.field("kind", self.kind.tag()); try object.field("function_id", self.function_id); try object.field("site_id", self.site_id); try object.field("stack_map_id", self.stack_map_id); try object.field("object_id", self.object_id); if (self.label) |label| try object.field("label", label); if (self.operation) |operation| try object.field("operation", operation); try object.endLine(); }};pub const UserEventFilter = struct { label: ?[]const u8 = null, protocol: ?[]const u8 = null, payload_filters: []const UserPayloadFilter = &.{},};pub const UserPayloadFilter = struct { field: []const u8, value: []const u8,};pub const UserEventStatSort = enum { total, count, min, max, mean, group, pub fn parse(text: []const u8) ?UserEventStatSort { if (std.mem.eql(u8, text, "total")) return .total; if (std.mem.eql(u8, text, "count")) return .count; if (std.mem.eql(u8, text, "min")) return .min; if (std.mem.eql(u8, text, "max")) return .max; if (std.mem.eql(u8, text, "mean")) return .mean; if (std.mem.eql(u8, text, "group")) return .group; return null; }};pub const UserEventEntry = struct { index: usize, timepoint: event.Timepoint, label: ?[]const u8 = null, data: ?[]const u8 = null, pub fn fromEvent(index: usize, item: event.Event) UserEventEntry { return .{ .index = index, .timepoint = item.timepoint, .label = item.label, .data = item.data, }; } pub fn writeJsonLine( self: UserEventEntry, allocator: std.mem.Allocator, writer: *std.Io.Writer, ) !void { var stream = pretty_json.Writer.init(writer, .minified); const object = try stream.object(); try object.field("event", "user"); try object.field("index", self.index); try writeTimepointField(object, "timepoint", self.timepoint); try object.field("epoch", self.timepoint.epoch); try object.field("thread", self.timepoint.thread_id); try object.field("seq", self.timepoint.seq); if (self.label) |label| try object.field("label", label); if (try userPayloadProtocolAlloc(allocator, self.data)) |protocol| { defer allocator.free(protocol); try object.field("protocol", protocol); } if (self.data) |bytes| { try object.hexString("data_hex", bytes); if (try payloadIsJson(allocator, bytes)) try object.raw("payload", bytes); } try object.endLine(); }};pub const UserEventStatEntry = struct { group: []u8, count: usize = 0, first_index: ?usize = null, last_index: ?usize = null, first_timepoint: ?event.Timepoint = null, last_timepoint: ?event.Timepoint = null, total: u64 = 0, min: u64 = 0, max: u64 = 0, fn deinit(self: *UserEventStatEntry, allocator: std.mem.Allocator) void { allocator.free(self.group); self.* = undefined; } fn record(self: *UserEventStatEntry, index: usize, timepoint: event.Timepoint, value: u64) void { self.count += 1; if (self.first_index == null) { self.first_index = index; self.first_timepoint = timepoint; } self.last_index = index; self.last_timepoint = timepoint; self.total +|= value; self.min = if (self.count == 1) value else @min(self.min, value); self.max = @max(self.max, value); } fn mean(self: UserEventStatEntry) u64 { return if (self.count == 0) 0 else self.total / self.count; } fn sortValue(self: UserEventStatEntry, sort: UserEventStatSort) u64 { return switch (sort) { .total => self.total, .count => @intCast(self.count), .min => self.min, .max => self.max, .mean => self.mean(), .group => 0, }; } fn writeJsonLine( self: UserEventStatEntry, group_field: []const u8, metric_field: []const u8, sort: UserEventStatSort, writer: *std.Io.Writer, ) !void { var stream = pretty_json.Writer.init(writer, .minified); const object = try stream.object(); try object.field("event", "user_event_stat"); try object.field("group_field", group_field); try object.field("group", self.group); try object.field("metric_field", metric_field); try object.field("sort_field", @tagName(sort)); try object.field("first_index", self.first_index); try object.field("last_index", self.last_index); try writeOptionalTimepointField(object, "first_timepoint", self.first_timepoint); try writeOptionalTimepointField(object, "last_timepoint", self.last_timepoint); try object.field("count", self.count); try object.field("total", self.total); try object.field("min", self.min); try object.field("max", self.max); try object.field("mean", self.mean()); try object.endLine(); }};pub const SummaryEntry = struct { event_count: usize = 0, first_timepoint: ?event.Timepoint = null, last_timepoint: ?event.Timepoint = null, thread_count: usize = 0, session_status: ?i64 = null, session_start_count: usize = 0, session_end_count: usize = 0, function_enter_count: usize = 0, function_exit_count: usize = 0, safepoint_count: usize = 0, boundary_count: usize = 0, allocation_count: usize = 0, free_count: usize = 0, checkpoint_count: usize = 0, checkpoint_restore_count: usize = 0, user_count: usize = 0, object_count: usize = 0, live_object_count: usize = 0, freed_object_count: usize = 0, free_without_allocation_count: usize = 0, allocated_bytes: u64 = 0, live_bytes: u64 = 0, pub fn record(self: *SummaryEntry, item: event.Event) void { if (self.event_count == 0) self.first_timepoint = item.timepoint; self.last_timepoint = item.timepoint; self.event_count += 1; switch (item.kind) { .session_start => self.session_start_count += 1, .session_end => { self.session_end_count += 1; self.session_status = item.status; }, .function_enter => self.function_enter_count += 1, .function_exit => self.function_exit_count += 1, .safepoint => self.safepoint_count += 1, .boundary => self.boundary_count += 1, .allocation => { self.allocation_count += 1; self.allocated_bytes += item.size; }, .free => self.free_count += 1, .checkpoint => self.checkpoint_count += 1, .checkpoint_restore => self.checkpoint_restore_count += 1, .user => self.user_count += 1, } } pub fn writeJsonLine(self: SummaryEntry, writer: *std.Io.Writer) !void { var stream = pretty_json.Writer.init(writer, .minified); const object = try stream.object(); try object.field("event", "summary"); try object.field("event_count", self.event_count); try writeOptionalTimepointField(object, "first_timepoint", self.first_timepoint); try writeOptionalTimepointField(object, "last_timepoint", self.last_timepoint); try object.field("thread_count", self.thread_count); try object.field("session_status", self.session_status); try object.field("session_start_count", self.session_start_count); try object.field("session_end_count", self.session_end_count); try object.field("function_enter_count", self.function_enter_count); try object.field("function_exit_count", self.function_exit_count); try object.field("safepoint_count", self.safepoint_count); try object.field("boundary_count", self.boundary_count); try object.field("allocation_count", self.allocation_count); try object.field("free_count", self.free_count); try object.field("checkpoint_count", self.checkpoint_count); try object.field("checkpoint_restore_count", self.checkpoint_restore_count); try object.field("user_count", self.user_count); try object.field("object_count", self.object_count); try object.field("live_object_count", self.live_object_count); try object.field("freed_object_count", self.freed_object_count); try object.field( "free_without_allocation_count", self.free_without_allocation_count, ); try object.field("allocated_bytes", self.allocated_bytes); try object.field("live_bytes", self.live_bytes); try object.endLine(); }};pub const ThreadEntry = struct { thread_id: event.ThreadId, first_index: usize, last_index: usize, first_timepoint: event.Timepoint, last_timepoint: event.Timepoint, event_count: usize = 0, session_start_count: usize = 0, session_end_count: usize = 0, function_enter_count: usize = 0, function_exit_count: usize = 0, safepoint_count: usize = 0, boundary_count: usize = 0, allocation_count: usize = 0, free_count: usize = 0, checkpoint_count: usize = 0, checkpoint_restore_count: usize = 0, user_count: usize = 0, pub fn init(index: usize, item: event.Event) ThreadEntry { var entry = ThreadEntry{ .thread_id = item.timepoint.thread_id, .first_index = index, .last_index = index, .first_timepoint = item.timepoint, .last_timepoint = item.timepoint, }; entry.record(item, index); return entry; } pub fn record(self: *ThreadEntry, item: event.Event, index: usize) void { self.last_index = index; self.last_timepoint = item.timepoint; self.event_count += 1; switch (item.kind) { .session_start => self.session_start_count += 1, .session_end => self.session_end_count += 1, .function_enter => self.function_enter_count += 1, .function_exit => self.function_exit_count += 1, .safepoint => self.safepoint_count += 1, .boundary => self.boundary_count += 1, .allocation => self.allocation_count += 1, .free => self.free_count += 1, .checkpoint => self.checkpoint_count += 1, .checkpoint_restore => self.checkpoint_restore_count += 1, .user => self.user_count += 1, } } pub fn writeJsonLine(self: ThreadEntry, writer: *std.Io.Writer) !void { var stream = pretty_json.Writer.init(writer, .minified); const object = try stream.object(); try object.field("event", "thread"); try object.field("thread", self.thread_id); try object.field("first_index", self.first_index); try object.field("last_index", self.last_index); try writeTimepointField(object, "first_timepoint", self.first_timepoint); try writeTimepointField(object, "last_timepoint", self.last_timepoint); try object.field("event_count", self.event_count); try object.field("session_start_count", self.session_start_count); try object.field("session_end_count", self.session_end_count); try object.field("function_enter_count", self.function_enter_count); try object.field("function_exit_count", self.function_exit_count); try object.field("safepoint_count", self.safepoint_count); try object.field("boundary_count", self.boundary_count); try object.field("allocation_count", self.allocation_count); try object.field("free_count", self.free_count); try object.field("checkpoint_count", self.checkpoint_count); try object.field("checkpoint_restore_count", self.checkpoint_restore_count); try object.field("user_count", self.user_count); try object.endLine(); }};pub const ObjectEntry = struct { object_id: u64, allocation_index: ?usize = null, free_index: ?usize = null, allocated_at: ?event.Timepoint = null, freed_at: ?event.Timepoint = null, size: u64 = 0, alignment: u32 = 0, label: ?[]const u8 = null, pub fn fromAllocationAlloc(allocator: std.mem.Allocator, index: usize, item: event.Event) !ObjectEntry { return .{ .object_id = item.object_id, .allocation_index = index, .allocated_at = item.timepoint, .size = item.size, .alignment = item.alignment, .label = if (item.label) |label| try allocator.dupe(u8, label) else null, }; } pub fn fromFree(index: usize, item: event.Event) ObjectEntry { return .{ .object_id = item.object_id, .free_index = index, .freed_at = item.timepoint, }; } pub fn status(self: ObjectEntry) []const u8 { if (self.allocated_at == null) return "free_without_allocation"; if (self.freed_at != null) return "freed"; return "live"; } pub fn deinit(self: *ObjectEntry, allocator: std.mem.Allocator) void { if (self.label) |label| allocator.free(label); self.* = undefined; } pub fn writeJsonLine(self: ObjectEntry, writer: *std.Io.Writer) !void { var stream = pretty_json.Writer.init(writer, .minified); const object = try stream.object(); try object.field("event", "object"); try object.field("object_id", self.object_id); try object.field("status", self.status()); try object.field("allocation_index", self.allocation_index); try object.field("free_index", self.free_index); try writeOptionalTimepointField(object, "allocated_at", self.allocated_at); try writeOptionalTimepointField(object, "freed_at", self.freed_at); try object.field("size", self.size); try object.field("alignment", self.alignment); if (self.label) |label| try object.field("label", label); try object.endLine(); }};pub fn writeTimepointsJsonl(source: event.Source, writer: *std.Io.Writer) !void { var index: usize = 0; while (try source.peek()) |item| { try TimepointEntry.fromEvent(index, item.*).writeJsonLine(writer); source.advance(); index += 1; }}pub fn writeUserEventsJsonl( allocator: std.mem.Allocator, source: event.Source, filter: UserEventFilter, writer: *std.Io.Writer,) !void { var index: usize = 0; while (try source.peek()) |item| { if (item.kind == .user and try userEventMatches(allocator, item.*, filter)) { try UserEventEntry.fromEvent(index, item.*).writeJsonLine(allocator, writer); } source.advance(); index += 1; }}pub fn writeUserEventStatsJsonl( allocator: std.mem.Allocator, source: event.Source, filter: UserEventFilter, group_field: []const u8, metric_field: []const u8, sort: UserEventStatSort, writer: *std.Io.Writer,) !void { var stats: std.ArrayListUnmanaged(UserEventStatEntry) = .empty; defer { for (stats.items) |*entry| entry.deinit(allocator); stats.deinit(allocator); } var index: usize = 0; while (try source.peek()) |item| { if (item.kind == .user and try userEventMatches(allocator, item.*, filter)) stats: { const data = item.data orelse break :stats; const parsed = std.json.parseFromSlice(std.json.Value, allocator, data, .{}) catch break :stats; defer parsed.deinit(); const object = switch (parsed.value) { .object => |object| object, else => break :stats, }; const group = object.get(group_field) orelse break :stats; const metric = object.get(metric_field) orelse break :stats; const metric_value = payloadJsonValueToUnsigned(metric) orelse break :stats; const group_text = try payloadJsonValueToTextAlloc(allocator, group); defer allocator.free(group_text); const stat_index = try userEventStatIndex(allocator, &stats, group_text); stats.items[stat_index].record(index, item.timepoint, metric_value); } source.advance(); index += 1; } std.mem.sort(UserEventStatEntry, stats.items, sort, userEventStatGreater); for (stats.items) |entry| { try entry.writeJsonLine(group_field, metric_field, sort, writer); }}pub fn writeSummaryJsonl( allocator: std.mem.Allocator, source: event.Source, writer: *std.Io.Writer,) !void { var summary = SummaryEntry{}; var thread_ids: std.AutoHashMapUnmanaged(event.ThreadId, void) = .empty; defer thread_ids.deinit(allocator); var live_objects: std.AutoHashMapUnmanaged(u64, u64) = .empty; defer live_objects.deinit(allocator); while (try source.peek()) |item| { summary.record(item.*); try thread_ids.put(allocator, item.timepoint.thread_id, {}); switch (item.kind) { .allocation => { const result = try live_objects.getOrPut(allocator, item.object_id); if (result.found_existing) return error.DuplicateLiveObject; result.value_ptr.* = item.size; summary.object_count += 1; summary.live_bytes += item.size; }, .free => if (live_objects.fetchRemove(item.object_id)) |removed| { summary.freed_object_count += 1; summary.live_bytes -= removed.value; } else { summary.object_count += 1; summary.free_without_allocation_count += 1; }, else => {}, } source.advance(); } summary.thread_count = thread_ids.count(); summary.live_object_count = live_objects.count(); try summary.writeJsonLine(writer);}pub fn writeThreadsJsonl( allocator: std.mem.Allocator, source: event.Source, writer: *std.Io.Writer,) !void { var threads: std.ArrayListUnmanaged(ThreadEntry) = .empty; defer threads.deinit(allocator); var index: usize = 0; while (try source.peek()) |item| { if (findThread(threads.items, item.timepoint.thread_id)) |thread_index| { threads.items[thread_index].record(item.*, index); } else { try threads.append(allocator, ThreadEntry.init(index, item.*)); } source.advance(); index += 1; } for (threads.items) |item| { try item.writeJsonLine(writer); }}pub fn writeObjectsJsonl( allocator: std.mem.Allocator, source: event.Source, writer: *std.Io.Writer,) !void { var live: std.AutoHashMapUnmanaged(u64, ObjectEntry) = .empty; defer { var iterator = live.valueIterator(); while (iterator.next()) |item| item.deinit(allocator); live.deinit(allocator); } var index: usize = 0; while (try source.peek()) |item| { switch (item.kind) { .allocation => { var owned = try ObjectEntry.fromAllocationAlloc(allocator, index, item.*); var inserted = false; errdefer if (!inserted) owned.deinit(allocator); const result = try live.getOrPut(allocator, item.object_id); if (result.found_existing) return error.DuplicateLiveObject; result.value_ptr.* = owned; inserted = true; }, .free => if (live.fetchRemove(item.object_id)) |removed| { var completed = removed.value; defer completed.deinit(allocator); completed.free_index = index; completed.freed_at = item.timepoint; try completed.writeJsonLine(writer); } else { try ObjectEntry.fromFree(index, item.*).writeJsonLine(writer); }, else => {}, } source.advance(); index += 1; } var iterator = live.valueIterator(); while (iterator.next()) |item| try item.writeJsonLine(writer);}pub fn nearestCheckpointTimepoint( source: event.Source, target: event.Timepoint,) !?event.Timepoint { var result: ?event.Timepoint = null; while (try source.peek()) |item| { if (item.kind == .checkpoint and item.timepoint.beforeOrEqual(target) and (result == null or result.?.beforeOrEqual(item.timepoint))) { result = item.timepoint; } source.advance(); } return result;}fn findThread(entries: []const ThreadEntry, thread_id: event.ThreadId) ?usize { for (entries, 0..) |entry, index| { if (entry.thread_id == thread_id) return index; } return null;}fn userEventMatches( allocator: std.mem.Allocator, item: event.Event, filter: UserEventFilter,) !bool { if (filter.label) |expected| { const actual = item.label orelse return false; if (!std.mem.eql(u8, actual, expected)) return false; } if (filter.protocol) |expected| { const protocol = try userPayloadProtocolAlloc(allocator, item.data) orelse return false; defer allocator.free(protocol); if (!std.mem.eql(u8, protocol, expected)) return false; } for (filter.payload_filters) |payload_filter| { if (!try userPayloadFieldMatches(allocator, item.data, payload_filter.field, payload_filter.value)) return false; } return true;}fn userPayloadFieldMatches( allocator: std.mem.Allocator, maybe_data: ?[]const u8, field: []const u8, expected: []const u8,) !bool { const data = maybe_data orelse return false; const parsed = std.json.parseFromSlice(std.json.Value, allocator, data, .{}) catch return false; defer parsed.deinit(); const object = switch (parsed.value) { .object => |object| object, else => return false, }; const actual = object.get(field) orelse return false; return payloadJsonValueMatchesText(actual, expected);}fn payloadJsonValueMatchesText(value: std.json.Value, expected: []const u8) bool { return switch (value) { .string => |actual| std.mem.eql(u8, actual, expected), .integer => |actual| blk: { const parsed = std.fmt.parseInt(i64, expected, 0) catch break :blk false; break :blk actual == parsed; }, .float => |actual| blk: { const parsed = std.fmt.parseFloat(f64, expected) catch break :blk false; break :blk actual == parsed; }, .number_string => |actual| std.mem.eql(u8, actual, expected), .bool => |actual| (actual and std.mem.eql(u8, expected, "true")) or (!actual and std.mem.eql(u8, expected, "false")), .null => std.mem.eql(u8, expected, "null"), .array, .object, => false, };}fn payloadJsonValueToUnsigned(value: std.json.Value) ?u64 { return switch (value) { .integer => |actual| if (actual >= 0) @intCast(actual) else null, .float => |actual| if (actual >= 0 and @floor(actual) == actual) @intFromFloat(actual) else null, .number_string => |actual| std.fmt.parseUnsigned(u64, actual, 0) catch null, else => null, };}fn payloadJsonValueToTextAlloc( allocator: std.mem.Allocator, value: std.json.Value,) ![]u8 { return switch (value) { .string => |actual| try allocator.dupe(u8, actual), .integer => |actual| try std.fmt.allocPrint(allocator, "{d}", .{actual}), .float => |actual| try std.fmt.allocPrint(allocator, "{d}", .{actual}), .number_string => |actual| try allocator.dupe(u8, actual), .bool => |actual| try allocator.dupe(u8, if (actual) "true" else "false"), .null => try allocator.dupe(u8, "null"), .array, .object, => error.UnsupportedPayloadField, };}fn userEventStatIndex( allocator: std.mem.Allocator, stats: *std.ArrayListUnmanaged(UserEventStatEntry), group: []const u8,) !usize { for (stats.items, 0..) |entry, index| { if (std.mem.eql(u8, entry.group, group)) return index; } const owned_group = try allocator.dupe(u8, group); errdefer allocator.free(owned_group); try stats.append(allocator, .{ .group = owned_group }); return stats.items.len - 1;}fn userEventStatGreater(sort: UserEventStatSort, left: UserEventStatEntry, right: UserEventStatEntry) bool { if (sort == .group) return std.mem.lessThan(u8, left.group, right.group); const left_value = left.sortValue(sort); const right_value = right.sortValue(sort); if (left_value != right_value) return left_value > right_value; if (left.total != right.total) return left.total > right.total; if (left.max != right.max) return left.max > right.max; return std.mem.lessThan(u8, left.group, right.group);}fn userPayloadProtocolAlloc( allocator: std.mem.Allocator, maybe_data: ?[]const u8,) !?[]u8 { const data = maybe_data orelse return null; const parsed = std.json.parseFromSlice(std.json.Value, allocator, data, .{}) catch return null; defer parsed.deinit(); const object = switch (parsed.value) { .object => |object| object, else => return null, }; const protocol = switch (object.get("protocol") orelse return null) { .string => |text| text, else => return null, }; return try allocator.dupe(u8, protocol);}fn payloadIsJson(allocator: std.mem.Allocator, bytes: []const u8) !bool { const parsed = std.json.parseFromSlice(std.json.Value, allocator, bytes, .{}) catch return false; parsed.deinit(); return true;}pub fn framesAtAlloc( allocator: std.mem.Allocator, source: event.Source, target: event.Timepoint,) ![]Frame { var stack: std.ArrayListUnmanaged(Frame) = .empty; defer stack.deinit(allocator); while (try source.peek()) |item| { if (item.timepoint.thread_id == target.thread_id and item.timepoint.beforeOrEqual(target)) { switch (item.kind) { .function_enter => try pushFrame(allocator, &stack, item.*, false), .safepoint => try recordSafepoint(allocator, &stack, item.*), .function_exit => try popFrame(&stack, item.*), else => {}, } } source.advance(); } const frames = try allocator.alloc(Frame, stack.items.len); for (frames, 0..) |*frame, depth| { frame.* = stack.items[stack.items.len - depth - 1]; frame.depth = depth; } return frames;}fn pushFrame( allocator: std.mem.Allocator, stack: *std.ArrayListUnmanaged(Frame), item: event.Event, synthetic: bool,) !void { try stack.append(allocator, .{ .function_id = item.function_id, .site_id = item.site_id, .stack_map_id = item.stack_map_id, .entered_at = item.timepoint, .last_timepoint = item.timepoint, .synthetic = synthetic, });}fn recordSafepoint( allocator: std.mem.Allocator, stack: *std.ArrayListUnmanaged(Frame), item: event.Event,) !void { if (stack.items.len == 0) { try pushFrame(allocator, stack, item, true); return; } const top = &stack.items[stack.items.len - 1]; if (top.function_id != item.function_id) return error.TraceFrameMismatch; top.site_id = item.site_id; top.stack_map_id = item.stack_map_id; top.last_timepoint = item.timepoint;}fn popFrame( stack: *std.ArrayListUnmanaged(Frame), item: event.Event,) !void { if (stack.items.len == 0) return error.TraceFrameMismatch; const top = stack.items[stack.items.len - 1]; if (top.function_id != item.function_id) return error.TraceFrameMismatch; _ = stack.pop();}fn writeTimepointField(object: pretty_json.Object, name: []const u8, value: event.Timepoint) !void { try object.formattedString( name, timepoint_string_capacity, "{d}:{d}:{d}", .{ value.epoch, value.thread_id, value.seq }, );}fn writeOptionalTimepointField( object: pretty_json.Object, name: []const u8, value: ?event.Timepoint,) !void { if (value) |actual| { try writeTimepointField(object, name, actual); } else { try object.field(name, null); }}const SliceSource = struct { events: []const event.Event, cursor: usize = 0, fn source(self: *SliceSource) event.Source { return .{ .context = self, .peekFn = peek, .advanceFn = advance, .countFn = count }; } fn peek(context: *anyopaque) !?*const event.Event { const self: *SliceSource = @ptrCast(@alignCast(context)); if (self.cursor == self.events.len) return null; return &self.events[self.cursor]; } fn advance(context: *anyopaque) void { const self: *SliceSource = @ptrCast(@alignCast(context)); self.cursor += 1; } fn count(context: *anyopaque) u64 { const self: *SliceSource = @ptrCast(@alignCast(context)); return self.events.len; }};fn writeTimepointsFromSlice(events: []const event.Event, writer: *std.Io.Writer) !void { var stream = SliceSource{ .events = events }; try writeTimepointsJsonl(stream.source(), writer);}fn writeUserEventsFromSlice( allocator: std.mem.Allocator, events: []const event.Event, filter: UserEventFilter, writer: *std.Io.Writer,) !void { var stream = SliceSource{ .events = events }; try writeUserEventsJsonl(allocator, stream.source(), filter, writer);}fn writeUserEventStatsFromSlice( allocator: std.mem.Allocator, events: []const event.Event, filter: UserEventFilter, group_field: []const u8, metric_field: []const u8, sort: UserEventStatSort, writer: *std.Io.Writer,) !void { var stream = SliceSource{ .events = events }; try writeUserEventStatsJsonl(allocator, stream.source(), filter, group_field, metric_field, sort, writer);}fn writeSummaryFromSlice(allocator: std.mem.Allocator, events: []const event.Event, writer: *std.Io.Writer) !void { var stream = SliceSource{ .events = events }; try writeSummaryJsonl(allocator, stream.source(), writer);}fn writeThreadsFromSlice(allocator: std.mem.Allocator, events: []const event.Event, writer: *std.Io.Writer) !void { var stream = SliceSource{ .events = events }; try writeThreadsJsonl(allocator, stream.source(), writer);}fn writeObjectsFromSlice(allocator: std.mem.Allocator, events: []const event.Event, writer: *std.Io.Writer) !void { var stream = SliceSource{ .events = events }; try writeObjectsJsonl(allocator, stream.source(), writer);}fn nearestCheckpointFromSlice(events: []const event.Event, target: event.Timepoint) !?event.Timepoint { var stream = SliceSource{ .events = events }; return try nearestCheckpointTimepoint(stream.source(), target);}fn framesFromSlice(allocator: std.mem.Allocator, events: []const event.Event, target: event.Timepoint) ![]Frame { var stream = SliceSource{ .events = events }; return try framesAtAlloc(allocator, stream.source(), target);}test "checkpoint lookup returns nearest predecessor" { const events = [_]event.Event{ event.Event.checkpoint(.{ .thread_id = 1, .seq = 2 }, "a", "state-a"), event.Event.user(.{ .thread_id = 1, .seq = 3 }, "note", "ignored"), event.Event.checkpoint(.{ .thread_id = 1, .seq = 8 }, "b", "state-b"), }; const found = (try nearestCheckpointFromSlice(&events, .{ .thread_id = 1, .seq = 9 })) orelse return error.MissingCheckpoint; try std.testing.expectEqual(@as(u64, 8), found.seq);}test "timepoint query writes compact navigation jsonl" { const events = [_]event.Event{ event.Event.sessionStart(.{ .thread_id = 1, .seq = 1 }, "run"), event.Event.safepointReached(.{ .thread_id = 1, .seq = 2 }, .{ .function_id = 20, .site_id = 5 }), event.Event.user(.{ .thread_id = 1, .seq = 3 }, "note", "ignored"), }; var out = std.Io.Writer.Allocating.init(std.testing.allocator); defer out.deinit(); try writeTimepointsFromSlice(&events, &out.writer); const bytes = try out.toOwnedSlice(); defer std.testing.allocator.free(bytes); try std.testing.expect(std.mem.indexOf(u8, bytes, "\"event\":\"timepoint\"") != null); try std.testing.expect(std.mem.indexOf(u8, bytes, "\"timepoint\":\"0:1:2\"") != null); try std.testing.expect(std.mem.indexOf(u8, bytes, "\"kind\":\"safepoint\"") != null); try std.testing.expect(std.mem.indexOf(u8, bytes, "\"label\":\"run\"") != null);}test "user event query filters labels and payload protocols" { const events = [_]event.Event{ event.Event.user( .{ .thread_id = 1, .seq = 1 }, "compiler.query.miss", "{\"protocol\":\"tiny.compiler.query/v1\",\"phase\":\"analysis\",\"product_kind\":\"function_summary\",\"product_identity\":7}", ), event.Event.user(.{ .thread_id = 1, .seq = 2 }, "process.argv", "tiny\x00compile"), event.Event.user( .{ .thread_id = 2, .seq = 3 }, "compiler.query.hit", "{\"protocol\":\"tiny.compiler.query/v1\",\"phase\":\"source_choir\",\"product_kind\":\"source_choir_module\",\"product_identity\":8}", ), }; var out = std.Io.Writer.Allocating.init(std.testing.allocator); defer out.deinit(); try writeUserEventsFromSlice( std.testing.allocator, &events, .{ .protocol = "tiny.compiler.query/v1" }, &out.writer, ); const bytes = try out.toOwnedSlice(); defer std.testing.allocator.free(bytes); try std.testing.expect(std.mem.indexOf(u8, bytes, "\"event\":\"user\"") != null); try std.testing.expect(std.mem.indexOf(u8, bytes, "\"label\":\"compiler.query.miss\"") != null); try std.testing.expect(std.mem.indexOf(u8, bytes, "\"label\":\"compiler.query.hit\"") != null); try std.testing.expect(std.mem.indexOf(u8, bytes, "\"label\":\"process.argv\"") == null); try std.testing.expect(std.mem.indexOf(u8, bytes, "\"protocol\":\"tiny.compiler.query/v1\"") != null); try std.testing.expect(std.mem.indexOf(u8, bytes, "\"payload\":{\"protocol\":\"tiny.compiler.query/v1\",\"phase\":\"analysis\"") != null); var label_out = std.Io.Writer.Allocating.init(std.testing.allocator); defer label_out.deinit(); try writeUserEventsFromSlice( std.testing.allocator, &events, .{ .label = "compiler.query.hit" }, &label_out.writer, ); const label_bytes = try label_out.toOwnedSlice(); defer std.testing.allocator.free(label_bytes); try std.testing.expect(std.mem.indexOf(u8, label_bytes, "\"label\":\"compiler.query.hit\"") != null); try std.testing.expect(std.mem.indexOf(u8, label_bytes, "\"label\":\"compiler.query.miss\"") == null); var payload_out = std.Io.Writer.Allocating.init(std.testing.allocator); defer payload_out.deinit(); const product_kind_filters = [_]UserPayloadFilter{.{ .field = "product_kind", .value = "function_summary", }}; try writeUserEventsFromSlice( std.testing.allocator, &events, .{ .protocol = "tiny.compiler.query/v1", .payload_filters = &product_kind_filters, }, &payload_out.writer, ); const payload_bytes = try payload_out.toOwnedSlice(); defer std.testing.allocator.free(payload_bytes); try std.testing.expect(std.mem.indexOf(u8, payload_bytes, "\"product_kind\":\"function_summary\"") != null); try std.testing.expect(std.mem.indexOf(u8, payload_bytes, "\"product_kind\":\"source_choir_module\"") == null); var identity_out = std.Io.Writer.Allocating.init(std.testing.allocator); defer identity_out.deinit(); const identity_filters = [_]UserPayloadFilter{.{ .field = "product_identity", .value = "8", }}; try writeUserEventsFromSlice( std.testing.allocator, &events, .{ .payload_filters = &identity_filters }, &identity_out.writer, ); const identity_bytes = try identity_out.toOwnedSlice(); defer std.testing.allocator.free(identity_bytes); try std.testing.expect(std.mem.indexOf(u8, identity_bytes, "\"product_identity\":8") != null); try std.testing.expect(std.mem.indexOf(u8, identity_bytes, "\"product_identity\":7") == null); var combined_out = std.Io.Writer.Allocating.init(std.testing.allocator); defer combined_out.deinit(); const combined_filters = [_]UserPayloadFilter{ .{ .field = "product_kind", .value = "source_choir_module" }, .{ .field = "product_identity", .value = "8" }, }; try writeUserEventsFromSlice( std.testing.allocator, &events, .{ .protocol = "tiny.compiler.query/v1", .payload_filters = &combined_filters, }, &combined_out.writer, ); const combined_bytes = try combined_out.toOwnedSlice(); defer std.testing.allocator.free(combined_bytes); try std.testing.expect(std.mem.indexOf(u8, combined_bytes, "\"product_kind\":\"source_choir_module\"") != null); try std.testing.expect(std.mem.indexOf(u8, combined_bytes, "\"product_kind\":\"function_summary\"") == null); try std.testing.expect(std.mem.indexOf(u8, combined_bytes, "\"product_identity\":8") != null);}test "user event stats groups by payload and sums numeric metrics" { const events = [_]event.Event{ event.Event.user( .{ .thread_id = 1, .seq = 1 }, "compiler.query.finish", "{\"protocol\":\"tiny.compiler.query/v1\",\"product_kind\":\"function_summary\",\"elapsed_ns\":7}", ), event.Event.user( .{ .thread_id = 1, .seq = 2 }, "compiler.query.finish", "{\"protocol\":\"tiny.compiler.query/v1\",\"product_kind\":\"partial_evaluation\",\"elapsed_ns\":11}", ), event.Event.user( .{ .thread_id = 1, .seq = 3 }, "compiler.query.finish", "{\"protocol\":\"tiny.compiler.query/v1\",\"product_kind\":\"function_summary\",\"elapsed_ns\":13}", ), event.Event.user(.{ .thread_id = 1, .seq = 4 }, "process.argv", "tiny\x00compile"), }; var out = std.Io.Writer.Allocating.init(std.testing.allocator); defer out.deinit(); try writeUserEventStatsFromSlice( std.testing.allocator, &events, .{ .label = "compiler.query.finish", .protocol = "tiny.compiler.query/v1", }, "product_kind", "elapsed_ns", .total, &out.writer, ); const bytes = try out.toOwnedSlice(); defer std.testing.allocator.free(bytes); const function_index = std.mem.indexOf(u8, bytes, "\"group\":\"function_summary\"") orelse return error.TestExpectedResult; const partial_index = std.mem.indexOf(u8, bytes, "\"group\":\"partial_evaluation\"") orelse return error.TestExpectedResult; try std.testing.expect(function_index < partial_index); try std.testing.expect(std.mem.indexOf(u8, bytes, "\"count\":2") != null); try std.testing.expect(std.mem.indexOf(u8, bytes, "\"sort_field\":\"total\"") != null); try std.testing.expect(std.mem.indexOf(u8, bytes, "\"first_index\":0") != null); try std.testing.expect(std.mem.indexOf(u8, bytes, "\"last_index\":2") != null); try std.testing.expect(std.mem.indexOf(u8, bytes, "\"first_timepoint\":\"0:1:1\"") != null); try std.testing.expect(std.mem.indexOf(u8, bytes, "\"last_timepoint\":\"0:1:3\"") != null); try std.testing.expect(std.mem.indexOf(u8, bytes, "\"total\":20") != null); try std.testing.expect(std.mem.indexOf(u8, bytes, "\"min\":7") != null); try std.testing.expect(std.mem.indexOf(u8, bytes, "\"max\":13") != null); try std.testing.expect(std.mem.indexOf(u8, bytes, "\"mean\":10") != null); try std.testing.expect(std.mem.indexOf(u8, bytes, "\"total\":11") != null); try std.testing.expect(std.mem.indexOf(u8, bytes, "\"mean\":11") != null);}test "user event stats can sort by mean metric" { const events = [_]event.Event{ event.Event.user( .{ .thread_id = 1, .seq = 1 }, "compiler.query.finish", "{\"protocol\":\"tiny.compiler.query/v1\",\"product_kind\":\"function_summary\",\"elapsed_ns\":7}", ), event.Event.user( .{ .thread_id = 1, .seq = 2 }, "compiler.query.finish", "{\"protocol\":\"tiny.compiler.query/v1\",\"product_kind\":\"partial_evaluation\",\"elapsed_ns\":11}", ), event.Event.user( .{ .thread_id = 1, .seq = 3 }, "compiler.query.finish", "{\"protocol\":\"tiny.compiler.query/v1\",\"product_kind\":\"function_summary\",\"elapsed_ns\":13}", ), }; var out = std.Io.Writer.Allocating.init(std.testing.allocator); defer out.deinit(); try writeUserEventStatsFromSlice( std.testing.allocator, &events, .{ .label = "compiler.query.finish", .protocol = "tiny.compiler.query/v1", }, "product_kind", "elapsed_ns", .mean, &out.writer, ); const bytes = try out.toOwnedSlice(); defer std.testing.allocator.free(bytes); const partial_index = std.mem.indexOf(u8, bytes, "\"group\":\"partial_evaluation\"") orelse return error.TestExpectedResult; const function_index = std.mem.indexOf(u8, bytes, "\"group\":\"function_summary\"") orelse return error.TestExpectedResult; try std.testing.expect(partial_index < function_index); try std.testing.expect(std.mem.indexOf(u8, bytes, "\"sort_field\":\"mean\"") != null);}test "summary query writes trace-level totals as jsonl" { const events = [_]event.Event{ event.Event.sessionStart(.{ .thread_id = 1, .seq = 1 }, "run"), event.Event.allocation(.{ .thread_id = 1, .seq = 2 }, 7, 64, 8, "buffer"), event.Event.boundaryBytes(.{ .thread_id = 2, .seq = 3 }, "env.TEST", "value"), event.Event.free(.{ .thread_id = 1, .seq = 4 }, 7), event.Event.free(.{ .thread_id = 1, .seq = 5 }, 99), event.Event.sessionEnd(.{ .thread_id = 2, .seq = 6 }, 0), }; var out = std.Io.Writer.Allocating.init(std.testing.allocator); defer out.deinit(); try writeSummaryFromSlice(std.testing.allocator, &events, &out.writer); const bytes = try out.toOwnedSlice(); defer std.testing.allocator.free(bytes); try std.testing.expect(std.mem.indexOf(u8, bytes, "\"event\":\"summary\"") != null); try std.testing.expect(std.mem.indexOf(u8, bytes, "\"event_count\":6") != null); try std.testing.expect(std.mem.indexOf(u8, bytes, "\"first_timepoint\":\"0:1:1\"") != null); try std.testing.expect(std.mem.indexOf(u8, bytes, "\"last_timepoint\":\"0:2:6\"") != null); try std.testing.expect(std.mem.indexOf(u8, bytes, "\"thread_count\":2") != null); try std.testing.expect(std.mem.indexOf(u8, bytes, "\"session_status\":0") != null); try std.testing.expect(std.mem.indexOf(u8, bytes, "\"boundary_count\":1") != null); try std.testing.expect(std.mem.indexOf(u8, bytes, "\"object_count\":2") != null); try std.testing.expect(std.mem.indexOf(u8, bytes, "\"freed_object_count\":1") != null); try std.testing.expect(std.mem.indexOf(u8, bytes, "\"free_without_allocation_count\":1") != null); try std.testing.expect(std.mem.indexOf(u8, bytes, "\"allocated_bytes\":64") != null); try std.testing.expect(std.mem.indexOf(u8, bytes, "\"live_bytes\":0") != null);}test "thread query writes per-thread event ranges and counts" { const events = [_]event.Event{ event.Event.sessionStart(.{ .thread_id = 1, .seq = 1 }, "run"), event.Event.functionEnter(.{ .thread_id = 1, .seq = 2 }, .{ .function_id = 20, .site_id = 1 }), event.Event.safepointReached(.{ .thread_id = 2, .seq = 3 }, .{ .function_id = 30, .site_id = 5 }), event.Event.boundaryBytes(.{ .thread_id = 1, .seq = 4 }, "env.TEST", "value"), event.Event.sessionEnd(.{ .thread_id = 2, .seq = 5 }, 0), }; var out = std.Io.Writer.Allocating.init(std.testing.allocator); defer out.deinit(); try writeThreadsFromSlice(std.testing.allocator, &events, &out.writer); const bytes = try out.toOwnedSlice(); defer std.testing.allocator.free(bytes); try std.testing.expect(std.mem.indexOf(u8, bytes, "\"event\":\"thread\"") != null); try std.testing.expect(std.mem.indexOf(u8, bytes, "\"thread\":1") != null); try std.testing.expect(std.mem.indexOf(u8, bytes, "\"first_timepoint\":\"0:1:1\"") != null); try std.testing.expect(std.mem.indexOf(u8, bytes, "\"last_timepoint\":\"0:1:4\"") != null); try std.testing.expect(std.mem.indexOf(u8, bytes, "\"event_count\":3") != null); try std.testing.expect(std.mem.indexOf(u8, bytes, "\"function_enter_count\":1") != null); try std.testing.expect(std.mem.indexOf(u8, bytes, "\"boundary_count\":1") != null); try std.testing.expect(std.mem.indexOf(u8, bytes, "\"thread\":2") != null); try std.testing.expect(std.mem.indexOf(u8, bytes, "\"safepoint_count\":1") != null); try std.testing.expect(std.mem.indexOf(u8, bytes, "\"session_end_count\":1") != null);}test "object query writes allocation lifetimes as jsonl" { const events = [_]event.Event{ event.Event.allocation(.{ .thread_id = 1, .seq = 1 }, 7, 64, 8, "buffer"), event.Event.safepointReached(.{ .thread_id = 1, .seq = 2 }, .{ .function_id = 20, .site_id = 5 }), event.Event.free(.{ .thread_id = 1, .seq = 3 }, 7), event.Event.allocation(.{ .thread_id = 1, .seq = 4 }, 8, 16, 4, null), event.Event.free(.{ .thread_id = 1, .seq = 5 }, 99), }; var out = std.Io.Writer.Allocating.init(std.testing.allocator); defer out.deinit(); try writeObjectsFromSlice(std.testing.allocator, &events, &out.writer); const bytes = try out.toOwnedSlice(); defer std.testing.allocator.free(bytes); try std.testing.expect(std.mem.indexOf(u8, bytes, "\"event\":\"object\"") != null); try std.testing.expect(std.mem.indexOf(u8, bytes, "\"object_id\":7") != null); try std.testing.expect(std.mem.indexOf(u8, bytes, "\"status\":\"freed\"") != null); try std.testing.expect(std.mem.indexOf(u8, bytes, "\"allocated_at\":\"0:1:1\"") != null); try std.testing.expect(std.mem.indexOf(u8, bytes, "\"freed_at\":\"0:1:3\"") != null); try std.testing.expect(std.mem.indexOf(u8, bytes, "\"label\":\"buffer\"") != null); try std.testing.expect(std.mem.indexOf(u8, bytes, "\"object_id\":8") != null); try std.testing.expect(std.mem.indexOf(u8, bytes, "\"status\":\"live\"") != null); try std.testing.expect(std.mem.indexOf(u8, bytes, "\"object_id\":99") != null); try std.testing.expect(std.mem.indexOf(u8, bytes, "\"status\":\"free_without_allocation\"") != null);}test "frame query returns active stack at safepoint" { const events = [_]event.Event{ event.Event.functionEnter(.{ .thread_id = 1, .seq = 1 }, .{ .function_id = 10, .site_id = 1 }), event.Event.functionEnter(.{ .thread_id = 1, .seq = 2 }, .{ .function_id = 20, .site_id = 1 }), event.Event.safepointReached(.{ .thread_id = 1, .seq = 3 }, .{ .function_id = 20, .site_id = 9, .stack_map_id = 7 }), event.Event.functionExit(.{ .thread_id = 1, .seq = 4 }, .{ .function_id = 20, .site_id = 10 }), }; const frames = try framesFromSlice(std.testing.allocator, &events, .{ .thread_id = 1, .seq = 3 }); defer std.testing.allocator.free(frames); try std.testing.expectEqual(@as(usize, 2), frames.len); try std.testing.expectEqual(@as(usize, 0), frames[0].depth); try std.testing.expectEqual(@as(u64, 20), frames[0].function_id); try std.testing.expectEqual(@as(u64, 9), frames[0].site_id); try std.testing.expectEqual(@as(u64, 7), frames[0].stack_map_id); try std.testing.expectEqual(@as(u64, 3), frames[0].last_timepoint.seq); try std.testing.expectEqual(@as(usize, 1), frames[1].depth); try std.testing.expectEqual(@as(u64, 10), frames[1].function_id);}test "frame query drops exited frames by target timepoint" { const events = [_]event.Event{ event.Event.functionEnter(.{ .thread_id = 1, .seq = 1 }, .{ .function_id = 10, .site_id = 1 }), event.Event.functionEnter(.{ .thread_id = 1, .seq = 2 }, .{ .function_id = 20, .site_id = 1 }), event.Event.functionExit(.{ .thread_id = 1, .seq = 3 }, .{ .function_id = 20, .site_id = 2 }), }; const frames = try framesFromSlice(std.testing.allocator, &events, .{ .thread_id = 1, .seq = 3 }); defer std.testing.allocator.free(frames); try std.testing.expectEqual(@as(usize, 1), frames.len); try std.testing.expectEqual(@as(u64, 10), frames[0].function_id);}test "frame query represents safepoint-only traces" { const events = [_]event.Event{ event.Event.safepointReached(.{ .thread_id = 1, .seq = 1 }, .{ .function_id = 20, .site_id = 5 }), }; const frames = try framesFromSlice(std.testing.allocator, &events, .{ .thread_id = 1, .seq = 1 }); defer std.testing.allocator.free(frames); try std.testing.expectEqual(@as(usize, 1), frames.len); try std.testing.expectEqual(@as(u64, 20), frames[0].function_id); try std.testing.expect(frames[0].synthetic);}test "frame query ignores other threads" { const events = [_]event.Event{ event.Event.functionEnter(.{ .thread_id = 1, .seq = 1 }, .{ .function_id = 10, .site_id = 1 }), event.Event.functionEnter(.{ .thread_id = 2, .seq = 2 }, .{ .function_id = 20, .site_id = 1 }), }; const frames = try framesFromSlice(std.testing.allocator, &events, .{ .thread_id = 1, .seq = 2 }); defer std.testing.allocator.free(frames); try std.testing.expectEqual(@as(usize, 1), frames.len); try std.testing.expectEqual(@as(u64, 10), frames[0].function_id);}Source: lib/trace/src/root.zig:36
zig
pub const replay = @import("replay.zig");Audit
| Definitions | 17 |
|---|---|
| Public names | 17 |
| Members | 24 |
| Version | 26.7.0 |
| Revision | daab053ee433 |