lib/tracy/src/transport.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const pretty_json = @import("pretty").json;
3
4 const assert = std.debug.assert;
5
6 pub const schema = "tracy.flight/v0";
7
8 pub const OverflowPolicy = enum {
9 overwrite_oldest,
10 stop_when_full,
11
12 pub fn tag(self: OverflowPolicy) []const u8 {
13 return switch (self) {
14 .overwrite_oldest => "overwrite_oldest",
15 .stop_when_full => "stop_when_full",
16 };
17 }
18 };
19
20 pub const State = enum {
21 accepting,
22 full,
23
24 pub fn tag(self: State) []const u8 {
25 return switch (self) {
26 .accepting => "accepting",
27 .full => "full",
28 };
29 }
30 };
31
32 pub const Report = struct {
33 policy: OverflowPolicy,
34 state: State,
35 capacity_bytes: usize,
36 retained_bytes: usize,
37 event_capacity_bytes: usize,
38 writer_capacity_bytes: usize,
39 observed_events: u64,
40 stored_events: u64,
41 retained_events: usize,
42 overwritten_events: u64,
43 dropped_events: u64,
44 oversized_events: u64,
45 partial_event_bytes: usize,
46 discarding_oversized_event: bool,
47
48 pub fn valid(self: Report) bool {
49 if (self.capacity_bytes == 0) return false;
50 if (self.event_capacity_bytes == 0) return false;
51 if (self.writer_capacity_bytes == 0) return false;
52 if (self.retained_bytes > self.capacity_bytes) return false;
53 if (self.partial_event_bytes > self.event_capacity_bytes) return false;
54 if (self.discarding_oversized_event and self.partial_event_bytes != 0) return false;
55 const retained_events = std.math.cast(u64, self.retained_events) orelse return false;
56 const retained_and_overwritten = retained_events +| self.overwritten_events;
57 if (retained_and_overwritten != self.stored_events) return false;
58 const rejected_events = self.dropped_events +| self.oversized_events;
59 const accounted_events = self.stored_events +| rejected_events;
60 if (accounted_events != self.observed_events) return false;
61 if (self.policy == .overwrite_oldest and self.dropped_events != 0) return false;
62 if (self.policy == .stop_when_full and self.overwritten_events != 0) return false;
63 if (self.state == .full and self.policy != .stop_when_full) return false;
64 return true;
65 }
66
67 pub fn writeJson(self: Report, writer: *std.Io.Writer) !void {
68 assert(self.valid());
69 var stream = pretty_json.Writer.init(writer, .minified);
70 const object = try stream.object();
71 try self.writeFields(object);
72 try object.end();
73 }
74
75 pub fn writeFields(self: Report, object: pretty_json.Object) !void {
76 assert(self.valid());
77 try object.field("schema", schema);
78 try object.field("policy", self.policy.tag());
79 try object.field("state", self.state.tag());
80 try object.field("capacity_bytes", self.capacity_bytes);
81 try object.field("retained_bytes", self.retained_bytes);
82 try object.field("event_capacity_bytes", self.event_capacity_bytes);
83 try object.field("writer_capacity_bytes", self.writer_capacity_bytes);
84 try object.field("observed_events", self.observed_events);
85 try object.field("stored_events", self.stored_events);
86 try object.field("retained_events", self.retained_events);
87 try object.field("overwritten_events", self.overwritten_events);
88 try object.field("dropped_events", self.dropped_events);
89 try object.field("oversized_events", self.oversized_events);
90 try object.field("partial_event_bytes", self.partial_event_bytes);
91 try object.field("discarding_oversized_event", self.discarding_oversized_event);
92 }
93
94 pub fn writeJsonl(self: Report, writer: *std.Io.Writer) !void {
95 try self.writeJson(writer);
96 try writer.writeByte('\n');
97 }
98 };
99
100 pub fn parseLine(allocator: std.mem.Allocator, line: []const u8) !Report {
101 const text = std.mem.trim(u8, line, " \t\r\n");
102 if (text.len == 0) return error.InvalidFlightReport;
103 var tree = std.json.parseFromSlice(std.json.Value, allocator, text, .{}) catch |err| {
104 if (err == error.OutOfMemory) return err;
105 return error.InvalidFlightReport;
106 };
107 defer tree.deinit();
108 const object = switch (tree.value) {
109 .object => |object| object,
110 else => return error.InvalidFlightReport,
111 };
112 const actual_schema = try jsonString(object, "schema");
113 if (!std.mem.eql(u8, actual_schema, schema)) return error.UnsupportedTraceSchema;
114 const result: Report = .{
115 .policy = try jsonEnum(OverflowPolicy, object, "policy"),
116 .state = try jsonEnum(State, object, "state"),
117 .capacity_bytes = try jsonUsize(object, "capacity_bytes"),
118 .retained_bytes = try jsonUsize(object, "retained_bytes"),
119 .event_capacity_bytes = try jsonUsize(object, "event_capacity_bytes"),
120 .writer_capacity_bytes = try jsonUsize(object, "writer_capacity_bytes"),
121 .observed_events = try jsonU64(object, "observed_events"),
122 .stored_events = try jsonU64(object, "stored_events"),
123 .retained_events = try jsonUsize(object, "retained_events"),
124 .overwritten_events = try jsonU64(object, "overwritten_events"),
125 .dropped_events = try jsonU64(object, "dropped_events"),
126 .oversized_events = try jsonU64(object, "oversized_events"),
127 .partial_event_bytes = try jsonUsize(object, "partial_event_bytes"),
128 .discarding_oversized_event = try jsonBool(object, "discarding_oversized_event"),
129 };
130 if (!result.valid()) return error.InvalidFlightReport;
131 return result;
132 }
133
134 fn jsonString(object: std.json.ObjectMap, key: []const u8) ![]const u8 {
135 return switch (object.get(key) orelse return error.InvalidFlightReport) {
136 .string => |text| text,
137 else => error.InvalidFlightReport,
138 };
139 }
140
141 fn jsonEnum(comptime T: type, object: std.json.ObjectMap, key: []const u8) !T {
142 return std.meta.stringToEnum(T, try jsonString(object, key)) orelse
143 error.InvalidFlightReport;
144 }
145
146 fn jsonU64(object: std.json.ObjectMap, key: []const u8) !u64 {
147 return switch (object.get(key) orelse return error.InvalidFlightReport) {
148 .integer => |value| if (value < 0) error.InvalidFlightReport else @intCast(value),
149 .number_string => |value| std.fmt.parseInt(u64, value, 10) catch
150 error.InvalidFlightReport,
151 else => error.InvalidFlightReport,
152 };
153 }
154
155 fn jsonUsize(object: std.json.ObjectMap, key: []const u8) !usize {
156 return std.math.cast(usize, try jsonU64(object, key)) orelse
157 error.InvalidFlightReport;
158 }
159
160 fn jsonBool(object: std.json.ObjectMap, key: []const u8) !bool {
161 return switch (object.get(key) orelse return error.InvalidFlightReport) {
162 .bool => |value| value,
163 else => error.InvalidFlightReport,
164 };
165 }
166
167 test "transport report round trips" {
168 const expected: Report = .{
169 .policy = .stop_when_full,
170 .state = .full,
171 .capacity_bytes = 64,
172 .retained_bytes = 32,
173 .event_capacity_bytes = 16,
174 .writer_capacity_bytes = 8,
175 .observed_events = 7,
176 .stored_events = 5,
177 .retained_events = 5,
178 .overwritten_events = 0,
179 .dropped_events = 2,
180 .oversized_events = 0,
181 .partial_event_bytes = 0,
182 .discarding_oversized_event = false,
183 };
184 var line = std.Io.Writer.Allocating.init(std.testing.allocator);
185 defer line.deinit();
186 try expected.writeJsonl(&line.writer);
187 const actual = try parseLine(std.testing.allocator, line.written());
188 try std.testing.expectEqualDeep(expected, actual);
189 }
190
191 test "transport report rejects malformed rows" {
192 try std.testing.expectError(
193 error.UnsupportedTraceSchema,
194 parseLine(std.testing.allocator, "{\"schema\":\"future/v0\"}"),
195 );
196 try std.testing.expectError(
197 error.InvalidFlightReport,
198 parseLine(std.testing.allocator, "{\"schema\":\"tracy.flight/v0\"}"),
199 );
200 }