Skip to content

Commit 3d3405a

Browse files
authored
[turbopack] use zero copy qfilter deserialization (#90574)
## What Switch turbo-persistence AMQF filters from owned `qfilter::Filter` (heap-allocated) to zero-copy `qfilter::FilterRef` that borrows directly from the memory-mapped meta file. ## Why - **Lower memory usage** — no heap copy of filter data; `FilterRef` is just a pointer into the mmap - **Faster open time** — no deserialization/allocation, just pointer math over the mmap - **OS-managed memory** — mmap pages can be cheaply evicted under pressure (free LRU behavior), unlike heap-allocated `Filter` data which needs to be swapped ## How - Update `qfilter` dependency to the latest alpha release with `FilterRef` support - Switch per-entry AMQF serialization from `turbo_bincode` to `pot` format, which supports zero-copy deserialization - Store `qfilter::FilterRef<'static>` directly in `MetaEntry` (lifetime transmuted from the mmap borrow) - Rely on Rust's struct field drop order guarantee: `MetaFile::entries` is declared before `MetaFile::mmap`, so all `FilterRef`s are dropped before the mmap is unmapped - Update compaction code to work with `FilterRef` avoiding many allocations when merging ### Safety invariants The `FilterRef<'static>` lifetime is transmuted 🙀 — the actual borrow is from `MetaFile::mmap`. This is safe because: 1. `MetaEntry` is never moved out of `MetaFile` (only accessed by `&` reference via `entries()` / `entry()`) 2. Rust drops struct fields in declaration order, and `entries` is declared before `mmap` 3. This is the same pattern used by `ArcBytes`/`RcBytes` in this crate (raw pointer into backing storage) ## Benchmark Results I ran a number of the Read benchmarks and compaction benchmarks and it is all in the noise, which makes sense. We might get a slight benefit from avoiding the `OnceLock` and lazy initialization, we should also have slightly lower maxrss and offer the OS more flexibility during memory pressure From measuring vercel-site, a warm build saves ~40m of MaxRSS
1 parent 3c301ac commit 3d3405a

7 files changed

Lines changed: 188 additions & 141 deletions

File tree

Cargo.lock

Lines changed: 8 additions & 20 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

turbopack/crates/turbo-persistence/Cargo.toml

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ verbose_log = []
1414

1515
[dependencies]
1616
anyhow = { workspace = true }
17-
bincode = { workspace = true }
1817
bitfield = { workspace = true }
1918
byteorder = { workspace = true }
2019
crc32fast = { workspace = true }
@@ -25,15 +24,15 @@ lzzzz = { workspace = true }
2524
memmap2 = "0.9.5"
2625
nohash-hasher = { workspace = true }
2726
parking_lot = { workspace = true }
28-
pot = "3.0.0"
2927
# See https://github.com/vercel/next.js/issues/91708 for why we need the legacy_x86_64_support feature
30-
qfilter = { version = "0.3.0-alpha.2", features = ["serde", "legacy_x86_64_support"] }
28+
qfilter = { version = "0.3.0-alpha.3", features = ["serde", "legacy_x86_64_support"] }
29+
postcard = { workspace = true, features = ["alloc", "use-std"] }
30+
zerocopy = { version = "0.8", features = ["derive"] }
3131
quick_cache = { workspace = true }
3232
rustc-hash = { workspace = true }
3333
smallvec = { workspace = true }
3434
thread_local = { workspace = true }
3535
tracing = { workspace = true }
36-
turbo-bincode= { workspace = true }
3736
xxhash-rust = { workspace = true }
3837

3938
[dev-dependencies]

turbopack/crates/turbo-persistence/src/arc_bytes.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ enum Backing {
2626
#[derive(Clone)]
2727
pub struct ArcBytes {
2828
data: *const [u8],
29+
// Safety: Backing should come last so that it is dropped after the data pointer so we don't
30+
// create a dangling pointer. This isn't really a problem since it is technically ok to have
31+
// dangling _pointers_.
2932
backing: Backing,
3033
}
3134

turbopack/crates/turbo-persistence/src/db.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -528,6 +528,11 @@ impl<S: ParallelScheduler, const FAMILIES: usize> TurboPersistence<S, FAMILIES>
528528
// len is only a snapshot at that time and it can change while we create the filter.
529529
// So we give it 5% more space to make resizes less likely.
530530
let initial_capacity = set.len() * 20 / 19;
531+
// TODO: Using u64::BITS as fingerprint size is wasteful for a
532+
// probabilistic membership filter. A smaller fingerprint (e.g. via
533+
// Filter::new with a target fp_rate) would significantly reduce size,
534+
// but would make merging slower since mismatched fingerprint sizes
535+
// fall back to one-by-one insertion instead of sorted merge.
531536
let mut amqf =
532537
qfilter::Filter::with_fingerprint_size(initial_capacity as u64, u64::BITS as u8)
533538
.unwrap();
@@ -987,7 +992,7 @@ impl<S: ParallelScheduler, const FAMILIES: usize> TurboPersistence<S, FAMILIES>
987992
// during the merge loop. Empty filters (from commits with no
988993
// reads) are discarded.
989994
let used_key_hashes: Option<qfilter::Filter> = {
990-
let filters: Vec<qfilter::Filter> = meta_files
995+
let filters: Vec<qfilter::FilterRef<'_>> = meta_files
991996
.iter()
992997
.filter(|m| m.family() == family)
993998
.filter_map(|meta_file| {
@@ -1001,9 +1006,11 @@ impl<S: ParallelScheduler, const FAMILIES: usize> TurboPersistence<S, FAMILIES>
10011006
None
10021007
} else if filters.len() == 1 {
10031008
// Just directly use the single item
1004-
filters.into_iter().next()
1009+
Some(filters[0].to_owned())
10051010
} else {
10061011
let total_len: u64 = filters.iter().map(|f| f.len()).sum();
1012+
// Fingerprint size must match the source filters to
1013+
// enable the efficient sorted merge path in qfilter.
10071014
let mut merged =
10081015
qfilter::Filter::with_fingerprint_size(total_len, u64::BITS as u8)
10091016
.expect("Failed to create merged AMQF filter");

0 commit comments

Comments
 (0)