Skip to content

Commit ab8b799

Browse files
authored
speed up main bulk insert batching
Merged clean main-based speed path from PR #135.
1 parent 85ab4cb commit ab8b799

3 files changed

Lines changed: 157 additions & 8 deletions

File tree

src/btree.zig

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,20 @@ pub const BTree = struct {
7373
self.mu.lockUncancelable(runtime.io);
7474
defer self.mu.unlock(runtime.io);
7575

76+
try self.insertLocked(entry);
77+
}
78+
79+
/// Insert or update many entries while holding the B-tree mutex once.
80+
pub fn insertMany(self: *BTree, entries: []const BTreeEntry) !void {
81+
if (entries.len == 0) return;
82+
83+
self.mu.lockUncancelable(runtime.io);
84+
defer self.mu.unlock(runtime.io);
85+
86+
for (entries) |entry| try self.insertLocked(entry);
87+
}
88+
89+
fn insertLocked(self: *BTree, entry: BTreeEntry) !void {
7690
if (self.root == 0) {
7791
// Create first root leaf (btree_leaf, NOT document leaf).
7892
self.root = try self.pf.allocPage(.btree_leaf);

src/collection.zig

Lines changed: 137 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -550,19 +550,151 @@ pub const Collection = struct {
550550
key: []const u8,
551551
value: []const u8,
552552
};
553+
554+
const BatchPreparedDoc = struct {
555+
key: []const u8,
556+
value: []const u8,
557+
key_hash: u64,
558+
doc_id: u64 = 0,
559+
enc_off: usize,
560+
enc_len: usize,
561+
page_no: u32 = 0,
562+
page_off: u16 = 0,
563+
};
564+
553565
pub fn insertBatch(
554566
self: *Collection,
555567
items: []const BatchItem,
556568
/// Optional output. If non-null, must have length >= items.len.
557569
out_doc_ids: ?[]u64,
558570
) !usize {
559-
var inserted: usize = 0;
571+
if (items.len == 0) return 0;
572+
if (out_doc_ids) |ids| {
573+
if (ids.len < items.len) return error.OutputTooSmall;
574+
}
575+
576+
var prepared: std.ArrayList(BatchPreparedDoc) = .empty;
577+
defer prepared.deinit(self.alloc);
578+
var encoded: std.ArrayList(u8) = .empty;
579+
defer encoded.deinit(self.alloc);
580+
var btree_entries: std.ArrayList(BTreeEntry) = .empty;
581+
defer btree_entries.deinit(self.alloc);
582+
583+
try prepared.ensureTotalCapacity(self.alloc, items.len);
584+
try btree_entries.ensureTotalCapacity(self.alloc, items.len);
585+
586+
var estimated_bytes: usize = 0;
560587
for (items) |item| {
561-
const id = try self.insert(item.key, item.value);
562-
if (out_doc_ids) |ids| ids[inserted] = id;
563-
inserted += 1;
588+
const key_value_size = try std.math.add(usize, item.key.len, item.value.len);
589+
const total_size = try std.math.add(usize, DocHeader.size, key_value_size);
590+
if (total_size > page_mod.PAGE_USABLE) return error.PageFull;
591+
estimated_bytes = try std.math.add(usize, estimated_bytes, total_size);
592+
prepared.appendAssumeCapacity(.{
593+
.key = item.key,
594+
.value = item.value,
595+
.key_hash = doc_mod.fnv1a(item.key),
596+
.enc_off = 0,
597+
.enc_len = total_size,
598+
});
599+
}
600+
try encoded.ensureTotalCapacity(self.alloc, estimated_bytes);
601+
602+
var locked_stripes = [_]bool{false} ** STRIPE_COUNT;
603+
for (prepared.items) |item| {
604+
locked_stripes[stripeIndex(item.key_hash)] = true;
605+
}
606+
for (locked_stripes, 0..) |locked, stripe| {
607+
if (locked) self.stripe_locks[stripe].lockUncancelable(runtime.io);
608+
}
609+
defer {
610+
var stripe = locked_stripes.len;
611+
while (stripe > 0) {
612+
stripe -= 1;
613+
if (locked_stripes[stripe]) self.stripe_locks[stripe].unlock(runtime.io);
614+
}
564615
}
565-
return inserted;
616+
617+
const first_doc_id = self.next_doc_id.fetchAdd(@intCast(prepared.items.len), .monotonic);
618+
for (prepared.items, 0..) |*item, i| {
619+
const doc_id = first_doc_id + @as(u64, @intCast(i));
620+
item.doc_id = doc_id;
621+
622+
const hdr = doc_mod.newHeader(doc_id, item.key, item.value);
623+
const d = Doc{ .header = hdr, .key = item.key, .value = item.value };
624+
item.enc_off = encoded.items.len;
625+
const enc = encoded.addManyAsSliceAssumeCapacity(item.enc_len);
626+
_ = try d.encodeBuf(enc);
627+
628+
const txn = self.wal_log.next_lsn.load(.monotonic);
629+
_ = try self.wal_log.write(txn, .doc_insert, 0, 0, enc);
630+
631+
const pno = try self.findOrAllocLeaf(enc.len);
632+
const page_off = self.pf.leafAppend(pno, enc) orelse return error.PageFull;
633+
item.page_no = pno;
634+
item.page_off = page_off;
635+
636+
btree_entries.appendAssumeCapacity(.{
637+
.key_hash = item.key_hash,
638+
.doc_id = doc_id,
639+
.page_no = pno,
640+
.page_off = page_off,
641+
});
642+
}
643+
644+
try self.idx.insertMany(btree_entries.items);
645+
646+
for (prepared.items, btree_entries.items, 0..) |item, entry, i| {
647+
self.hash_idx.put(item.key_hash, entry) catch {};
648+
649+
const epoch = self.epochs.advance();
650+
self.versions.appendVersion(self.alloc, item.doc_id, entry.page_no, entry.page_off, epoch) catch {};
651+
self.key_doc_ids.put(item.key_hash, item.doc_id) catch {};
652+
if (out_doc_ids) |ids| ids[i] = item.doc_id;
653+
654+
if (self.shouldIndexText(item.value)) {
655+
if (self.index_thread == null) {
656+
self.tri.indexFile(item.key, item.value) catch {};
657+
self.words.indexFile(item.key, item.value) catch {};
658+
} else {
659+
const q = &self.index_queue;
660+
if (!q.push(item.key, item.value)) {
661+
self.tri.indexFile(item.key, item.value) catch {};
662+
self.words.indexFile(item.key, item.value) catch {};
663+
} else {
664+
_ = self.index_wake.fetchAdd(1, .release);
665+
runtime.io.futexWake(u32, &self.index_wake.raw, 1);
666+
}
667+
}
668+
}
669+
670+
if (self.vectors) |vc| {
671+
const field = self.vector_field[0..self.vector_field_len];
672+
const dims: usize = vc.dims;
673+
var embed_stack: [4096]f32 = undefined;
674+
const emb = if (dims <= 4096) embed_stack[0..dims] else blk: {
675+
break :blk self.alloc.alloc(f32, dims) catch null;
676+
};
677+
if (emb) |e| {
678+
defer if (dims > 4096) self.alloc.free(e);
679+
if (extractJsonFloatArray(item.value, field, e)) |count| {
680+
if (count == dims) {
681+
vc.append(self.alloc, e) catch {};
682+
self.vec_entries.append(self.alloc, entry) catch {};
683+
}
684+
}
685+
}
686+
}
687+
688+
emitChange(self, .insert, item.key, item.value, item.doc_id);
689+
}
690+
691+
const inserted_count: u64 = @intCast(prepared.items.len);
692+
const old_gc_counter = self.gc_counter.fetchAdd(inserted_count, .monotonic);
693+
if (old_gc_counter / GC_INTERVAL != (old_gc_counter + inserted_count) / GC_INTERVAL) {
694+
_ = self.gcVersions();
695+
}
696+
697+
return prepared.items.len;
566698
}
567699
pub fn insertWithEmbedding(self: *Collection, key: []const u8, value: []const u8, embedding: []const f32) !u64 {
568700
const doc_id = self.insert(key, value) catch |e| return e;

src/server.zig

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -430,14 +430,15 @@ fn doInsert(srv: *Server, tenant_id: []const u8, col_name: []const u8, key: []co
430430
/// Body: NDJSON — one {"key":"...","value":"..."} per line.
431431
/// Response: {"inserted":N,"errors":M,"collection":"...","tenant":"..."}
432432
fn handleBulkInsert(srv: *Server, tenant_id: []const u8, col_name: []const u8, body: []const u8, alloc: std.mem.Allocator) usize {
433-
_ = alloc;
434433
const start_ns = compat.nanoTimestamp();
435434
srv.db.recordTenantOperation(tenant_id) catch return err(429, "tenant ops quota exceeded");
436435
const col = srv.db.collectionForTenant(tenant_id, col_name) catch return err(500, "open collection failed");
437436

438437
var inserted: u32 = 0;
439438
var errors: u32 = 0;
440439
var total_bytes: u64 = 0;
440+
var items: std.ArrayList(collection.Collection.BatchItem) = .empty;
441+
defer items.deinit(alloc);
441442

442443
// Parse NDJSON: iterate lines, each is a {"key":"...","value":...} object
443444
var pos: usize = 0;
@@ -454,14 +455,16 @@ fn handleBulkInsert(srv: *Server, tenant_id: []const u8, col_name: []const u8, b
454455
// Extract value field; fall back to full line for backwards compat.
455456
const value = jsonValue(line, "value") orelse line;
456457

457-
_ = col.insert(key, value) catch {
458+
items.append(alloc, .{ .key = key, .value = value }) catch {
458459
errors += 1;
459460
continue;
460461
};
461-
inserted += 1;
462462
total_bytes += line.len;
463463
}
464464

465+
const inserted_count = col.insertBatch(items.items, null) catch return err(500, "bulk insert failed");
466+
inserted = @intCast(inserted_count);
467+
465468
srv.recordQueryCost(tenant_id, "bulk_insert", inserted, total_bytes, start_ns);
466469

467470
var w = std.Io.Writer.fixed(getBodyBuf());

0 commit comments

Comments
 (0)