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 }