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 }