a73x

6986eef7

Add BaselineRecord + JSON round-trip for frame stats

a73x   2026-04-17 15:28

Lays the foundation for bench-baseline / bench-check tooling (Task 10).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

diff --git a/src/bench_stats.zig b/src/bench_stats.zig
index dd14c67..457ee79 100644
--- a/src/bench_stats.zig
+++ b/src/bench_stats.zig
@@ -200,3 +200,68 @@ test "FrameTimingStats handles empty ring" {
    try std.testing.expectEqual(@as(usize, 0), stats.frame_count);
    try std.testing.expectEqual(@as(u32, 0), stats.snapshot.min);
}

pub const BaselineRecord = struct {
    workload_sha: []const u8,
    zig_version: []const u8,
    waystty_sha: []const u8,
    frame_count: usize,
    sections: struct {
        snapshot: SectionStats,
        row_rebuild: SectionStats,
        atlas_upload: SectionStats,
        instance_upload: SectionStats,
        gpu_submit: SectionStats,
    },
};

/// Serialize `rec` to JSON and return an owned slice. Caller must free.
pub fn writeBaselineJson(alloc: std.mem.Allocator, rec: BaselineRecord) ![]u8 {
    var out: std.Io.Writer.Allocating = .init(alloc);
    errdefer out.deinit();
    try std.json.Stringify.value(rec, .{ .whitespace = .indent_2 }, &out.writer);
    return out.toOwnedSlice();
}

pub fn readBaselineJson(alloc: std.mem.Allocator, bytes: []const u8) !BaselineRecord {
    var parsed = try std.json.parseFromSlice(BaselineRecord, alloc, bytes, .{});
    defer parsed.deinit();
    return .{
        .workload_sha = try alloc.dupe(u8, parsed.value.workload_sha),
        .zig_version = try alloc.dupe(u8, parsed.value.zig_version),
        .waystty_sha = try alloc.dupe(u8, parsed.value.waystty_sha),
        .frame_count = parsed.value.frame_count,
        .sections = parsed.value.sections,
    };
}

test "baseline JSON round-trip" {
    const alloc = std.testing.allocator;
    const rec = BaselineRecord{
        .workload_sha = "abcdef",
        .zig_version = "0.15.0",
        .waystty_sha = "123abc",
        .frame_count = 256,
        .sections = .{
            .snapshot = .{ .min = 1, .avg = 2, .p99 = 3, .max = 4 },
            .row_rebuild = .{ .min = 10, .avg = 20, .p99 = 30, .max = 40 },
            .atlas_upload = .{ .min = 0, .avg = 0, .p99 = 0, .max = 0 },
            .instance_upload = .{ .min = 5, .avg = 6, .p99 = 7, .max = 8 },
            .gpu_submit = .{ .min = 9, .avg = 9, .p99 = 9, .max = 9 },
        },
    };

    const json_bytes = try writeBaselineJson(alloc, rec);
    defer alloc.free(json_bytes);

    const parsed = try readBaselineJson(alloc, json_bytes);
    defer {
        alloc.free(parsed.workload_sha);
        alloc.free(parsed.zig_version);
        alloc.free(parsed.waystty_sha);
    }

    try std.testing.expectEqual(@as(usize, 256), parsed.frame_count);
    try std.testing.expectEqual(@as(u32, 30), parsed.sections.row_rebuild.p99);
    try std.testing.expectEqualStrings("abcdef", parsed.workload_sha);
}