From b32e20d8421bc66e78614020f22cfc5ee10c9e2a Mon Sep 17 00:00:00 2001 From: DualFroz Date: Thu, 27 Aug 2026 12:25:00 +0200 Subject: [PATCH] feat(bench): add throughput benchmark harness --- src/bench.zig | 121 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 src/bench.zig diff --git a/src/bench.zig b/src/bench.zig new file mode 100644 index 0000000..5268744 --- /dev/null +++ b/src/bench.zig @@ -0,0 +1,121 @@ +const std = @import("std"); +const cobalt = @import("root.zig"); + +fn throughput(ops: usize, elapsedNs: u64) f64 { + const seconds = @as(f64, @floatFromInt(elapsedNs)) / std.time.ns_per_s; + if (seconds == 0) return 0; + return @as(f64, @floatFromInt(ops)) / seconds; +} + +fn report(writer: anytype, label: []const u8, ops: usize, elapsedNs: u64) !void { + const ms = @as(f64, @floatFromInt(elapsedNs)) / std.time.ns_per_ms; + try writer.print("{s:<14} {d:>9} ops in {d:>9.2} ms => {d:>12.0} ops/s\n", .{ label, ops, ms, throughput(ops, elapsedNs) }); +} + +pub fn main() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + defer _ = gpa.deinit(); + const allocator = gpa.allocator(); + const stdout = std.io.getStdOut().writer(); + + var opCount: usize = 100_000; + { + var args = try std.process.argsWithAllocator(allocator); + defer args.deinit(); + _ = args.next(); + if (args.next()) |arg| opCount = std.fmt.parseInt(usize, arg, 10) catch opCount; + } + + const dataDir = "cobalt-bench-data"; + std.fs.cwd().deleteTree(dataDir) catch {}; + defer std.fs.cwd().deleteTree(dataDir) catch {}; + + var db = try cobalt.Db.open(allocator, dataDir, .{ .flushThreshold = 1024 * 1024 }); + defer db.close(); + + try stdout.print("== cobalt benchmark ==\n", .{}); + try stdout.print("operations: {d}, deterministic keys\n\n", .{opCount}); + + var keyBuf: [32]u8 = undefined; + var valBuf: [96]u8 = undefined; + var timer = try std.time.Timer.start(); + + var i: usize = 0; + while (i < opCount) : (i += 1) { + const key = try std.fmt.bufPrint(&keyBuf, "key:{d:0>12}", .{i}); + const value = try std.fmt.bufPrint(&valBuf, "value-{d:0>12}-cobalt-lsm-payload", .{i}); + try db.put(key, value); + } + try report(stdout, "put", opCount, timer.read()); + + const batchSize = 1000; + timer.reset(); + var batched: usize = 0; + while (batched < opCount) : (batched += batchSize) { + var batch = cobalt.WriteBatch.init(allocator); + defer batch.deinit(); + var j: usize = 0; + while (j < batchSize and batched + j < opCount) : (j += 1) { + const idx = opCount + batched + j; + const key = try std.fmt.bufPrint(&keyBuf, "bat:{d:0>12}", .{idx}); + const value = try std.fmt.bufPrint(&valBuf, "value-{d:0>12}-cobalt-lsm-payload", .{idx}); + try batch.put(key, value); + } + try db.apply(&batch); + } + try report(stdout, "batch-put", opCount, timer.read()); + + timer.reset(); + try db.maybeCompact(); + try report(stdout, "compact", db.sstables.items.len, timer.read()); + + timer.reset(); + var found: usize = 0; + i = 0; + while (i < opCount) : (i += 1) { + const key = try std.fmt.bufPrint(&keyBuf, "key:{d:0>12}", .{i}); + if (try db.get(allocator, key)) |value| { + found += 1; + allocator.free(value); + } + } + try report(stdout, "get", opCount, timer.read()); + + timer.reset(); + var scanned: usize = 0; + { + var iter = try db.scan(allocator, null, null); + defer iter.deinit(); + while (try iter.next()) |_| scanned += 1; + } + try report(stdout, "scan-forward", scanned, timer.read()); + + timer.reset(); + var reverseScanned: usize = 0; + { + var iter = try db.scanReverse(allocator, .{}); + defer iter.deinit(); + while (try iter.next()) |_| reverseScanned += 1; + } + try report(stdout, "scan-reverse", reverseScanned, timer.read()); + + timer.reset(); + var prefixHits: usize = 0; + { + var iter = try db.scanPrefix(allocator, "key:00000000000"); + defer iter.deinit(); + while (try iter.next()) |_| prefixHits += 1; + } + try report(stdout, "scan-prefix", prefixHits, timer.read()); + + try stdout.print("\nget hits: {d}/{d}, forward/reverse scan entries: {d}/{d}\n", .{ found, opCount, scanned, reverseScanned }); + + const stats = db.stats(); + try stdout.writeAll("\n== database statistics ==\n"); + try stats.print(stdout); + try stdout.print("compression: {d} live entries in {d} physical bytes ({d:.1} bytes/entry)\n", .{ + stats.liveEntries, + stats.physicalBytes, + if (stats.liveEntries == 0) 0 else @as(f64, @floatFromInt(stats.physicalBytes)) / @as(f64, @floatFromInt(stats.liveEntries)), + }); +}