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 }