lib/memtrace/src/event.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const observe = @import("alloc_observe");
3 const pretty_json = @import("pretty").json;
4
5 pub const format_version: u32 = 3;
6
7 pub const Layer = enum {
8 backing_boundary,
9 logical_allocator,
10 physical_page,
11
12 pub fn tag(self: Layer) []const u8 {
13 return @tagName(self);
14 }
15
16 pub fn fromTag(text: []const u8) ?Layer {
17 return std.meta.stringToEnum(Layer, text);
18 }
19 };
20
21 pub const LayerFilter = enum {
22 backing,
23 logical,
24 physical,
25 all,
26
27 pub fn includes(self: LayerFilter, layer: Layer) bool {
28 return switch (self) {
29 .backing => layer == .backing_boundary,
30 .logical => layer == .logical_allocator,
31 .physical => layer == .physical_page,
32 .all => true,
33 };
34 }
35
36 pub fn tag(self: LayerFilter) []const u8 {
37 return @tagName(self);
38 }
39
40 pub fn fromTag(text: []const u8) ?LayerFilter {
41 return std.meta.stringToEnum(LayerFilter, text);
42 }
43 };
44
45 pub const Kind = enum {
46 trace_start,
47 trace_stop,
48 allocator,
49 scope_enter,
50 scope_exit,
51 alloc,
52 free,
53 release,
54 resize,
55 remap,
56 lifecycle,
57 map,
58 unmap,
59 protect,
60 discard,
61 decommit,
62 snapshot,
63 census,
64 advise,
65
66 pub fn tag(self: Kind) []const u8 {
67 return switch (self) {
68 .trace_start => "trace.start",
69 .trace_stop => "trace.stop",
70 .allocator => "allocator",
71 .scope_enter => "scope.enter",
72 .scope_exit => "scope.exit",
73 .alloc => "alloc",
74 .free => "free",
75 .release => "release",
76 .resize => "resize",
77 .remap => "remap",
78 .lifecycle => "lifecycle",
79 .map => "map",
80 .unmap => "unmap",
81 .protect => "protect",
82 .discard => "discard",
83 .decommit => "decommit",
84 .advise => "advise",
85 .snapshot => "snapshot",
86 .census => "census",
87 };
88 }
89
90 pub fn fromTag(text: []const u8) ?Kind {
91 if (std.mem.eql(u8, text, "trace.start")) return .trace_start;
92 if (std.mem.eql(u8, text, "trace.stop")) return .trace_stop;
93 if (std.mem.eql(u8, text, "allocator")) return .allocator;
94 if (std.mem.eql(u8, text, "scope.enter")) return .scope_enter;
95 if (std.mem.eql(u8, text, "scope.exit")) return .scope_exit;
96 if (std.mem.eql(u8, text, "alloc")) return .alloc;
97 if (std.mem.eql(u8, text, "free")) return .free;
98 if (std.mem.eql(u8, text, "release")) return .release;
99 if (std.mem.eql(u8, text, "resize")) return .resize;
100 if (std.mem.eql(u8, text, "remap")) return .remap;
101 if (std.mem.eql(u8, text, "lifecycle")) return .lifecycle;
102 if (std.mem.eql(u8, text, "map")) return .map;
103 if (std.mem.eql(u8, text, "unmap")) return .unmap;
104 if (std.mem.eql(u8, text, "protect")) return .protect;
105 if (std.mem.eql(u8, text, "discard")) return .discard;
106 if (std.mem.eql(u8, text, "decommit")) return .decommit;
107 if (std.mem.eql(u8, text, "advise")) return .advise;
108 if (std.mem.eql(u8, text, "snapshot")) return .snapshot;
109 if (std.mem.eql(u8, text, "census")) return .census;
110 return null;
111 }
112
113 pub fn isMemoryOperation(self: Kind) bool {
114 return switch (self) {
115 .alloc,
116 .free,
117 .release,
118 .resize,
119 .remap,
120 .map,
121 .unmap,
122 .protect,
123 .discard,
124 .decommit,
125 .advise,
126 => true,
127 else => false,
128 };
129 }
130 };
131
132 pub const Event = struct {
133 seq: u64 = 0,
134 kind: Kind,
135 allocator_id: u32 = 0,
136 allocation_id: u64 = 0,
137 scope_id: u32 = 0,
138 label_id: u32 = 0,
139 address: usize = 0,
140 old_address: usize = 0,
141 len: usize = 0,
142 old_len: usize = 0,
143 alignment: u32 = 0,
144 return_address: usize = 0,
145 stack_id: u32 = 0,
146 succeeded: bool = true,
147 tracked: bool = true,
148 live_bytes: usize = 0,
149 recording_failures: u64 = 0,
150 retains_freed_memory: bool = false,
151 layer: Layer = .backing_boundary,
152 operation_id: u64 = 0,
153 parent_operation_id: u64 = 0,
154 producer_id: u64 = 0,
155 producer: observe.Producer = .boundary,
156 generation: u64 = 0,
157 owner_cookie: usize = 0,
158 lifecycle_disposition: observe.LifecycleDisposition = .none,
159 lifecycle_reason: observe.LifecycleReason = .none,
160 lifecycle_instrumented: bool = false,
161 observation_prefix_complete: bool = true,
162
163 pub fn writeJsonLine(
164 self: Event,
165 writer: *std.Io.Writer,
166 label_text: ?[]const u8,
167 scope_text: ?[]const u8,
168 ) !void {
169 var stream = pretty_json.Writer.init(writer, .minified);
170 const object = try stream.object();
171 try object.field("v", format_version);
172 try object.field("seq", self.seq);
173 try object.field("kind", self.kind.tag());
174 if (self.allocator_id != 0) try object.field("allocator_id", self.allocator_id);
175 if (self.allocation_id != 0) try object.field("allocation_id", self.allocation_id);
176 if (self.scope_id != 0) try object.field("scope_id", self.scope_id);
177 if (self.address != 0) try object.field("address", self.address);
178 if (self.old_address != 0 and self.old_address != self.address) {
179 try object.field("old_address", self.old_address);
180 }
181 if (self.len != 0) try object.field("len", self.len);
182 if (self.old_len != 0) try object.field("old_len", self.old_len);
183 if (self.alignment != 0) try object.field("alignment", self.alignment);
184 if (self.return_address != 0) try object.field("return_address", self.return_address);
185 if (self.stack_id != 0) try object.field("stack_id", self.stack_id);
186 if (!self.succeeded) try object.field("succeeded", false);
187 if (!self.tracked) try object.field("tracked", false);
188 if (self.recording_failures != 0) {
189 try object.field("recording_failures", self.recording_failures);
190 }
191 if (self.retains_freed_memory) try object.field("retains_freed_memory", true);
192 if (self.layer != .backing_boundary) try object.field("layer", self.layer.tag());
193 if (self.operation_id != 0) try object.field("operation_id", self.operation_id);
194 if (self.parent_operation_id != 0) {
195 try object.field("parent_operation_id", self.parent_operation_id);
196 }
197 if (self.producer_id != 0) try object.field("producer_id", self.producer_id);
198 if (self.producer != .boundary) try object.field("producer", @tagName(self.producer));
199 if (self.generation != 0) try object.field("generation", self.generation);
200 if (self.owner_cookie != 0) try object.field("owner_cookie", self.owner_cookie);
201 if (self.lifecycle_disposition != .none) {
202 try object.field(
203 "lifecycle_disposition",
204 @tagName(self.lifecycle_disposition),
205 );
206 }
207 if (self.lifecycle_reason != .none) {
208 try object.field("lifecycle_reason", @tagName(self.lifecycle_reason));
209 }
210 if (self.lifecycle_instrumented) {
211 try object.field("lifecycle_instrumented", true);
212 }
213 if (!self.observation_prefix_complete) {
214 try object.field("observation_prefix_complete", false);
215 }
216 if (label_text) |text| try object.field("label", text);
217 if (scope_text) |text| try object.field("scope", text);
218 try object.endLine();
219 }
220 };
221
222 pub const ReplayEvent = struct {
223 seq: ?u64 = null,
224 kind: Kind,
225 allocator_id: u32 = 0,
226 allocation_id: u64 = 0,
227 scope_id: u32 = 0,
228 address: u64 = 0,
229 old_address: u64 = 0,
230 len: usize = 0,
231 old_len: usize = 0,
232 alignment: u32 = 0,
233 return_address: u64 = 0,
234 stack_id: u32 = 0,
235 succeeded: bool = true,
236 tracked: bool = true,
237 recording_failures: u64 = 0,
238 scope: []const u8 = "root",
239 retains_freed_memory: bool = false,
240 layer: Layer = .backing_boundary,
241 operation_id: u64 = 0,
242 parent_operation_id: u64 = 0,
243 producer_id: u64 = 0,
244 producer: observe.Producer = .boundary,
245 generation: u64 = 0,
246 owner_cookie: u64 = 0,
247 lifecycle_disposition: observe.LifecycleDisposition = .none,
248 lifecycle_reason: observe.LifecycleReason = .none,
249 lifecycle_instrumented: bool = false,
250 observation_prefix_complete: bool = true,
251 };
252
253 const CanonicalCursor = struct {
254 line: []const u8,
255 index: usize = 0,
256
257 fn expect(self: *CanonicalCursor, expected: []const u8) !void {
258 if (!std.mem.startsWith(u8, self.line[self.index..], expected)) return error.UnsupportedFastEvent;
259 self.index += expected.len;
260 }
261
262 fn consume(self: *CanonicalCursor, expected: []const u8) bool {
263 if (!std.mem.startsWith(u8, self.line[self.index..], expected)) return false;
264 self.index += expected.len;
265 return true;
266 }
267
268 fn unsigned(self: *CanonicalCursor) !u64 {
269 var value: u64 = 0;
270 var digits: usize = 0;
271 while (self.index < self.line.len and self.line[self.index] >= '0' and self.line[self.index] <= '9') : (self.index += 1) {
272 const digit: u64 = self.line[self.index] - '0';
273 if (digits == 19) {
274 if (value > std.math.maxInt(u64) / 10 or
275 (value == std.math.maxInt(u64) / 10 and digit > std.math.maxInt(u64) % 10)) return error.InvalidEventJson;
276 } else if (digits > 19) {
277 return error.UnsupportedFastEvent;
278 }
279 value = value * 10 + digit;
280 digits += 1;
281 }
282 if (digits == 0) return error.UnsupportedFastEvent;
283 return value;
284 }
285
286 fn skipUnsigned(self: *CanonicalCursor) !void {
287 const start = self.index;
288 while (self.index < self.line.len and self.line[self.index] >= '0' and self.line[self.index] <= '9') : (self.index += 1) {}
289 if (self.index == start) return error.UnsupportedFastEvent;
290 }
291
292 fn string(self: *CanonicalCursor) ![]const u8 {
293 try self.expect("\"");
294 const start = self.index;
295 while (self.index < self.line.len) : (self.index += 1) {
296 switch (self.line[self.index]) {
297 '"' => {
298 const value = self.line[start..self.index];
299 self.index += 1;
300 return value;
301 },
302 '\\' => return error.UnsupportedFastEvent,
303 else => {},
304 }
305 }
306 return error.InvalidEventJson;
307 }
308
309 fn boolean(self: *CanonicalCursor) !bool {
310 if (self.consume("true")) return true;
311 if (self.consume("false")) return false;
312 return error.UnsupportedFastEvent;
313 }
314
315 fn finish(self: *CanonicalCursor) !void {
316 try self.expect("}");
317 if (self.index != self.line.len) return error.UnsupportedFastEvent;
318 }
319 };
320
321 pub fn parseReplayFast(line: []const u8) !ReplayEvent {
322 return parseSparseCanonical(line) catch |sparse_err| switch (sparse_err) {
323 error.UnsupportedFastEvent => parseCanonical(line) catch |dense_err| switch (dense_err) {
324 error.UnsupportedFastEvent => parseFlat(line),
325 else => return dense_err,
326 },
327 else => return sparse_err,
328 };
329 }
330
331 fn parseSparseCanonical(line: []const u8) !ReplayEvent {
332 var cursor = CanonicalCursor{ .line = line };
333 try cursor.expect("{\"v\":");
334 const version = try cursor.unsigned();
335 if (version != format_version) return error.UnsupportedTraceVersion;
336 try cursor.expect(",\"seq\":");
337 const seq = try cursor.unsigned();
338 try cursor.expect(",\"kind\":");
339 const kind = Kind.fromTag(try cursor.string()) orelse
340 return error.InvalidEventJson;
341 const allocator_id = if (cursor.consume(",\"allocator_id\":"))
342 std.math.cast(u32, try cursor.unsigned()) orelse
343 return error.InvalidEventJson
344 else
345 0;
346 const allocation_id = if (cursor.consume(",\"allocation_id\":"))
347 try cursor.unsigned()
348 else
349 0;
350 const scope_id = if (cursor.consume(",\"scope_id\":"))
351 std.math.cast(u32, try cursor.unsigned()) orelse
352 return error.InvalidEventJson
353 else
354 0;
355 const address = if (cursor.consume(",\"address\":"))
356 try cursor.unsigned()
357 else
358 0;
359 const old_address = if (cursor.consume(",\"old_address\":"))
360 try cursor.unsigned()
361 else
362 address;
363 const len = if (cursor.consume(",\"len\":"))
364 std.math.cast(usize, try cursor.unsigned()) orelse
365 return error.InvalidEventJson
366 else
367 0;
368 const old_len = if (cursor.consume(",\"old_len\":"))
369 std.math.cast(usize, try cursor.unsigned()) orelse
370 return error.InvalidEventJson
371 else
372 0;
373 const alignment = if (cursor.consume(",\"alignment\":"))
374 std.math.cast(u32, try cursor.unsigned()) orelse
375 return error.InvalidEventJson
376 else
377 0;
378 const return_address = if (cursor.consume(",\"return_address\":"))
379 try cursor.unsigned()
380 else
381 0;
382 const stack_id = if (cursor.consume(",\"stack_id\":"))
383 std.math.cast(u32, try cursor.unsigned()) orelse
384 return error.InvalidEventJson
385 else
386 0;
387 const succeeded = if (cursor.consume(",\"succeeded\":"))
388 try cursor.boolean()
389 else
390 true;
391 const tracked = if (cursor.consume(",\"tracked\":"))
392 try cursor.boolean()
393 else
394 true;
395 const recording_failures =
396 if (cursor.consume(",\"recording_failures\":"))
397 try cursor.unsigned()
398 else
399 0;
400 const retains_freed_memory =
401 if (cursor.consume(",\"retains_freed_memory\":"))
402 try cursor.boolean()
403 else
404 false;
405 const layer = if (cursor.consume(",\"layer\":"))
406 Layer.fromTag(try cursor.string()) orelse
407 return error.InvalidEventJson
408 else
409 .backing_boundary;
410 const operation_id = if (cursor.consume(",\"operation_id\":"))
411 try cursor.unsigned()
412 else
413 0;
414 const parent_operation_id =
415 if (cursor.consume(",\"parent_operation_id\":"))
416 try cursor.unsigned()
417 else
418 0;
419 const producer_id = if (cursor.consume(",\"producer_id\":"))
420 try cursor.unsigned()
421 else
422 0;
423 const producer = if (cursor.consume(",\"producer\":"))
424 std.meta.stringToEnum(observe.Producer, try cursor.string()) orelse
425 return error.InvalidEventJson
426 else
427 .boundary;
428 const generation = if (cursor.consume(",\"generation\":"))
429 try cursor.unsigned()
430 else
431 0;
432 const owner_cookie = if (cursor.consume(",\"owner_cookie\":"))
433 try cursor.unsigned()
434 else
435 0;
436 const lifecycle_disposition =
437 if (cursor.consume(",\"lifecycle_disposition\":"))
438 std.meta.stringToEnum(
439 observe.LifecycleDisposition,
440 try cursor.string(),
441 ) orelse return error.InvalidEventJson
442 else
443 .none;
444 const lifecycle_reason =
445 if (cursor.consume(",\"lifecycle_reason\":"))
446 std.meta.stringToEnum(
447 observe.LifecycleReason,
448 try cursor.string(),
449 ) orelse return error.InvalidEventJson
450 else
451 .none;
452 const lifecycle_instrumented =
453 if (cursor.consume(",\"lifecycle_instrumented\":"))
454 try cursor.boolean()
455 else
456 false;
457 const observation_prefix_complete =
458 if (cursor.consume(",\"observation_prefix_complete\":"))
459 try cursor.boolean()
460 else
461 true;
462 if (cursor.consume(",\"label\":")) _ = try cursor.string();
463 const scope = if (cursor.consume(",\"scope\":"))
464 try cursor.string()
465 else
466 "";
467 try cursor.finish();
468 return .{
469 .seq = seq,
470 .kind = kind,
471 .allocator_id = allocator_id,
472 .allocation_id = allocation_id,
473 .scope_id = scope_id,
474 .address = address,
475 .old_address = old_address,
476 .len = len,
477 .old_len = old_len,
478 .alignment = alignment,
479 .return_address = return_address,
480 .stack_id = stack_id,
481 .succeeded = succeeded,
482 .tracked = tracked,
483 .recording_failures = recording_failures,
484 .scope = scope,
485 .retains_freed_memory = retains_freed_memory,
486 .layer = layer,
487 .operation_id = operation_id,
488 .parent_operation_id = parent_operation_id,
489 .producer_id = producer_id,
490 .producer = producer,
491 .generation = generation,
492 .owner_cookie = owner_cookie,
493 .lifecycle_disposition = lifecycle_disposition,
494 .lifecycle_reason = lifecycle_reason,
495 .lifecycle_instrumented = lifecycle_instrumented,
496 .observation_prefix_complete = observation_prefix_complete,
497 };
498 }
499
500 pub fn replayFromJsonObject(object: std.json.ObjectMap) !ReplayEvent {
501 const version = try jsonU64(object.get("v") orelse return error.InvalidEventJson);
502 if (version != format_version) return error.UnsupportedTraceVersion;
503
504 const kind_text = try jsonString(object.get("kind") orelse return error.InvalidEventJson);
505 const kind = Kind.fromTag(kind_text) orelse return error.InvalidEventJson;
506 const allocator_id = std.math.cast(
507 u32,
508 try jsonU64Default(object, "allocator_id", 0),
509 ) orelse return error.InvalidEventJson;
510 const allocation_id = try jsonU64Default(object, "allocation_id", 0);
511 const address = try jsonU64Default(object, "address", 0);
512 const scope_id = std.math.cast(
513 u32,
514 try jsonU64Default(object, "scope_id", 0),
515 ) orelse return error.InvalidEventJson;
516 const len = std.math.cast(
517 usize,
518 try jsonU64Default(object, "len", 0),
519 ) orelse return error.InvalidEventJson;
520 const old_len = std.math.cast(
521 usize,
522 try jsonU64Default(object, "old_len", 0),
523 ) orelse return error.InvalidEventJson;
524 return .{
525 .seq = try jsonOptionalU64(object, "seq"),
526 .kind = kind,
527 .allocator_id = allocator_id,
528 .allocation_id = allocation_id,
529 .scope_id = scope_id,
530 .address = address,
531 .old_address = try jsonU64Default(object, "old_address", address),
532 .len = len,
533 .old_len = old_len,
534 .alignment = std.math.cast(
535 u32,
536 try jsonU64Default(object, "alignment", 0),
537 ) orelse return error.InvalidEventJson,
538 .return_address = try jsonU64Default(object, "return_address", 0),
539 .stack_id = std.math.cast(
540 u32,
541 try jsonU64Default(object, "stack_id", 0),
542 ) orelse return error.InvalidEventJson,
543 .succeeded = try jsonBoolDefault(object, "succeeded", true),
544 .tracked = try jsonBoolDefault(object, "tracked", true),
545 .recording_failures = try jsonU64Default(
546 object,
547 "recording_failures",
548 0,
549 ),
550 .scope = try jsonStringDefault(object, "scope", ""),
551 .retains_freed_memory = try jsonBoolDefault(object, "retains_freed_memory", false),
552 .layer = Layer.fromTag(
553 try jsonStringDefault(
554 object,
555 "layer",
556 Layer.backing_boundary.tag(),
557 ),
558 ) orelse return error.InvalidEventJson,
559 .operation_id = try jsonU64Default(object, "operation_id", 0),
560 .parent_operation_id = try jsonU64Default(
561 object,
562 "parent_operation_id",
563 0,
564 ),
565 .producer_id = try jsonU64Default(object, "producer_id", 0),
566 .producer = std.meta.stringToEnum(
567 observe.Producer,
568 try jsonStringDefault(object, "producer", "boundary"),
569 ) orelse return error.InvalidEventJson,
570 .generation = try jsonU64Default(object, "generation", 0),
571 .owner_cookie = try jsonU64Default(object, "owner_cookie", 0),
572 .lifecycle_disposition = std.meta.stringToEnum(
573 observe.LifecycleDisposition,
574 try jsonStringDefault(
575 object,
576 "lifecycle_disposition",
577 "none",
578 ),
579 ) orelse return error.InvalidEventJson,
580 .lifecycle_reason = std.meta.stringToEnum(
581 observe.LifecycleReason,
582 try jsonStringDefault(object, "lifecycle_reason", "none"),
583 ) orelse return error.InvalidEventJson,
584 .lifecycle_instrumented = try jsonBoolDefault(
585 object,
586 "lifecycle_instrumented",
587 false,
588 ),
589 .observation_prefix_complete = try jsonBoolDefault(
590 object,
591 "observation_prefix_complete",
592 true,
593 ),
594 };
595 }
596
597 fn parseCanonical(line: []const u8) !ReplayEvent {
598 var cursor = CanonicalCursor{ .line = line };
599 try cursor.expect("{\"v\":");
600 const version = try cursor.unsigned();
601 if (version != format_version) return error.UnsupportedTraceVersion;
602 try cursor.expect(",\"seq\":");
603 const seq = try cursor.unsigned();
604 try cursor.expect(",\"kind\":");
605 const kind = Kind.fromTag(try cursor.string()) orelse return error.InvalidEventJson;
606 try cursor.expect(",\"allocator_id\":");
607 const allocator_id = std.math.cast(u32, try cursor.unsigned()) orelse return error.InvalidEventJson;
608 try cursor.expect(",\"allocation_id\":");
609 const allocation_id = try cursor.unsigned();
610 try cursor.expect(",\"scope_id\":");
611 const scope_id = std.math.cast(u32, try cursor.unsigned()) orelse
612 return error.InvalidEventJson;
613 try cursor.expect(",\"label_id\":");
614 try cursor.skipUnsigned();
615 try cursor.expect(",\"address\":");
616 const address = try cursor.unsigned();
617 try cursor.expect(",\"old_address\":");
618 const old_address = try cursor.unsigned();
619 try cursor.expect(",\"len\":");
620 const len = std.math.cast(usize, try cursor.unsigned()) orelse return error.InvalidEventJson;
621 try cursor.expect(",\"old_len\":");
622 const old_len = std.math.cast(usize, try cursor.unsigned()) orelse return error.InvalidEventJson;
623 try cursor.expect(",\"alignment\":");
624 const alignment = std.math.cast(u32, try cursor.unsigned()) orelse
625 return error.InvalidEventJson;
626 try cursor.expect(",\"return_address\":");
627 const return_address = try cursor.unsigned();
628 const stack_id = if (cursor.consume(",\"stack_id\":"))
629 std.math.cast(u32, try cursor.unsigned()) orelse
630 return error.InvalidEventJson
631 else
632 0;
633 try cursor.expect(",\"succeeded\":");
634 const succeeded = try cursor.boolean();
635 const tracked = if (cursor.consume(",\"tracked\":"))
636 try cursor.boolean()
637 else
638 true;
639 try cursor.expect(",\"live_bytes\":");
640 try cursor.skipUnsigned();
641 try cursor.expect(",\"recording_failures\":");
642 const recording_failures = try cursor.unsigned();
643 try cursor.expect(",\"retains_freed_memory\":");
644 const retains_freed_memory = try cursor.boolean();
645 const layer = if (cursor.consume(",\"layer\":"))
646 Layer.fromTag(try cursor.string()) orelse
647 return error.InvalidEventJson
648 else
649 .backing_boundary;
650 const operation_id = if (cursor.consume(",\"operation_id\":"))
651 try cursor.unsigned()
652 else
653 0;
654 const parent_operation_id = if (cursor.consume(",\"parent_operation_id\":"))
655 try cursor.unsigned()
656 else
657 0;
658 const producer_id = if (cursor.consume(",\"producer_id\":"))
659 try cursor.unsigned()
660 else
661 0;
662 const producer = if (cursor.consume(",\"producer\":"))
663 std.meta.stringToEnum(observe.Producer, try cursor.string()) orelse
664 return error.InvalidEventJson
665 else
666 .boundary;
667 const generation = if (cursor.consume(",\"generation\":"))
668 try cursor.unsigned()
669 else
670 0;
671 const owner_cookie = if (cursor.consume(",\"owner_cookie\":"))
672 try cursor.unsigned()
673 else
674 0;
675 const lifecycle_disposition =
676 if (cursor.consume(",\"lifecycle_disposition\":"))
677 std.meta.stringToEnum(
678 observe.LifecycleDisposition,
679 try cursor.string(),
680 ) orelse return error.InvalidEventJson
681 else
682 .none;
683 const lifecycle_reason =
684 if (cursor.consume(",\"lifecycle_reason\":"))
685 std.meta.stringToEnum(
686 observe.LifecycleReason,
687 try cursor.string(),
688 ) orelse return error.InvalidEventJson
689 else
690 .none;
691 const lifecycle_instrumented =
692 if (cursor.consume(",\"lifecycle_instrumented\":"))
693 try cursor.boolean()
694 else
695 false;
696 const observation_prefix_complete =
697 if (cursor.consume(",\"observation_prefix_complete\":"))
698 try cursor.boolean()
699 else
700 true;
701 if (cursor.consume(",\"label\":")) _ = try cursor.string();
702 const scope = if (cursor.consume(",\"scope\":")) try cursor.string() else "";
703 try cursor.finish();
704 return .{
705 .seq = seq,
706 .kind = kind,
707 .allocator_id = allocator_id,
708 .allocation_id = allocation_id,
709 .scope_id = scope_id,
710 .address = address,
711 .old_address = old_address,
712 .len = len,
713 .old_len = old_len,
714 .alignment = alignment,
715 .return_address = return_address,
716 .stack_id = stack_id,
717 .succeeded = succeeded,
718 .tracked = tracked,
719 .recording_failures = recording_failures,
720 .scope = scope,
721 .retains_freed_memory = retains_freed_memory,
722 .layer = layer,
723 .operation_id = operation_id,
724 .parent_operation_id = parent_operation_id,
725 .producer_id = producer_id,
726 .producer = producer,
727 .generation = generation,
728 .owner_cookie = owner_cookie,
729 .lifecycle_disposition = lifecycle_disposition,
730 .lifecycle_reason = lifecycle_reason,
731 .lifecycle_instrumented = lifecycle_instrumented,
732 .observation_prefix_complete = observation_prefix_complete,
733 };
734 }
735
736 fn parseFlat(line: []const u8) !ReplayEvent {
737 const version = try jsonU64Flat(line, "\"v\":", null);
738 if (version != format_version) return error.UnsupportedTraceVersion;
739
740 const kind_text = try jsonStringFlat(line, "\"kind\":", null);
741 const kind = Kind.fromTag(kind_text) orelse return error.InvalidEventJson;
742 const allocator_id = std.math.cast(
743 u32,
744 try jsonU64Flat(line, "\"allocator_id\":", 0),
745 ) orelse return error.InvalidEventJson;
746 const allocation_id = try jsonU64Flat(line, "\"allocation_id\":", 0);
747 const address = try jsonU64Flat(line, "\"address\":", 0);
748 const seq = try jsonU64Flat(line, "\"seq\":", 0);
749 const scope_id = std.math.cast(
750 u32,
751 try jsonU64Flat(line, "\"scope_id\":", 0),
752 ) orelse return error.InvalidEventJson;
753 const len = std.math.cast(
754 usize,
755 try jsonU64Flat(line, "\"len\":", 0),
756 ) orelse return error.InvalidEventJson;
757 const old_len = std.math.cast(
758 usize,
759 try jsonU64Flat(line, "\"old_len\":", 0),
760 ) orelse return error.InvalidEventJson;
761 return .{
762 .seq = if (seq == 0) null else seq,
763 .kind = kind,
764 .allocator_id = allocator_id,
765 .allocation_id = allocation_id,
766 .scope_id = scope_id,
767 .address = address,
768 .old_address = try jsonU64Flat(line, "\"old_address\":", address),
769 .len = len,
770 .old_len = old_len,
771 .alignment = std.math.cast(
772 u32,
773 try jsonU64Flat(line, "\"alignment\":", 0),
774 ) orelse return error.InvalidEventJson,
775 .return_address = try jsonU64Flat(line, "\"return_address\":", 0),
776 .stack_id = std.math.cast(
777 u32,
778 try jsonU64Flat(line, "\"stack_id\":", 0),
779 ) orelse return error.InvalidEventJson,
780 .succeeded = try jsonBoolFlat(line, "\"succeeded\":", true),
781 .tracked = try jsonBoolFlat(line, "\"tracked\":", true),
782 .recording_failures = try jsonU64Flat(
783 line,
784 "\"recording_failures\":",
785 0,
786 ),
787 .scope = try jsonStringFlat(line, "\"scope\":", ""),
788 .retains_freed_memory = try jsonBoolFlat(line, "\"retains_freed_memory\":", false),
789 .layer = Layer.fromTag(
790 try jsonStringFlat(
791 line,
792 "\"layer\":",
793 Layer.backing_boundary.tag(),
794 ),
795 ) orelse return error.InvalidEventJson,
796 .operation_id = try jsonU64Flat(line, "\"operation_id\":", 0),
797 .parent_operation_id = try jsonU64Flat(
798 line,
799 "\"parent_operation_id\":",
800 0,
801 ),
802 .producer_id = try jsonU64Flat(line, "\"producer_id\":", 0),
803 .producer = std.meta.stringToEnum(
804 observe.Producer,
805 try jsonStringFlat(line, "\"producer\":", "boundary"),
806 ) orelse return error.InvalidEventJson,
807 .generation = try jsonU64Flat(line, "\"generation\":", 0),
808 .owner_cookie = try jsonU64Flat(line, "\"owner_cookie\":", 0),
809 .lifecycle_disposition = std.meta.stringToEnum(
810 observe.LifecycleDisposition,
811 try jsonStringFlat(
812 line,
813 "\"lifecycle_disposition\":",
814 "none",
815 ),
816 ) orelse return error.InvalidEventJson,
817 .lifecycle_reason = std.meta.stringToEnum(
818 observe.LifecycleReason,
819 try jsonStringFlat(line, "\"lifecycle_reason\":", "none"),
820 ) orelse return error.InvalidEventJson,
821 .lifecycle_instrumented = try jsonBoolFlat(
822 line,
823 "\"lifecycle_instrumented\":",
824 false,
825 ),
826 .observation_prefix_complete = try jsonBoolFlat(
827 line,
828 "\"observation_prefix_complete\":",
829 true,
830 ),
831 };
832 }
833
834 fn jsonU64Flat(line: []const u8, needle: []const u8, default: ?u64) !u64 {
835 const start = std.mem.indexOf(u8, line, needle) orelse return default orelse return error.UnsupportedFastEvent;
836 var index = start + needle.len;
837 if (index >= line.len) return error.InvalidEventJson;
838 const first = line[index];
839 if (first < '0' or first > '9') return error.UnsupportedFastEvent;
840 while (index < line.len and line[index] >= '0' and line[index] <= '9') : (index += 1) {}
841 return std.fmt.parseUnsigned(u64, line[start + needle.len .. index], 10) catch error.InvalidEventJson;
842 }
843
844 fn jsonBoolFlat(line: []const u8, needle: []const u8, default: ?bool) !bool {
845 const start = std.mem.indexOf(u8, line, needle) orelse return default orelse return error.UnsupportedFastEvent;
846 const index = start + needle.len;
847 if (std.mem.startsWith(u8, line[index..], "true")) return true;
848 if (std.mem.startsWith(u8, line[index..], "false")) return false;
849 return error.UnsupportedFastEvent;
850 }
851
852 fn jsonStringFlat(line: []const u8, needle: []const u8, default: ?[]const u8) ![]const u8 {
853 const start = std.mem.indexOf(u8, line, needle) orelse return default orelse return error.UnsupportedFastEvent;
854 var index = start + needle.len;
855 if (index >= line.len or line[index] != '"') return error.UnsupportedFastEvent;
856 index += 1;
857 const text_start = index;
858 while (index < line.len) : (index += 1) {
859 switch (line[index]) {
860 '"' => return line[text_start..index],
861 '\\' => return error.UnsupportedFastEvent,
862 else => {},
863 }
864 }
865 return error.InvalidEventJson;
866 }
867
868 fn jsonString(value: std.json.Value) ![]const u8 {
869 return switch (value) {
870 .string => |text| text,
871 else => error.InvalidEventJson,
872 };
873 }
874
875 fn jsonStringDefault(object: std.json.ObjectMap, key: []const u8, default: []const u8) ![]const u8 {
876 const value = object.get(key) orelse return default;
877 return try jsonString(value);
878 }
879
880 fn jsonU64(value: std.json.Value) !u64 {
881 return switch (value) {
882 .integer => |integer| if (integer < 0) error.InvalidEventJson else @intCast(integer),
883 else => error.InvalidEventJson,
884 };
885 }
886
887 fn jsonU64Default(object: std.json.ObjectMap, key: []const u8, default: u64) !u64 {
888 const value = object.get(key) orelse return default;
889 return try jsonU64(value);
890 }
891
892 fn jsonOptionalU64(object: std.json.ObjectMap, key: []const u8) !?u64 {
893 const value = object.get(key) orelse return null;
894 return try jsonU64(value);
895 }
896
897 fn jsonBool(value: std.json.Value) !bool {
898 return switch (value) {
899 .bool => |actual| actual,
900 else => error.InvalidEventJson,
901 };
902 }
903
904 fn jsonBoolDefault(object: std.json.ObjectMap, key: []const u8, default: bool) !bool {
905 const value = object.get(key) orelse return default;
906 return try jsonBool(value);
907 }
908
909 test "event json line includes identities and omits default fields" {
910 var out = std.Io.Writer.Allocating.init(std.testing.allocator);
911 defer out.deinit();
912
913 try (Event{
914 .seq = 7,
915 .kind = .alloc,
916 .allocation_id = 9,
917 .scope_id = 3,
918 .label_id = 2,
919 .address = 128,
920 .len = 64,
921 .alignment = 8,
922 .live_bytes = 64,
923 }).writeJsonLine(&out.writer, "phase", "root/phase");
924
925 const line = out.written();
926 try std.testing.expect(std.mem.indexOf(u8, line, "\"kind\":\"alloc\"") != null);
927 try std.testing.expect(std.mem.indexOf(u8, line, "\"label\":\"phase\"") != null);
928 try std.testing.expect(std.mem.indexOf(u8, line, "\"scope\":\"root/phase\"") != null);
929 try std.testing.expect(std.mem.indexOf(u8, line, "\"alignment\":8") != null);
930 try std.testing.expect(std.mem.indexOf(u8, line, "\"live_bytes\"") == null);
931 try std.testing.expect(std.mem.indexOf(u8, line, "\"retains_freed_memory\"") == null);
932 }
933
934 test "replay parser consumes sparse event writer output" {
935 var bytes = std.Io.Writer.Allocating.init(std.testing.allocator);
936 defer bytes.deinit();
937 try (Event{
938 .seq = 19,
939 .kind = .remap,
940 .allocator_id = 7,
941 .allocation_id = 11,
942 .address = 8192,
943 .old_address = 4096,
944 .len = 96,
945 .old_len = 64,
946 .alignment = 16,
947 .return_address = 0xabc,
948 .live_bytes = 128,
949 .retains_freed_memory = true,
950 }).writeJsonLine(&bytes.writer, "heap", "root/phase");
951
952 const line = std.mem.trimEnd(u8, bytes.written(), "\n");
953 const parsed = try parseReplayFast(line);
954 try std.testing.expectEqual(@as(?u64, 19), parsed.seq);
955 try std.testing.expectEqual(Kind.remap, parsed.kind);
956 try std.testing.expectEqual(@as(u32, 7), parsed.allocator_id);
957 try std.testing.expectEqual(@as(u64, 11), parsed.allocation_id);
958 try std.testing.expectEqual(@as(u64, 8192), parsed.address);
959 try std.testing.expectEqual(@as(u64, 4096), parsed.old_address);
960 try std.testing.expectEqual(@as(usize, 96), parsed.len);
961 try std.testing.expectEqual(@as(usize, 64), parsed.old_len);
962 try std.testing.expectEqual(@as(u32, 16), parsed.alignment);
963 try std.testing.expectEqual(@as(u64, 0xabc), parsed.return_address);
964 try std.testing.expectEqualStrings("root/phase", parsed.scope);
965 try std.testing.expect(parsed.retains_freed_memory);
966 }
967
968 test "lifecycle identity round trips through sparse event writer" {
969 var bytes = std.Io.Writer.Allocating.init(std.testing.allocator);
970 defer bytes.deinit();
971 try (Event{
972 .seq = 23,
973 .kind = .lifecycle,
974 .allocator_id = 4,
975 .return_address = 0xdef,
976 .layer = .logical_allocator,
977 .operation_id = 31,
978 .producer_id = 9,
979 .producer = .arena,
980 .generation = 5,
981 .owner_cookie = 0x1234,
982 .lifecycle_disposition = .invalidate,
983 .lifecycle_reason = .reset,
984 }).writeJsonLine(&bytes.writer, null, "root");
985
986 const line = std.mem.trimEnd(u8, bytes.written(), "\n");
987 const parsed = try parseReplayFast(line);
988 try std.testing.expectEqual(Kind.lifecycle, parsed.kind);
989 try std.testing.expectEqual(Layer.logical_allocator, parsed.layer);
990 try std.testing.expectEqual(@as(u64, 5), parsed.generation);
991 try std.testing.expectEqual(@as(u64, 0x1234), parsed.owner_cookie);
992 try std.testing.expectEqual(
993 observe.LifecycleDisposition.invalidate,
994 parsed.lifecycle_disposition,
995 );
996 try std.testing.expectEqual(
997 observe.LifecycleReason.reset,
998 parsed.lifecycle_reason,
999 );
1000 }