Wrap signatures and long expressions to the 100-column limit and make every file zig fmt clean. Semantics-preserving throughout: ignoring whitespace and the trailing commas that wrapping introduces, every file here is byte-identical to its predecessor, and the one apparent exception is a warning string split with `++`, which concatenates at comptime to the same bytes. src/index.zig and src/commands.zig are reformatted in the commits that follow, because their reformat is interleaved with in-flight changes to them and separating the two would need the reformat re-derived rather than moved.
222 lines
8.4 KiB
Zig
222 lines
8.4 KiB
Zig
// Dev stress test for the index B+tree (not part of the build):
|
|
// zig run -O ReleaseFast src/stress.zig
|
|
// Bulk-builds 30k entries, inserts 20k more one at a time (splits at depth 2),
|
|
// deletes 16.6k randomly (empty-leaf cascades), drains everything, and checks
|
|
// lookups against a brute-force model throughout.
|
|
const std = @import("std");
|
|
const index = @import("index.zig");
|
|
const bson = @import("bson.zig");
|
|
|
|
fn doc_of(gpa: std.mem.Allocator, pairs: []const bson.Pair) ![]u8 {
|
|
var out: std.ArrayListUnmanaged(u8) = .empty;
|
|
defer out.deinit(gpa);
|
|
try bson.write_doc(pairs, gpa, &out);
|
|
return out.toOwnedSlice(gpa);
|
|
}
|
|
|
|
const Fact = struct { a: i32, b: i32 };
|
|
|
|
fn check_range(
|
|
gpa: std.mem.Allocator,
|
|
ix: *const index.Index,
|
|
prefix: bson.Value,
|
|
lo: ?bson.Value,
|
|
hi: ?bson.Value,
|
|
facts: []const Fact,
|
|
alive: []const bool,
|
|
) !void {
|
|
var out: std.ArrayListUnmanaged([]const u8) = .empty;
|
|
defer out.deinit(gpa);
|
|
try ix.lookup_range(gpa, &.{prefix}, lo, true, hi, false, &out);
|
|
var expected: usize = 0;
|
|
for (facts, 0..) |f, fi| {
|
|
if (!alive[fi]) continue;
|
|
if (f.a != prefix.int32) continue;
|
|
if (lo) |l| if (f.b < l.int32) continue;
|
|
if (hi) |h| if (f.b >= h.int32) continue;
|
|
expected += 1;
|
|
}
|
|
if (out.items.len != expected) {
|
|
std.debug.print("MISMATCH: prefix={d} lo={?d} hi={?d}: got {d}, want {d}\n", .{
|
|
prefix.int32,
|
|
if (lo) |l| l.int32 else null,
|
|
if (hi) |h| h.int32 else null,
|
|
out.items.len,
|
|
expected,
|
|
});
|
|
std.process.exit(1);
|
|
}
|
|
}
|
|
|
|
pub fn main() !void {
|
|
const gpa = std.heap.page_allocator;
|
|
var prng = std.Random.DefaultPrng.init(0xBADCAFE);
|
|
const rand = prng.random();
|
|
|
|
const N = 30_000;
|
|
var ids: std.ArrayListUnmanaged([]u8) = .empty;
|
|
var facts: std.ArrayListUnmanaged(Fact) = .empty;
|
|
var alive: std.ArrayListUnmanaged(bool) = .empty;
|
|
var pairs: [2]bson.Pair = undefined;
|
|
|
|
// 1. Bulk build an index over N docs.
|
|
var keys = [_]index.IndexKey{ .{ .path = "a", .descending = false }, .{ .path = "b", .descending = false } };
|
|
var ix = try index.Index.init(gpa, "ab", &keys, false, false, null);
|
|
for (0..N) |i| {
|
|
const id = try std.fmt.allocPrint(gpa, "id{d}", .{i});
|
|
try ids.append(gpa, id);
|
|
const a = rand.intRangeAtMost(i32, 0, 99);
|
|
const b = rand.intRangeAtMost(i32, 0, 999);
|
|
try facts.append(gpa, .{ .a = a, .b = b });
|
|
try alive.append(gpa, true);
|
|
pairs[0] = .{ .key = "a", .value = .{ .int32 = a } };
|
|
pairs[1] = .{ .key = "b", .value = .{ .int32 = b } };
|
|
const d = try doc_of(gpa, &pairs);
|
|
try ix.append_doc_entries(gpa, d, id);
|
|
}
|
|
_ = try ix.finish_bulk(gpa, false);
|
|
std.debug.print("bulk: count={d} leaves={d} depth={d} nodes={d}\n", .{
|
|
ix.count(),
|
|
ix.leaf_count,
|
|
ix.depth,
|
|
ix.nodes.items.len,
|
|
});
|
|
if (ix.count() != N) return error.BadCount;
|
|
|
|
// Random range checks against brute force.
|
|
for (0..500) |_| {
|
|
const a = rand.intRangeAtMost(i32, 0, 99);
|
|
const lo_v = rand.intRangeAtMost(i32, -10, 1009);
|
|
const hi_v = rand.intRangeAtMost(i32, -10, 1009);
|
|
const use_lo = rand.boolean();
|
|
const use_hi = rand.boolean();
|
|
try check_range(gpa, &ix, .{ .int32 = a }, if (use_lo) .{ .int32 = lo_v } else null, if (use_hi) .{ .int32 = hi_v } else null, facts.items, alive.items);
|
|
}
|
|
|
|
// 2. Incremental inserts, random order (splits + rebalancing-free path).
|
|
const M = 20_000;
|
|
for (0..M) |i| {
|
|
const id = try std.fmt.allocPrint(gpa, "new{d}", .{i});
|
|
try ids.append(gpa, id);
|
|
const a = rand.intRangeAtMost(i32, 0, 99);
|
|
const b = rand.intRangeAtMost(i32, 0, 999);
|
|
try facts.append(gpa, .{ .a = a, .b = b });
|
|
try alive.append(gpa, true);
|
|
pairs[0] = .{ .key = "a", .value = .{ .int32 = a } };
|
|
pairs[1] = .{ .key = "b", .value = .{ .int32 = b } };
|
|
const d = try doc_of(gpa, &pairs);
|
|
_ = try ix.add_doc(gpa, d, id, false);
|
|
}
|
|
std.debug.print("after inserts: count={d} leaves={d} depth={d} nodes={d}\n", .{
|
|
ix.count(),
|
|
ix.leaf_count,
|
|
ix.depth,
|
|
ix.nodes.items.len,
|
|
});
|
|
if (ix.count() != N + M) return error.BadCount;
|
|
for (0..500) |_| {
|
|
const a = rand.intRangeAtMost(i32, 0, 99);
|
|
const lo_v = rand.intRangeAtMost(i32, -10, 1009);
|
|
const hi_v = rand.intRangeAtMost(i32, -10, 1009);
|
|
const use_lo = rand.boolean();
|
|
const use_hi = rand.boolean();
|
|
try check_range(gpa, &ix, .{ .int32 = a }, if (use_lo) .{ .int32 = lo_v } else null, if (use_hi) .{ .int32 = hi_v } else null, facts.items, alive.items);
|
|
}
|
|
|
|
// 3. Delete every 3rd doc in random order (empty-leaf cascades,
|
|
// one-child internals).
|
|
var order: std.ArrayListUnmanaged(usize) = .empty;
|
|
for (0..N + M) |i| if (i % 3 == 0) try order.append(gpa, i);
|
|
rand.shuffle(usize, order.items);
|
|
for (order.items) |i| {
|
|
const a = facts.items[i].a;
|
|
const b = facts.items[i].b;
|
|
pairs[0] = .{ .key = "a", .value = .{ .int32 = a } };
|
|
pairs[1] = .{ .key = "b", .value = .{ .int32 = b } };
|
|
const d = try doc_of(gpa, &pairs);
|
|
ix.remove_doc(gpa, d, ids.items[i]);
|
|
alive.items[i] = false;
|
|
}
|
|
std.debug.print("after deletes: count={d} leaves={d} depth={d} nodes={d}\n", .{
|
|
ix.count(),
|
|
ix.leaf_count,
|
|
ix.depth,
|
|
ix.nodes.items.len,
|
|
});
|
|
if (ix.count() != (N + M) - (N + M) / 3 - 1) return error.BadCount;
|
|
for (0..500) |_| {
|
|
const a = rand.intRangeAtMost(i32, 0, 99);
|
|
const lo_v = rand.intRangeAtMost(i32, -10, 1009);
|
|
const hi_v = rand.intRangeAtMost(i32, -10, 1009);
|
|
const use_lo = rand.boolean();
|
|
const use_hi = rand.boolean();
|
|
try check_range(gpa, &ix, .{ .int32 = a }, if (use_lo) .{ .int32 = lo_v } else null, if (use_hi) .{ .int32 = hi_v } else null, facts.items, alive.items);
|
|
}
|
|
|
|
// 4. Equality lookups still exact.
|
|
for (0..300) |_| {
|
|
const a = rand.intRangeAtMost(i32, 0, 99);
|
|
var out: std.ArrayListUnmanaged([]const u8) = .empty;
|
|
defer out.deinit(gpa);
|
|
try ix.lookup_eq(gpa, &.{.{ .int32 = a }}, &out);
|
|
var expected: usize = 0;
|
|
for (facts.items, 0..) |f, fi| {
|
|
if (alive.items[fi] and f.a == a) expected += 1;
|
|
}
|
|
if (out.items.len != expected) {
|
|
std.debug.print("EQ MISMATCH a={d}: got {d} want {d}\n", .{
|
|
a,
|
|
out.items.len,
|
|
expected,
|
|
});
|
|
return error.BadCount;
|
|
}
|
|
}
|
|
|
|
// 5. Delete everything (empty-leaf cascades, one-child internals),
|
|
// then verify the tree still works for fresh inserts.
|
|
var live: std.ArrayListUnmanaged(usize) = .empty;
|
|
defer live.deinit(gpa);
|
|
for (facts.items, 0..) |_, i| if (alive.items[i]) try live.append(gpa, i);
|
|
rand.shuffle(usize, live.items);
|
|
var remaining = live.items.len;
|
|
for (live.items) |i| {
|
|
const a = facts.items[i].a;
|
|
const b = facts.items[i].b;
|
|
pairs[0] = .{ .key = "a", .value = .{ .int32 = a } };
|
|
pairs[1] = .{ .key = "b", .value = .{ .int32 = b } };
|
|
const d = try doc_of(gpa, &pairs);
|
|
ix.remove_doc(gpa, d, ids.items[i]);
|
|
remaining -= 1;
|
|
if (ix.count() != remaining) {
|
|
std.debug.print("COUNT MISMATCH during drain: {d} != {d}\n", .{
|
|
ix.count(),
|
|
remaining,
|
|
});
|
|
return error.BadCount;
|
|
}
|
|
}
|
|
std.debug.print("after full drain: count={d} leaves={d} depth={d} nodes={d}\n", .{
|
|
ix.count(),
|
|
ix.leaf_count,
|
|
ix.depth,
|
|
ix.nodes.items.len,
|
|
});
|
|
if (ix.count() != 0) return error.BadCount;
|
|
// The drained tree still accepts and finds entries.
|
|
pairs[0] = .{ .key = "a", .value = .{ .int32 = 7 } };
|
|
pairs[1] = .{ .key = "b", .value = .{ .int32 = 42 } };
|
|
const d2 = try doc_of(gpa, &pairs);
|
|
defer gpa.free(d2);
|
|
_ = try ix.add_doc(gpa, d2, "final", false);
|
|
var out: std.ArrayListUnmanaged([]const u8) = .empty;
|
|
defer out.deinit(gpa);
|
|
try ix.lookup_eq(gpa, &.{.{ .int32 = 7 }}, &out);
|
|
if (out.items.len != 1) return error.BadCount;
|
|
var it = ix.iter();
|
|
if (it.next() == null) return error.BadCount;
|
|
if (it.next() != null) return error.BadCount;
|
|
|
|
std.debug.print("STRESS OK\n", .{});
|
|
}
|