atproto utils for zig zat.dev
atproto sdk zig
26
fork

Configure Feed

Select the types of activity you want to include in your feed.

add DAG-CBOR codec benchmarks

15 benchmarks covering encode/decode for full records, primitives (text,
uint, CID link, varint), CID computation, and composite operations.
Uses FixedBufferAllocator to measure codec work rather than mmap
syscalls. Run with `just bench` (builds with ReleaseFast).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

jcalabro a7fc9728 8a4411c7

+299
+15
build.zig
··· 77 77 const smoke_step = b.step("smoke", "run jetstream smoke test"); 78 78 smoke_step.dependOn(&run_smoke.step); 79 79 80 + // CBOR codec benchmarks 81 + const cbor_bench = b.addExecutable(.{ 82 + .name = "cbor-bench", 83 + .root_module = b.createModule(.{ 84 + .root_source_file = b.path("src/internal/repo/cbor_bench.zig"), 85 + .target = target, 86 + .optimize = optimize, 87 + }), 88 + }); 89 + b.installArtifact(cbor_bench); 90 + 91 + const run_bench = b.addRunArtifact(cbor_bench); 92 + const bench_step = b.step("bench", "run CBOR codec benchmarks"); 93 + bench_step.dependOn(&run_bench.step); 94 + 80 95 // publish-docs script (uses zat to publish docs to ATProto) 81 96 const publish_docs = b.addExecutable(.{ 82 97 .name = "publish-docs",
+4
justfile
··· 15 15 # run tests 16 16 test: 17 17 zig build test --summary all -freference-trace 18 + 19 + # run CBOR codec benchmarks 20 + bench: 21 + zig build bench -Doptimize=ReleaseFast
+280
src/internal/repo/cbor_bench.zig
··· 1 + //! DAG-CBOR codec benchmarks 2 + //! 3 + //! measures low-level encoding/decoding primitives and full record 4 + //! round-trips to track performance regressions and compare with 5 + //! the atmos (Go) implementation. 6 + //! 7 + //! run: zig build bench -Doptimize=ReleaseFast 8 + //! or: just bench 9 + 10 + const std = @import("std"); 11 + const cbor = @import("cbor.zig"); 12 + const Value = cbor.Value; 13 + const Cid = cbor.Cid; 14 + 15 + // --------------------------------------------------------------------------- 16 + // benchmark harness 17 + // --------------------------------------------------------------------------- 18 + 19 + const warmup_iters = 1_000; 20 + const min_iters = 10_000; 21 + const target_ns: u64 = 500_000_000; // run each bench for ~500ms 22 + 23 + fn clockNs() u64 { 24 + var ts: std.os.linux.timespec = undefined; 25 + _ = std.os.linux.clock_gettime(.MONOTONIC, &ts); 26 + return @intCast(ts.sec * std.time.ns_per_s + ts.nsec); 27 + } 28 + 29 + fn bench(name: []const u8, comptime func: anytype) void { 30 + // warmup 31 + for (0..warmup_iters) |_| { 32 + func(); 33 + } 34 + 35 + // calibrate: run min_iters, then scale up to fill target_ns 36 + var start = clockNs(); 37 + for (0..min_iters) |_| { 38 + func(); 39 + } 40 + const calibrate_ns = clockNs() - start; 41 + const iters: u64 = if (calibrate_ns == 0) 42 + min_iters * 100 43 + else 44 + @max(min_iters, target_ns * min_iters / calibrate_ns); 45 + 46 + // measured run 47 + start = clockNs(); 48 + for (0..iters) |_| { 49 + func(); 50 + } 51 + const elapsed_ns = clockNs() - start; 52 + const ns_per_op = elapsed_ns / iters; 53 + 54 + std.debug.print(" {s:<40} {d:>8} ns/op ({d} iters)\n", .{ name, ns_per_op, iters }); 55 + } 56 + 57 + // --------------------------------------------------------------------------- 58 + // test data: a realistic AT Protocol record (same structure as atmos bench) 59 + // --------------------------------------------------------------------------- 60 + 61 + const bench_record: Value = .{ .map = &.{ 62 + .{ .key = "$type", .value = .{ .text = "app.bsky.feed.post" } }, 63 + .{ .key = "createdAt", .value = .{ .text = "2024-01-15T12:00:00.000Z" } }, 64 + .{ .key = "langs", .value = .{ .array = &.{.{ .text = "en" }} } }, 65 + .{ .key = "reply", .value = .{ .map = &.{ 66 + .{ .key = "parent", .value = .{ .map = &.{ 67 + .{ .key = "cid", .value = .{ .text = "bafyreib3pwrff2yadznophzf4hcvtyoctwzcujvz7x4pngk2isicz7yszq" } }, 68 + .{ .key = "uri", .value = .{ .text = "at://did:plc:4nendwqrs754gt6qvgr56jmn/app.bsky.feed.post/3medg2qvcuc2c" } }, 69 + } } }, 70 + .{ .key = "root", .value = .{ .map = &.{ 71 + .{ .key = "cid", .value = .{ .text = "bafyreib3pwrff2yadznophzf4hcvtyoctwzcujvz7x4pngk2isicz7yszq" } }, 72 + .{ .key = "uri", .value = .{ .text = "at://did:plc:4nendwqrs754gt6qvgr56jmn/app.bsky.feed.post/3medg2qvcuc2c" } }, 73 + } } }, 74 + } } }, 75 + .{ .key = "text", .value = .{ .text = "Hello, world! This is a test post with some content." } }, 76 + } }; 77 + 78 + const bench_text = "Hello, world! This is a test post with some content."; 79 + 80 + // pre-encoded data (initialized in main) 81 + var encoded_record: []const u8 = undefined; 82 + var encoded_text: []const u8 = undefined; 83 + var encoded_uint: []const u8 = undefined; 84 + var encoded_cid_link: []const u8 = undefined; 85 + var bench_cid: Cid = undefined; 86 + var bench_arena: std.heap.ArenaAllocator = undefined; 87 + 88 + fn initBenchData() void { 89 + bench_arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); 90 + const alloc = bench_arena.allocator(); 91 + 92 + encoded_record = cbor.encodeAlloc(alloc, bench_record) catch @panic("encode record"); 93 + encoded_text = cbor.encodeAlloc(alloc, .{ .text = bench_text }) catch @panic("encode text"); 94 + encoded_uint = cbor.encodeAlloc(alloc, .{ .unsigned = 1_234_567_890 }) catch @panic("encode uint"); 95 + bench_cid = Cid.forDagCbor(alloc, encoded_record) catch @panic("compute cid"); 96 + encoded_cid_link = cbor.encodeAlloc(alloc, .{ .cid = bench_cid }) catch @panic("encode cid"); 97 + } 98 + 99 + // --------------------------------------------------------------------------- 100 + // shared allocator for benchmarks 101 + // 102 + // uses a FixedBufferAllocator over a stack buffer so we measure codec 103 + // work, not mmap/munmap syscalls. the encoder needs temp space for map 104 + // key sorting; the decoder needs space for Value arrays/map entries. 105 + // a 16 KB buffer is more than enough for the bench record. 106 + // --------------------------------------------------------------------------- 107 + 108 + // --------------------------------------------------------------------------- 109 + // individual benchmarks 110 + // --------------------------------------------------------------------------- 111 + 112 + // --- full record encode/decode --- 113 + 114 + fn benchMarshal() void { 115 + var scratch: [4096]u8 = undefined; 116 + var fba = std.heap.FixedBufferAllocator.init(&scratch); 117 + var out_buf: [1024]u8 = undefined; 118 + var w: std.Io.Writer = .fixed(&out_buf); 119 + cbor.encode(fba.allocator(), &w, bench_record) catch @panic("encode"); 120 + std.mem.doNotOptimizeAway(w.end); 121 + } 122 + 123 + fn benchUnmarshal() void { 124 + var scratch: [8192]u8 = undefined; 125 + var fba = std.heap.FixedBufferAllocator.init(&scratch); 126 + const val = cbor.decodeAll(fba.allocator(), encoded_record) catch @panic("decode"); 127 + std.mem.doNotOptimizeAway(val); 128 + } 129 + 130 + fn benchMarshalRoundTrip() void { 131 + var scratch: [16384]u8 = undefined; 132 + var fba = std.heap.FixedBufferAllocator.init(&scratch); 133 + const alloc = fba.allocator(); 134 + const enc = cbor.encodeAlloc(alloc, bench_record) catch @panic("encode"); 135 + const dec = cbor.decodeAll(alloc, enc) catch @panic("decode"); 136 + std.mem.doNotOptimizeAway(dec); 137 + } 138 + 139 + fn benchDecodeReencode() void { 140 + var scratch: [16384]u8 = undefined; 141 + var fba = std.heap.FixedBufferAllocator.init(&scratch); 142 + const alloc = fba.allocator(); 143 + const dec = cbor.decodeAll(alloc, encoded_record) catch @panic("decode"); 144 + const enc = cbor.encodeAlloc(alloc, dec) catch @panic("encode"); 145 + std.mem.doNotOptimizeAway(enc); 146 + } 147 + 148 + // --- CID computation --- 149 + 150 + fn benchComputeCID() void { 151 + var scratch: [256]u8 = undefined; 152 + var fba = std.heap.FixedBufferAllocator.init(&scratch); 153 + const cid = Cid.forDagCbor(fba.allocator(), encoded_record) catch @panic("cid"); 154 + std.mem.doNotOptimizeAway(cid); 155 + } 156 + 157 + fn benchEncodeAndCID() void { 158 + var scratch: [8192]u8 = undefined; 159 + var fba = std.heap.FixedBufferAllocator.init(&scratch); 160 + const alloc = fba.allocator(); 161 + const enc = cbor.encodeAlloc(alloc, bench_record) catch @panic("encode"); 162 + const cid = Cid.forDagCbor(alloc, enc) catch @panic("cid"); 163 + std.mem.doNotOptimizeAway(cid); 164 + } 165 + 166 + // --- text string encode/decode --- 167 + 168 + fn benchEncodeText() void { 169 + var buf: [128]u8 = undefined; 170 + var w: std.Io.Writer = .fixed(&buf); 171 + cbor.encode(std.heap.page_allocator, &w, .{ .text = bench_text }) catch @panic("encode"); 172 + std.mem.doNotOptimizeAway(w.end); 173 + } 174 + 175 + fn benchDecodeText() void { 176 + // text decoding doesn't allocate — pass a failing allocator to prove it 177 + const val = cbor.decodeAll(std.heap.page_allocator, encoded_text) catch @panic("decode"); 178 + std.mem.doNotOptimizeAway(val); 179 + } 180 + 181 + // --- unsigned integer encode/decode --- 182 + 183 + fn benchEncodeUint() void { 184 + var buf: [16]u8 = undefined; 185 + var w: std.Io.Writer = .fixed(&buf); 186 + cbor.encode(std.heap.page_allocator, &w, .{ .unsigned = 1_234_567_890 }) catch @panic("encode"); 187 + std.mem.doNotOptimizeAway(w.end); 188 + } 189 + 190 + fn benchDecodeUint() void { 191 + // uint decoding doesn't allocate 192 + const val = cbor.decodeAll(std.heap.page_allocator, encoded_uint) catch @panic("decode"); 193 + std.mem.doNotOptimizeAway(val); 194 + } 195 + 196 + // --- CID link encode/decode --- 197 + 198 + fn benchEncodeCidLink() void { 199 + var buf: [128]u8 = undefined; 200 + var w: std.Io.Writer = .fixed(&buf); 201 + cbor.encode(std.heap.page_allocator, &w, .{ .cid = bench_cid }) catch @panic("encode"); 202 + std.mem.doNotOptimizeAway(w.end); 203 + } 204 + 205 + fn benchDecodeCidLink() void { 206 + // CID link decoding doesn't allocate (borrows from input bytes) 207 + const val = cbor.decodeAll(std.heap.page_allocator, encoded_cid_link) catch @panic("decode"); 208 + std.mem.doNotOptimizeAway(val); 209 + } 210 + 211 + // --- map key lookup --- 212 + 213 + fn benchMapKeyLookup() void { 214 + var scratch: [8192]u8 = undefined; 215 + var fba = std.heap.FixedBufferAllocator.init(&scratch); 216 + const val = cbor.decodeAll(fba.allocator(), encoded_record) catch @panic("decode"); 217 + std.mem.doNotOptimizeAway(val.getString("text")); 218 + std.mem.doNotOptimizeAway(val.getString("$type")); 219 + std.mem.doNotOptimizeAway(val.getString("createdAt")); 220 + } 221 + 222 + // --- varint encode/decode --- 223 + 224 + fn benchWriteUvarint() void { 225 + var buf: [16]u8 = undefined; 226 + var w: std.Io.Writer = .fixed(&buf); 227 + cbor.writeUvarint(&w, 1_234_567_890) catch @panic("write"); 228 + std.mem.doNotOptimizeAway(w.end); 229 + } 230 + 231 + fn benchReadUvarint() void { 232 + // pre-encoded varint for 1_234_567_890 233 + const data = [_]u8{ 0xd2, 0x85, 0xd8, 0xcc, 0x04 }; 234 + var pos: usize = 0; 235 + const val = cbor.readUvarint(&data, &pos); 236 + std.mem.doNotOptimizeAway(val); 237 + } 238 + 239 + // --------------------------------------------------------------------------- 240 + // main 241 + // --------------------------------------------------------------------------- 242 + 243 + pub fn main() void { 244 + initBenchData(); 245 + defer bench_arena.deinit(); 246 + 247 + std.debug.print("\nDAG-CBOR benchmarks (record: {d} bytes encoded)\n", .{encoded_record.len}); 248 + std.debug.print("{s}\n\n", .{"=" ** 68}); 249 + 250 + std.debug.print("record encode/decode:\n", .{}); 251 + bench("encode record", benchMarshal); 252 + bench("decode record", benchUnmarshal); 253 + bench("encode + decode round-trip", benchMarshalRoundTrip); 254 + bench("decode + re-encode", benchDecodeReencode); 255 + 256 + std.debug.print("\nCID operations:\n", .{}); 257 + bench("compute CID (SHA-256)", benchComputeCID); 258 + bench("encode + compute CID", benchEncodeAndCID); 259 + 260 + std.debug.print("\ntext string:\n", .{}); 261 + bench("encode text (54 bytes)", benchEncodeText); 262 + bench("decode text (54 bytes)", benchDecodeText); 263 + 264 + std.debug.print("\nunsigned integer:\n", .{}); 265 + bench("encode uint (1234567890)", benchEncodeUint); 266 + bench("decode uint (1234567890)", benchDecodeUint); 267 + 268 + std.debug.print("\nCID link:\n", .{}); 269 + bench("encode CID link", benchEncodeCidLink); 270 + bench("decode CID link", benchDecodeCidLink); 271 + 272 + std.debug.print("\nvarint:\n", .{}); 273 + bench("write uvarint (1234567890)", benchWriteUvarint); 274 + bench("read uvarint (1234567890)", benchReadUvarint); 275 + 276 + std.debug.print("\ncomposite:\n", .{}); 277 + bench("decode + key lookup (3 keys)", benchMapKeyLookup); 278 + 279 + std.debug.print("\n", .{}); 280 + }