Skip to content

Commit 13ae82d

Browse files
committed
fix(btree): spill large values to overflow chains in bulk_load
1 parent 78f5a89 commit 13ae82d

3 files changed

Lines changed: 63 additions & 67 deletions

File tree

src/btree/overflow.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,14 @@ use crate::{RealmId, Result};
2626
/// Header length for chain pages (non-root): `next[8] || data_len[4]`.
2727
pub const OVERFLOW_HEADER_LEN: usize = 12;
2828

29+
/// Values longer than this are stored as an overflow chain rather than inline in
30+
/// a leaf. Shared by the `put` path and `bulk_load` so a repack reproduces the
31+
/// same inline/overflow split the original writes produced.
32+
#[must_use]
33+
pub fn inline_value_threshold(page_size: usize) -> usize {
34+
page_size / 4
35+
}
36+
2937
/// Extra bytes at the start of a v2 root body before the standard header:
3038
/// `refcount[4]`.
3139
const OVERFLOW_ROOT_V2_PREFIX: usize = 4;

src/btree/tree/bulk.rs

Lines changed: 54 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,26 @@
11
//! Bulk-load: build a dense tree from sorted pairs without `CoW` overhead.
22
3+
use std::sync::Arc;
4+
35
use crate::Result;
46
use crate::errors::PagedbError;
57
use crate::vfs::Vfs;
68

79
use crate::btree::leaf::{Leaf, LeafValue};
810
use crate::btree::node;
11+
use crate::btree::overflow;
912

1013
use super::core::BTree;
1114

1215
impl<V: Vfs> BTree<V> {
1316
/// Bulk-load sorted `(key, value)` pairs into a freshly-created tree
1417
/// without any `CoW` overhead. The tree must be empty (`root_page_id == 0`).
1518
///
16-
/// All records are first placed into as few leaves as needed, then internal
17-
/// nodes are built bottom-up. Each page is written exactly once; no freed
18-
/// pages are generated. The resulting layout is dense and compact.
19-
///
20-
/// Overflow values are NOT supported in bulk-load; callers must ensure all
21-
/// values fit inline (`value.len()` ≤ `page_size` / 4). For compaction use-
22-
/// cases, the pairs come from `collect_range` which already resolves
23-
/// overflow chains into inline bytes.
19+
/// Values larger than the inline threshold (`page_size / 4`) are spilled to
20+
/// overflow chains, exactly as the `put` path does, so a dense repack
21+
/// reproduces the original storage shape. Records are then packed into as few
22+
/// leaves as needed and internal nodes are built bottom-up. Each leaf/internal
23+
/// page is written exactly once; the layout is dense and compact.
2424
///
2525
/// Returns `Err` if the tree is not empty.
2626
#[allow(clippy::too_many_lines)]
@@ -34,82 +34,70 @@ impl<V: Vfs> BTree<V> {
3434
return Ok(());
3535
}
3636

37-
// Build leaves greedily: pack as many records as fit into each leaf.
38-
// No sibling pointers yet — we'll wire them up at the end.
39-
let mut leaves: Vec<(u64, Vec<u8>)> = Vec::new(); // (page_id, first_key)
40-
41-
let body_cap = node::body_capacity(self.page_size);
42-
let mut current_leaf = Leaf::new();
37+
// Resolve each value to its stored form, spilling values past the inline
38+
// threshold to overflow chains (same threshold as `put`). Inlining an
39+
// oversized value would exceed leaf capacity and fail the encode.
40+
let realm = self.realm_id;
41+
let ps = self.page_size;
42+
let pager = Arc::clone(&self.pager);
43+
let inline_threshold = overflow::inline_value_threshold(ps);
44+
let mut records: Vec<(Vec<u8>, LeafValue)> = Vec::with_capacity(pairs.len());
45+
for (k, v) in pairs {
46+
let value = if v.len() > inline_threshold {
47+
let total_len = v.len() as u64;
48+
let root_page_id =
49+
overflow::write_chain(&pager, realm, &v, ps, &mut || self.allocate_page())
50+
.await?;
51+
LeafValue::Overflow {
52+
total_len,
53+
root_page_id,
54+
}
55+
} else {
56+
LeafValue::Inline(v)
57+
};
58+
records.push((k, value));
59+
}
4360

44-
let flush_leaf = |leaf: &Leaf, page_id: u64, next_id: u64| {
45-
let _ = (leaf, page_id, next_id); // will write below
46-
};
47-
let _ = flush_leaf; // suppress unused-variable lint (closure is a placeholder)
61+
let body_cap = node::body_capacity(ps);
4862

49-
// First pass: group records into leaves.
50-
let mut leaf_groups: Vec<Vec<(Vec<u8>, Vec<u8>)>> = Vec::new();
51-
for (k, v) in &pairs {
52-
let entry_size = {
53-
let suffix_len = k.len(); // conservative: no prefix compression yet
54-
2 + suffix_len + 2 + v.len() // slot entry (inline value)
55-
};
56-
// Rough check: header + slot_dir entry + record body
63+
// First pass: group records into leaves, sized by encoded record width.
64+
let mut leaf_groups: Vec<Vec<(Vec<u8>, LeafValue)>> = Vec::new();
65+
let mut current: Vec<(Vec<u8>, LeafValue)> = Vec::new();
66+
for (k, value) in records {
67+
// New record's body contribution: suffix-len field + key + value.
68+
// (No prefix compression at build time, so suffix == full key.)
69+
let entry_size = 2 + k.len() + value.encoded_size();
5770
let projected = node::HEADER_LEN
58-
+ (current_leaf.records.len() + 1) * 2
59-
+ current_leaf
60-
.records
71+
+ (current.len() + 1) * 2
72+
+ current
6173
.iter()
6274
.map(|(ck, cv)| 2 + ck.len() + cv.encoded_size())
6375
.sum::<usize>()
6476
+ entry_size;
65-
if projected > body_cap && !current_leaf.records.is_empty() {
66-
leaf_groups.push(
67-
std::mem::take(&mut current_leaf.records)
68-
.into_iter()
69-
.map(|(lk, lv)| {
70-
let vbytes = match lv {
71-
LeafValue::Inline(b) => b,
72-
LeafValue::Overflow { .. } => Vec::new(),
73-
};
74-
(lk, vbytes)
75-
})
76-
.collect(),
77-
);
78-
current_leaf = Leaf::new();
77+
if projected > body_cap && !current.is_empty() {
78+
leaf_groups.push(std::mem::take(&mut current));
7979
}
80-
current_leaf.upsert(k, LeafValue::Inline(v.clone()));
80+
current.push((k, value));
8181
}
82-
if !current_leaf.records.is_empty() {
83-
leaf_groups.push(
84-
std::mem::take(&mut current_leaf.records)
85-
.into_iter()
86-
.map(|(lk, lv)| {
87-
let vbytes = match lv {
88-
LeafValue::Inline(b) => b,
89-
LeafValue::Overflow { .. } => Vec::new(),
90-
};
91-
(lk, vbytes)
92-
})
93-
.collect(),
94-
);
82+
if !current.is_empty() {
83+
leaf_groups.push(current);
9584
}
9685

97-
// Second pass: allocate page_ids and write leaves with correct sibling pointers.
86+
// Second pass: allocate page ids and write leaves with sibling links.
87+
// Input is sorted and grouping preserves order, so each leaf's records
88+
// are already in key order.
89+
let mut leaves: Vec<(u64, Vec<u8>)> = Vec::new(); // (page_id, first_key)
9890
let n_leaves = leaf_groups.len();
9991
let page_ids: Vec<u64> = (0..n_leaves).map(|_| self.allocate_page()).collect();
10092

101-
for (i, group) in leaf_groups.iter().enumerate() {
102-
let mut leaf = Leaf {
93+
for (i, group) in leaf_groups.into_iter().enumerate() {
94+
let first_key = group.first().map(|(k, _)| k.clone()).unwrap_or_default();
95+
let leaf = Leaf {
10396
left_sibling: if i == 0 { 0 } else { page_ids[i - 1] },
10497
right_sibling: if i + 1 < n_leaves { page_ids[i + 1] } else { 0 },
105-
records: group
106-
.iter()
107-
.map(|(k, v)| (k.clone(), LeafValue::Inline(v.clone())))
108-
.collect(),
98+
records: group,
10999
};
110-
leaf.records.sort_by(|(a, _), (b, _)| a.cmp(b)); // already sorted
111100
self.write_leaf(page_ids[i], &leaf).await?;
112-
let first_key = group.first().map(|(k, _)| k.clone()).unwrap_or_default();
113101
leaves.push((page_ids[i], first_key));
114102
}
115103

src/btree/tree/write.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ impl<V: Vfs> BTree<V> {
1717
/// Insert or overwrite a key-value pair.
1818
pub async fn put(&mut self, key: &[u8], value: &[u8]) -> Result<()> {
1919
// Build the leaf value — inline if small enough, overflow chain otherwise.
20-
let leaf_value = if value.len() > self.page_size / 4 {
20+
let leaf_value = if value.len() > overflow::inline_value_threshold(self.page_size) {
2121
let realm = self.realm_id;
2222
let ps = self.page_size;
2323
let pager = Arc::clone(&self.pager);

0 commit comments

Comments
 (0)