lib/tracy/src/capture.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const pretty_json = @import("pretty").json;
3 const event = @import("event.zig");
4 const transport = @import("transport.zig");
5
6 pub const method = "tracy_event_sequence_and_lifecycle_v1";
7
8 pub const Counters = struct {
9 events: u64 = 0,
10 sequenced_events: u64 = 0,
11 unsequenced_events: u64 = 0,
12 sequence_gaps: u64 = 0,
13 missing_sequence_events: u64 = 0,
14 sequence_regressions: u64 = 0,
15 start_events: u64 = 0,
16 stop_events: u64 = 0,
17 };
18
19 pub const Integrity = struct {
20 status: []const u8,
21 action: []const u8,
22 message: ?[]const u8,
23 event_count: u64,
24 sequenced_event_count: u64,
25 unsequenced_event_count: u64,
26 first_sequence: ?u64,
27 last_sequence: ?u64,
28 sequence_gap_count: u64,
29 missing_sequence_event_count: u64,
30 sequence_regression_count: u64,
31 start_event_count: u64,
32 stop_event_count: u64,
33 start_sequence: ?u64,
34 stop_sequence: ?u64,
35 unbalanced_event_count: u64,
36 flight_report: ?transport.Report,
37 };
38
39 pub const Tracker = struct {
40 counters: Counters = .{},
41 first_sequence: ?u64 = null,
42 last_sequence: ?u64 = null,
43 start_sequence: ?u64 = null,
44 stop_sequence: ?u64 = null,
45 flight_report: ?transport.Report = null,
46
47 pub fn record(self: *Tracker, parsed: event.Parsed) void {
48 self.counters.events +|= 1;
49 self.recordSequence(parsed.seq);
50 switch (parsed.kind) {
51 .start => {
52 self.counters.start_events +|= 1;
53 if (self.start_sequence == null) self.start_sequence = parsed.seq;
54 },
55 .stop => {
56 self.counters.stop_events +|= 1;
57 self.stop_sequence = parsed.seq;
58 },
59 else => {},
60 }
61 }
62
63 pub fn recordFlightReport(self: *Tracker, report: transport.Report) void {
64 self.flight_report = report;
65 }
66
67 pub fn integrity(self: Tracker, unbalanced_event_count: u64) Integrity {
68 var result: Integrity = .{
69 .status = "complete",
70 .action = "none",
71 .message = null,
72 .event_count = self.counters.events,
73 .sequenced_event_count = self.counters.sequenced_events,
74 .unsequenced_event_count = self.counters.unsequenced_events,
75 .first_sequence = self.first_sequence,
76 .last_sequence = self.last_sequence,
77 .sequence_gap_count = self.counters.sequence_gaps,
78 .missing_sequence_event_count = self.counters.missing_sequence_events,
79 .sequence_regression_count = self.counters.sequence_regressions,
80 .start_event_count = self.counters.start_events,
81 .stop_event_count = self.counters.stop_events,
82 .start_sequence = self.start_sequence,
83 .stop_sequence = self.stop_sequence,
84 .unbalanced_event_count = unbalanced_event_count,
85 .flight_report = self.flight_report,
86 };
87 classify(&result);
88 return result;
89 }
90
91 fn recordSequence(self: *Tracker, sequence: u64) void {
92 if (sequence == 0) {
93 self.counters.unsequenced_events +|= 1;
94 return;
95 }
96 self.counters.sequenced_events +|= 1;
97 const previous = self.last_sequence orelse {
98 self.first_sequence = sequence;
99 self.last_sequence = sequence;
100 if (sequence > 1) {
101 self.counters.sequence_gaps +|= 1;
102 self.counters.missing_sequence_events +|= sequence - 1;
103 }
104 return;
105 };
106 if (sequence <= previous) {
107 self.counters.sequence_regressions +|= 1;
108 } else if (sequence - previous > 1) {
109 self.counters.sequence_gaps +|= 1;
110 self.counters.missing_sequence_events +|= sequence - previous - 1;
111 }
112 self.last_sequence = sequence;
113 }
114 };
115
116 pub fn writeText(writer: *std.Io.Writer, integrity: Integrity) !void {
117 try writer.print(
118 "tracy capture_integrity={s} events={d} sequenced_events={d} " ++
119 "unsequenced_events={d} sequence_gaps={d} missing_sequence_events={d} " ++
120 "sequence_regressions={d} start_events={d} stop_events={d} " ++
121 "unbalanced_events={d}",
122 .{
123 integrity.status,
124 integrity.event_count,
125 integrity.sequenced_event_count,
126 integrity.unsequenced_event_count,
127 integrity.sequence_gap_count,
128 integrity.missing_sequence_event_count,
129 integrity.sequence_regression_count,
130 integrity.start_event_count,
131 integrity.stop_event_count,
132 integrity.unbalanced_event_count,
133 },
134 );
135 try writer.writeAll(" first_sequence=");
136 try writeOptionalU64Text(writer, integrity.first_sequence);
137 try writer.writeAll(" last_sequence=");
138 try writeOptionalU64Text(writer, integrity.last_sequence);
139 try writer.writeAll(" start_sequence=");
140 try writeOptionalU64Text(writer, integrity.start_sequence);
141 try writer.writeAll(" stop_sequence=");
142 try writeOptionalU64Text(writer, integrity.stop_sequence);
143 try writer.writeByte('\n');
144 if (integrity.flight_report) |report| try writeFlightText(writer, report);
145 if (integrity.message) |message| {
146 try writer.print("tracy capture caveat={s} action={s} message=", .{
147 integrity.status,
148 integrity.action,
149 });
150 try pretty_json.writeString(writer, message);
151 try writer.writeByte('\n');
152 }
153 }
154
155 pub fn writeFields(object: pretty_json.Object, integrity: Integrity) !void {
156 const capture = try object.object("capture_integrity");
157 try capture.field("method", method);
158 try capture.field("status", integrity.status);
159 try capture.field("action", integrity.action);
160 try capture.field("message", integrity.message);
161 try capture.field("event_count", integrity.event_count);
162 try capture.field("sequenced_event_count", integrity.sequenced_event_count);
163 try capture.field("unsequenced_event_count", integrity.unsequenced_event_count);
164 try capture.field("first_sequence", integrity.first_sequence);
165 try capture.field("last_sequence", integrity.last_sequence);
166 try capture.field("sequence_gap_count", integrity.sequence_gap_count);
167 try capture.field("missing_sequence_event_count", integrity.missing_sequence_event_count);
168 try capture.field("sequence_regression_count", integrity.sequence_regression_count);
169 try capture.field("start_event_count", integrity.start_event_count);
170 try capture.field("stop_event_count", integrity.stop_event_count);
171 try capture.field("unbalanced_event_count", integrity.unbalanced_event_count);
172 try capture.field("start_sequence", integrity.start_sequence);
173 try capture.field("stop_sequence", integrity.stop_sequence);
174 if (integrity.flight_report) |report| {
175 const flight = try capture.object("flight_report");
176 try report.writeFields(flight);
177 try flight.end();
178 } else {
179 try capture.field("flight_report", null);
180 }
181 const limits = try capture.array("limits");
182 try limits.element(
183 "sequence gaps are lower-bound missing-row evidence; " ++
184 "source loss counters remain authoritative",
185 );
186 try limits.element(
187 "complete sequence and lifecycle evidence does not prove low perturbation or " ++
188 "representative coverage",
189 );
190 try limits.end();
191 try capture.end();
192 }
193
194 fn writeFlightText(writer: *std.Io.Writer, report: transport.Report) !void {
195 try writer.print(
196 "tracy flight policy={s} state={s} capacity_bytes={d} retained_bytes={d} " ++
197 "event_capacity_bytes={d} writer_capacity_bytes={d} " ++
198 "observed_events={d} stored_events={d} retained_events={d} " ++
199 "overwritten_events={d} dropped_events={d} oversized_events={d} " ++
200 "partial_event_bytes={d} discarding_oversized_event={}\n",
201 .{
202 report.policy.tag(),
203 report.state.tag(),
204 report.capacity_bytes,
205 report.retained_bytes,
206 report.event_capacity_bytes,
207 report.writer_capacity_bytes,
208 report.observed_events,
209 report.stored_events,
210 report.retained_events,
211 report.overwritten_events,
212 report.dropped_events,
213 report.oversized_events,
214 report.partial_event_bytes,
215 report.discarding_oversized_event,
216 },
217 );
218 }
219
220 fn classify(result: *Integrity) void {
221 if (result.event_count == 0) return set(
222 result,
223 "no_events",
224 "capture_trace_events",
225 "Tracy trace contains no events",
226 );
227 if (result.sequence_regression_count != 0) return set(
228 result,
229 "non_monotonic_sequence",
230 "inspect_trace_transport",
231 "Tracy event sequence is non-monotonic; treat derived reports as corrupt evidence",
232 );
233 if (result.missing_sequence_event_count != 0) return set(
234 result,
235 "sequence_gaps",
236 "inspect_loss_counters_or_increase_capacity",
237 "Tracy event sequence has gaps; treat derived reports as partial evidence",
238 );
239 if (result.unsequenced_event_count != 0) return set(
240 result,
241 "missing_sequence_metadata",
242 "recapture_with_sequence_metadata",
243 "one or more Tracy events lack sequence metadata; capture completeness is unknown",
244 );
245 classifyLifecycle(result);
246 }
247
248 fn classifyLifecycle(result: *Integrity) void {
249 if (result.start_event_count > 1 or result.stop_event_count > 1) return set(
250 result,
251 "multiple_trace_sessions",
252 "capture_one_trace_session",
253 "Tracy trace mixes multiple lifecycles; capture completeness is ambiguous",
254 );
255 if (result.start_event_count == 0) return set(
256 result,
257 "missing_start_event",
258 "capture_complete_trace_lifecycle",
259 "Tracy trace does not contain its start event; treat it as partial evidence",
260 );
261 if (result.stop_event_count == 0) return set(
262 result,
263 "missing_stop_event",
264 "capture_complete_trace_lifecycle",
265 "Tracy trace does not contain its stop event; treat terminal state as partial evidence",
266 );
267 if (result.start_sequence != result.first_sequence or
268 result.stop_sequence != result.last_sequence)
269 {
270 return set(
271 result,
272 "lifecycle_not_bounded",
273 "capture_complete_trace_lifecycle",
274 "Tracy start and stop events do not bound the event sequence; " ++
275 "treat it as partial evidence",
276 );
277 }
278 if (result.unbalanced_event_count != 0) set(
279 result,
280 "unbalanced_events",
281 "inspect_trace_lifecycles",
282 "Tracy trace contains unmatched lifecycle events; inspect capture integrity details",
283 );
284 }
285
286 fn set(result: *Integrity, status: []const u8, action: []const u8, message: []const u8) void {
287 result.status = status;
288 result.action = action;
289 result.message = message;
290 }
291
292 fn writeOptionalU64Text(writer: *std.Io.Writer, value: ?u64) !void {
293 if (value) |actual| try writer.print("{d}", .{actual}) else try writer.writeAll("none");
294 }
295
296 test "capture tracker classifies missing sequences" {
297 var trace = std.Io.Writer.Allocating.init(std.testing.allocator);
298 defer trace.deinit();
299 try (event.TraceEvent{ .seq = 1, .kind = .start }).writeJsonLine(&trace.writer);
300 try (event.TraceEvent{ .seq = 4, .kind = .message }).writeJsonLine(&trace.writer);
301 try (event.TraceEvent{ .seq = 5, .kind = .stop }).writeJsonLine(&trace.writer);
302
303 var tracker: Tracker = .{};
304 var lines = std.mem.tokenizeScalar(u8, trace.written(), '\n');
305 while (lines.next()) |line| {
306 var parsed = try event.parseLine(std.testing.allocator, line);
307 tracker.record(parsed);
308 parsed.deinit();
309 }
310 const integrity = tracker.integrity(0);
311 try std.testing.expectEqualStrings("sequence_gaps", integrity.status);
312 try std.testing.expectEqual(@as(u64, 2), integrity.missing_sequence_event_count);
313 }
314
315 test "capture tracker writes canonical text and json evidence" {
316 var tracker: Tracker = .{};
317 tracker.counters.events = 2;
318 tracker.counters.unsequenced_events = 2;
319 const integrity = tracker.integrity(0);
320 var text = std.Io.Writer.Allocating.init(std.testing.allocator);
321 defer text.deinit();
322 try writeText(&text.writer, integrity);
323 try std.testing.expect(std.mem.indexOf(
324 u8,
325 text.written(),
326 "capture_integrity=missing_sequence_metadata",
327 ) != null);
328 var json = std.Io.Writer.Allocating.init(std.testing.allocator);
329 defer json.deinit();
330 var stream = pretty_json.Writer.init(&json.writer, .minified);
331 const object = try stream.object();
332 try object.field("seed", 0);
333 try writeFields(object, integrity);
334 try object.end();
335 var parsed = try std.json.parseFromSlice(
336 std.json.Value,
337 std.testing.allocator,
338 json.written(),
339 .{},
340 );
341 defer parsed.deinit();
342 }
343
344 test "capture tracker preserves an exact flight report" {
345 const report: transport.Report = .{
346 .policy = .overwrite_oldest,
347 .state = .accepting,
348 .capacity_bytes = 256,
349 .retained_bytes = 128,
350 .event_capacity_bytes = 64,
351 .writer_capacity_bytes = 32,
352 .observed_events = 9,
353 .stored_events = 9,
354 .retained_events = 6,
355 .overwritten_events = 3,
356 .dropped_events = 0,
357 .oversized_events = 0,
358 .partial_event_bytes = 0,
359 .discarding_oversized_event = false,
360 };
361 var tracker: Tracker = .{};
362 tracker.recordFlightReport(report);
363 const integrity = tracker.integrity(0);
364 try std.testing.expectEqualDeep(report, integrity.flight_report.?);
365
366 var text = std.Io.Writer.Allocating.init(std.testing.allocator);
367 defer text.deinit();
368 try writeText(&text.writer, integrity);
369 try std.testing.expect(std.mem.indexOf(
370 u8,
371 text.written(),
372 "overwritten_events=3",
373 ) != null);
374
375 var json = std.Io.Writer.Allocating.init(std.testing.allocator);
376 defer json.deinit();
377 var stream = pretty_json.Writer.init(&json.writer, .minified);
378 const object = try stream.object();
379 try object.field("seed", 0);
380 try writeFields(object, integrity);
381 try object.end();
382 try std.testing.expect(std.mem.indexOf(
383 u8,
384 json.written(),
385 "\"flight_report\":{\"schema\":\"tracy.flight/v0\"",
386 ) != null);
387 try std.testing.expect(std.mem.indexOf(
388 u8,
389 json.written(),
390 "\"overwritten_events\":3",
391 ) != null);
392 }