storage: byte documents in a per-collection slab (roadmap item 4)
Documents live as canonical BSON bytes in a segmented per-collection slab (fixed 8 MiB segments keep capacity slack under one segment); the docs map holds flat offsets that stay valid across segment growth, and removed documents leave garbage bytes until compaction rewrites. The per-document ArenaAllocator and its second full Pair-tree copy are gone. The matcher walks the stored bytes directly, skipping by length any field the filter does not name (a new bson byte-walker: element_key, skip_value, read_value with borrowed leaves, get_at, and a borrowed spine parse). The byte matcher is differential-tested against the tree matcher on a corpus and shares its operator logic. Stored documents are never materialized on the scan path or in aggregate $match; $group reads group keys and sums straight off the bytes. Sort, projection, findAndModify, updates and index entry generation use a borrowed spine into the slab (or the byte collector, which also replaced collect_values in build_entries). The compaction threshold now counts uncompressed data volume, since a compressed log would otherwise never trigger. Measured (tests/e2e/results/phase5.txt): server RSS 1979 -> 539 MB (2.4x smaller than MongoDB; phase1 baseline 2.0 GB), range-scan 22.5 -> ~12 ms (parity, best run faster than MongoDB), proj 4.1 -> 3.4 ms, createIndex parity. Verified: unit suite in all three modes with zero leaks, the crash pair, e2e6, and the stress/spill programs.
This commit is contained in:
247
src/bson.zig
247
src/bson.zig
@@ -102,7 +102,7 @@ pub const Document = struct {
|
||||
var arena = std.heap.ArenaAllocator.init(allocator);
|
||||
errdefer arena.deinit();
|
||||
var idx: usize = 0;
|
||||
const pairs = try parse_doc_into(&arena, bytes, &idx);
|
||||
const pairs = try parse_doc_into(arena.allocator(), bytes, &idx, false);
|
||||
return .{ .arena = arena, .pairs = pairs };
|
||||
}
|
||||
|
||||
@@ -146,14 +146,19 @@ pub fn get_pair_index(pairs: []const Pair, key: []const u8) ?usize {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const Parser = struct {
|
||||
arena: *std.heap.ArenaAllocator,
|
||||
allocator: std.mem.Allocator,
|
||||
bytes: []const u8,
|
||||
/// When set, strings and keys point into `bytes` instead of being
|
||||
/// copied, so the parsed spine is only valid while `bytes` is. Used for
|
||||
/// the borrowed spine of slab-resident documents; the full `parse`
|
||||
/// keeps the owned, self-contained behavior.
|
||||
borrow: bool,
|
||||
};
|
||||
|
||||
const ParseError = error{ InvalidBson, OutOfMemory };
|
||||
|
||||
fn parse_doc_into(arena: *std.heap.ArenaAllocator, bytes: []const u8, idx: *usize) ParseError![]const Pair {
|
||||
const p = Parser{ .arena = arena, .bytes = bytes };
|
||||
fn parse_doc_into(allocator: std.mem.Allocator, bytes: []const u8, idx: *usize, borrow: bool) ParseError![]const Pair {
|
||||
const p = Parser{ .allocator = allocator, .bytes = bytes, .borrow = borrow };
|
||||
return parse_doc_inner(p, idx);
|
||||
}
|
||||
|
||||
@@ -177,7 +182,7 @@ fn parse_doc_inner(p: Parser, idx: *usize) ParseError![]const Pair {
|
||||
const start = idx.*;
|
||||
const end = try doc_extent(p, start);
|
||||
|
||||
const gpa = p.arena.allocator();
|
||||
const gpa = p.allocator;
|
||||
var pairs: std.ArrayListUnmanaged(Pair) = .empty;
|
||||
errdefer pairs.deinit(gpa);
|
||||
|
||||
@@ -205,9 +210,12 @@ fn parse_cstring(p: Parser, idx: *usize) ParseError![]const u8 {
|
||||
while (idx.* < p.bytes.len and p.bytes[idx.*] != 0) idx.* += 1;
|
||||
if (idx.* >= p.bytes.len) return error.InvalidBson;
|
||||
idx.* += 1;
|
||||
// Strings are copied into the arena so documents are self-contained and
|
||||
// outlive the input buffer (wire messages and log records are transient).
|
||||
return p.arena.allocator().dupe(u8, p.bytes[start .. idx.* - 1]);
|
||||
// Keys and strings are copied into the arena so owned documents are
|
||||
// self-contained and outlive the input buffer (wire messages and log
|
||||
// records are transient); a borrowed spine leaves them pointing at the
|
||||
// source bytes instead.
|
||||
if (p.borrow) return p.bytes[start .. idx.* - 1];
|
||||
return p.allocator.dupe(u8, p.bytes[start .. idx.* - 1]);
|
||||
}
|
||||
|
||||
fn parse_string(p: Parser, idx: *usize) ParseError![]const u8 {
|
||||
@@ -217,7 +225,8 @@ fn parse_string(p: Parser, idx: *usize) ParseError![]const u8 {
|
||||
const str = p.bytes[idx.* + 4 .. idx.* + 4 + len];
|
||||
if (str[len - 1] != 0) return error.InvalidBson;
|
||||
idx.* += 4 + len;
|
||||
return p.arena.allocator().dupe(u8, str[0 .. len - 1]);
|
||||
if (p.borrow) return str[0 .. len - 1];
|
||||
return p.allocator.dupe(u8, str[0 .. len - 1]);
|
||||
}
|
||||
|
||||
fn parse_value(p: Parser, tag: u8, idx: *usize) ParseError!Value {
|
||||
@@ -238,7 +247,7 @@ fn parse_value(p: Parser, tag: u8, idx: *usize) ParseError!Value {
|
||||
try ensure_available(p.bytes, idx.* + 5, len);
|
||||
const data = p.bytes[idx.* + 5 .. idx.* + 5 + len];
|
||||
idx.* += 5 + len;
|
||||
break :blk .{ .binary = .{ .subtype = subtype, .data = try p.arena.allocator().dupe(u8, data) } };
|
||||
break :blk .{ .binary = .{ .subtype = subtype, .data = try p.allocator.dupe(u8, data) } };
|
||||
},
|
||||
0x06 => .{ .opaque_val = .{ .kind = 0x06, .data = &.{} } },
|
||||
0x07 => blk: {
|
||||
@@ -272,7 +281,7 @@ fn parse_value(p: Parser, tag: u8, idx: *usize) ParseError!Value {
|
||||
idx.* += 12;
|
||||
// Like all other types, the payload is copied into the arena so
|
||||
// documents stay valid after the input buffer is reused.
|
||||
break :blk .{ .opaque_val = .{ .kind = 0x0C, .data = try p.arena.allocator().dupe(u8, p.bytes[start..idx.*]) } };
|
||||
break :blk .{ .opaque_val = .{ .kind = 0x0C, .data = try p.allocator.dupe(u8, p.bytes[start..idx.*]) } };
|
||||
},
|
||||
0x0D => .{ .code = try parse_string(p, idx) },
|
||||
0x0E => .{ .symbol = try parse_string(p, idx) },
|
||||
@@ -282,7 +291,7 @@ fn parse_value(p: Parser, tag: u8, idx: *usize) ParseError!Value {
|
||||
if (total < 4 or total > p.bytes.len - idx.*) return error.InvalidBson;
|
||||
const data = p.bytes[idx.* .. idx.* + total];
|
||||
idx.* += total;
|
||||
break :blk .{ .opaque_val = .{ .kind = 0x0F, .data = try p.arena.allocator().dupe(u8, data) } };
|
||||
break :blk .{ .opaque_val = .{ .kind = 0x0F, .data = try p.allocator.dupe(u8, data) } };
|
||||
},
|
||||
0x10 => blk: {
|
||||
try ensure_available(p.bytes, idx.*, 4);
|
||||
@@ -316,7 +325,7 @@ fn parse_array(p: Parser, idx: *usize) ParseError![]const Value {
|
||||
const start = idx.*;
|
||||
const end = try doc_extent(p, start);
|
||||
|
||||
const gpa = p.arena.allocator();
|
||||
const gpa = p.allocator;
|
||||
var values: std.ArrayListUnmanaged(Value) = .empty;
|
||||
errdefer values.deinit(gpa);
|
||||
|
||||
@@ -333,6 +342,218 @@ fn parse_array(p: Parser, idx: *usize) ParseError![]const Value {
|
||||
return values.toOwnedSlice(gpa);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Borrowed spine and byte walking
|
||||
// ---------------------------------------------------------------------------
|
||||
//
|
||||
// Slab-resident documents are stored as canonical BSON bytes; the tree
|
||||
// machinery needs a `[]const Pair` spine over them. `spine` parses one with
|
||||
// keys and leaf values pointing at the source bytes (valid while the bytes
|
||||
// live — the collection slab outlives any query). The byte-walking
|
||||
// primitives below let a matcher skip over fields it does not name instead
|
||||
// of materializing the whole document.
|
||||
|
||||
/// A borrowed parse of `bytes` into an arena: keys, strings, binary and
|
||||
/// regex payloads point into `bytes`; only the pair/value skeleton is
|
||||
/// allocated. The result is valid while both `bytes` and `arena` live.
|
||||
pub fn spine(allocator: std.mem.Allocator, bytes: []const u8) error{ InvalidBson, OutOfMemory }![]const Pair {
|
||||
var idx: usize = 0;
|
||||
return parse_doc_into(allocator, bytes, &idx, true);
|
||||
}
|
||||
|
||||
/// The first element's key, or null when the remaining bytes are not an
|
||||
/// element. Advances `idx` past the key.
|
||||
pub fn element_key(bytes: []const u8, idx: *usize) ?[]const u8 {
|
||||
if (idx.* >= bytes.len) return null;
|
||||
const start = idx.*;
|
||||
while (idx.* < bytes.len and bytes[idx.*] != 0) idx.* += 1;
|
||||
if (idx.* >= bytes.len) return null;
|
||||
idx.* += 1;
|
||||
return bytes[start .. idx.* - 1];
|
||||
}
|
||||
|
||||
/// Advance past one value of `tag` (the key is already consumed). The input
|
||||
/// is canonical (validated when it was stored), so skips are cheap; the
|
||||
/// length-prefixed types still bounds-check their prefix.
|
||||
pub fn skip_value(bytes: []const u8, idx: *usize, tag: u8) error{InvalidBson}!void {
|
||||
switch (tag) {
|
||||
0x01, 0x09, 0x11, 0x12 => idx.* += 8,
|
||||
0x10 => idx.* += 4,
|
||||
0x13 => idx.* += 16,
|
||||
0x07 => idx.* += 12,
|
||||
0x08 => idx.* += 1,
|
||||
0x0A, 0x06 => {},
|
||||
0x02, 0x0D, 0x0E => {
|
||||
try ensure_available(bytes, idx.*, 4);
|
||||
idx.* += 4 + std.mem.readInt(u32, bytes[idx.*..][0..4], .little);
|
||||
},
|
||||
0x03, 0x04 => {
|
||||
try ensure_available(bytes, idx.*, 4);
|
||||
idx.* += std.mem.readInt(u32, bytes[idx.*..][0..4], .little);
|
||||
},
|
||||
0x05 => {
|
||||
try ensure_available(bytes, idx.*, 5);
|
||||
idx.* += 5 + std.mem.readInt(u32, bytes[idx.*..][0..4], .little);
|
||||
},
|
||||
0x0B => {
|
||||
_ = element_key(bytes, idx) orelse return error.InvalidBson;
|
||||
_ = element_key(bytes, idx) orelse return error.InvalidBson;
|
||||
},
|
||||
0x0C => {
|
||||
try ensure_available(bytes, idx.*, 4);
|
||||
idx.* += 4 + std.mem.readInt(u32, bytes[idx.*..][0..4], .little) + 12;
|
||||
},
|
||||
0x0F => {
|
||||
try ensure_available(bytes, idx.*, 4);
|
||||
idx.* += std.mem.readInt(u32, bytes[idx.*..][0..4], .little);
|
||||
},
|
||||
else => return error.InvalidBson,
|
||||
}
|
||||
}
|
||||
|
||||
/// Read one value of `tag` into a Value whose leaves borrow `bytes`; nested
|
||||
/// documents and arrays materialize their spines into `arena`. Advances
|
||||
/// `idx` past the value.
|
||||
pub fn read_value(allocator: std.mem.Allocator, bytes: []const u8, idx: *usize, tag: u8) error{ InvalidBson, OutOfMemory }!Value {
|
||||
switch (tag) {
|
||||
0x01 => {
|
||||
try ensure_available(bytes, idx.*, 8);
|
||||
const v: f64 = @bitCast(std.mem.readInt(u64, bytes[idx.*..][0..8], .little));
|
||||
idx.* += 8;
|
||||
return .{ .double = v };
|
||||
},
|
||||
0x02 => {
|
||||
try ensure_available(bytes, idx.*, 4);
|
||||
const len: u32 = std.mem.readInt(u32, bytes[idx.*..][0..4], .little);
|
||||
const str = bytes[idx.* + 4 .. idx.* + 4 + len];
|
||||
idx.* += 4 + len;
|
||||
return .{ .string = str[0 .. len - 1] };
|
||||
},
|
||||
0x03 => {
|
||||
const start = idx.*;
|
||||
const total: u32 = std.mem.readInt(u32, bytes[start..][0..4], .little);
|
||||
var sub: usize = 0;
|
||||
const pairs = try parse_doc_into(allocator, bytes[start..], &sub, true);
|
||||
idx.* = start + total;
|
||||
return .{ .doc = pairs };
|
||||
},
|
||||
0x04 => {
|
||||
const start = idx.*;
|
||||
const total: u32 = std.mem.readInt(u32, bytes[start..][0..4], .little);
|
||||
var sub: usize = start + 4;
|
||||
var values: std.ArrayListUnmanaged(Value) = .empty;
|
||||
errdefer values.deinit(allocator);
|
||||
while (sub < start + total - 1 and bytes[sub] != 0) {
|
||||
const atag = bytes[sub];
|
||||
sub += 1;
|
||||
_ = element_key(bytes, &sub) orelse return error.InvalidBson;
|
||||
try values.append(allocator, try read_value(allocator, bytes, &sub, atag));
|
||||
}
|
||||
idx.* = start + total;
|
||||
return .{ .array = try values.toOwnedSlice(allocator) };
|
||||
},
|
||||
0x05 => {
|
||||
try ensure_available(bytes, idx.*, 5);
|
||||
const len: u32 = std.mem.readInt(u32, bytes[idx.*..][0..4], .little);
|
||||
const subtype = bytes[idx.* + 4];
|
||||
const data = bytes[idx.* + 5 .. idx.* + 5 + len];
|
||||
idx.* += 5 + len;
|
||||
return .{ .binary = .{ .subtype = subtype, .data = data } };
|
||||
},
|
||||
0x07 => {
|
||||
try ensure_available(bytes, idx.*, 12);
|
||||
const oid: ObjectId = bytes[idx.*..][0..12].*;
|
||||
idx.* += 12;
|
||||
return .{ .object_id = oid };
|
||||
},
|
||||
0x08 => {
|
||||
try ensure_available(bytes, idx.*, 1);
|
||||
const v = bytes[idx.*] != 0;
|
||||
idx.* += 1;
|
||||
return .{ .bool = v };
|
||||
},
|
||||
0x09 => {
|
||||
try ensure_available(bytes, idx.*, 8);
|
||||
const v: i64 = std.mem.readInt(i64, bytes[idx.*..][0..8], .little);
|
||||
idx.* += 8;
|
||||
return .{ .datetime = v };
|
||||
},
|
||||
0x0A => return .null,
|
||||
0x0B => {
|
||||
const pattern = element_key(bytes, idx) orelse return error.InvalidBson;
|
||||
const options = element_key(bytes, idx) orelse return error.InvalidBson;
|
||||
return .{ .regex = .{ .pattern = pattern, .options = options } };
|
||||
},
|
||||
0x0C => {
|
||||
try ensure_available(bytes, idx.*, 4);
|
||||
const len: u32 = std.mem.readInt(u32, bytes[idx.*..][0..4], .little);
|
||||
const start = idx.*;
|
||||
idx.* += 4 + len + 12;
|
||||
return .{ .opaque_val = .{ .kind = 0x0C, .data = bytes[start..idx.*] } };
|
||||
},
|
||||
0x0D => {
|
||||
try ensure_available(bytes, idx.*, 4);
|
||||
const len: u32 = std.mem.readInt(u32, bytes[idx.*..][0..4], .little);
|
||||
const str = bytes[idx.* + 4 .. idx.* + 4 + len];
|
||||
idx.* += 4 + len;
|
||||
return .{ .code = str[0 .. len - 1] };
|
||||
},
|
||||
0x0E => {
|
||||
try ensure_available(bytes, idx.*, 4);
|
||||
const len: u32 = std.mem.readInt(u32, bytes[idx.*..][0..4], .little);
|
||||
const str = bytes[idx.* + 4 .. idx.* + 4 + len];
|
||||
idx.* += 4 + len;
|
||||
return .{ .symbol = str[0 .. len - 1] };
|
||||
},
|
||||
0x0F => {
|
||||
try ensure_available(bytes, idx.*, 4);
|
||||
const total: u32 = std.mem.readInt(u32, bytes[idx.*..][0..4], .little);
|
||||
const start = idx.*;
|
||||
idx.* += total;
|
||||
return .{ .opaque_val = .{ .kind = 0x0F, .data = bytes[start..idx.*] } };
|
||||
},
|
||||
0x10 => {
|
||||
try ensure_available(bytes, idx.*, 4);
|
||||
const v: i32 = std.mem.readInt(i32, bytes[idx.*..][0..4], .little);
|
||||
idx.* += 4;
|
||||
return .{ .int32 = v };
|
||||
},
|
||||
0x11 => {
|
||||
try ensure_available(bytes, idx.*, 8);
|
||||
const v: u64 = std.mem.readInt(u64, bytes[idx.*..][0..8], .little);
|
||||
idx.* += 8;
|
||||
return .{ .timestamp = v };
|
||||
},
|
||||
0x12 => {
|
||||
try ensure_available(bytes, idx.*, 8);
|
||||
const v: i64 = std.mem.readInt(i64, bytes[idx.*..][0..8], .little);
|
||||
idx.* += 8;
|
||||
return .{ .int64 = v };
|
||||
},
|
||||
0x13 => {
|
||||
try ensure_available(bytes, idx.*, 16);
|
||||
const v: [16]u8 = bytes[idx.*..][0..16].*;
|
||||
idx.* += 16;
|
||||
return .{ .decimal128 = v };
|
||||
},
|
||||
else => return error.InvalidBson,
|
||||
}
|
||||
}
|
||||
|
||||
/// The value stored under `key` in a document's bytes, or null when absent.
|
||||
/// Nested documents and arrays materialize their spines into `arena`.
|
||||
pub fn get_at(arena: std.mem.Allocator, bytes: []const u8, key: []const u8) error{ InvalidBson, OutOfMemory }!?Value {
|
||||
var idx: usize = 4;
|
||||
while (idx + 1 < bytes.len and bytes[idx] != 0) {
|
||||
const tag = bytes[idx];
|
||||
idx += 1;
|
||||
const k = element_key(bytes, &idx) orelse return error.InvalidBson;
|
||||
if (std.mem.eql(u8, k, key)) return try read_value(arena, bytes, &idx, tag);
|
||||
try skip_value(bytes, &idx, tag);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Serialization
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user