vercel/next.js · #97714

Reduce Turbopack cache size with per-family compression

lukesandberg · merged Sep 2, 202619 files · 488 + / 138
Cargo.lock1 + / 0
@@ -10030,6 +10030,7 @@ dependencies = [  "turbo-tasks-malloc",  "xxhash-rust",  "zerocopy",+ "zstd", ]  [[package]]
Cargo.toml1 + / 0
@@ -358,6 +358,7 @@ tracing = "0.1.44" tracing-subscriber = "0.3.16" triomphe = { git = "https://github.com/sokra/triomphe", branch = "sokra/unstable" } xxhash-rust = { version = "0.8.12", features = ["xxh3"] }+zstd = "0.13.2" unsize = "1.1.0" unty = "0.0.4" url = "2.2.2"
turbopack/crates/turbo-persistence/Cargo.toml1 + / 0
@@ -38,6 +38,7 @@ smallvec = { workspace = true } thread_local = { workspace = true } tracing = { workspace = true } xxhash-rust = { workspace = true }+zstd = { workspace = true }  [dev-dependencies] criterion = { workspace = true }
turbopack/crates/turbo-persistence/README.md6 + / 3
@@ -56,6 +56,7 @@ A meta file can contain metadata about multiple SST files. The metadata is store - Header   - 4 bytes magic number (0xFE4ADA4A)   - 4 bytes key family+  - 1 byte compression algorithm, which must match the configuration used to open the database   - 4 bytes count of obsolete SST files   - foreach obsolete SST file     - 4 bytes sequence number of the obsolete SST file@@ -88,9 +89,11 @@ The SST file contains only data without any header.  #### Block Compression -Blocks can be stored compressed (LZ4) or uncompressed. The 4-byte header distinguishes them:+Blocks can be stored compressed or uncompressed. The compression algorithm is specified in the meta file. -- **Header > 0**: Block is LZ4 compressed. Header value is the uncompressed length.+The 4-byte header distinguishes compressed from uncompressed storage:++- **Header > 0**: Block is compressed with the family's configured algorithm. Header value is the uncompressed length. - **Header = 0**: Block is stored uncompressed. Actual length is derived from block offsets.  #### Block Checksum@@ -223,7 +226,7 @@ The plain value compressed with dynamic compression. Each blob file has an 8-byt  - 4 bytes: uncompressed length (u32 big-endian) - 4 bytes: CRC32 checksum of the compressed data (u32 big-endian)-- remaining bytes: LZ4-compressed value data+- remaining bytes: value data compressed with the blob's key-family configuration  The checksum is verified on the compressed data **before** decompression when the blob is read. 
turbopack/crates/turbo-persistence/benches/mod.rs14 + / 6
@@ -10,9 +10,9 @@ use quick_cache::sync::GuardResult; use rand::{RngExt, SeedableRng, rngs::SmallRng, seq::SliceRandom}; use tempfile::TempDir; use turbo_persistence::{-    ArcBytes, BlockCache, CompactConfig, DbConfig as TpDbConfig, Entry, EntryValue, FamilyConfig,-    FamilyKind, MetaEntryFlags, SerialScheduler, StaticSortedFile, StaticSortedFileMetaData,-    TurboPersistence, hash_key, write_static_stored_file,+    ArcBytes, BlockCache, CompactConfig, Compression, DbConfig as TpDbConfig, Entry, EntryValue,+    FamilyConfig, FamilyKind, MetaEntryFlags, SerialScheduler, StaticSortedFile,+    StaticSortedFileMetaData, TurboPersistence, hash_key, write_static_stored_file, }; use turbo_tasks_malloc::TurboMalloc; @@ -622,6 +622,7 @@ fn prefill_multi_value_database(         family_configs: [FamilyConfig {             name: "test",             kind: FamilyKind::MultiValue,+            compression: Compression::Lz4,         }],     };     let db =@@ -696,6 +697,7 @@ fn open_multi_value_db(path: &Path) -> TurboPersistence<SerialScheduler, 1> {         family_configs: [FamilyConfig {             name: "test",             kind: FamilyKind::MultiValue,+            compression: Compression::Lz4,         }],     };     TurboPersistence::<SerialScheduler, 1>::open_with_config(path.to_path_buf(), db_config).unwrap()@@ -964,6 +966,7 @@ fn bench_write_multi_value(c: &mut Criterion) {                             family_configs: [FamilyConfig {                                 name: "test",                                 kind: FamilyKind::MultiValue,+                                compression: Compression::Lz4,                             }],                         };                         let db = TurboPersistence::<SerialScheduler, 1>::open_with_config(@@ -1201,15 +1204,20 @@ fn bench_static_sorted_file_lookup(c: &mut Criterion) {             // Create temp directory and write SST file             let tempdir = tempfile::tempdir().unwrap();             let sst_path = tempdir.path().join("00000001.sst");-            let (meta, _file) =-                write_static_stored_file(&entries, &sst_path, MetaEntryFlags::FRESH).unwrap();+            let (meta, _file) = write_static_stored_file(+                &entries,+                &sst_path,+                MetaEntryFlags::FRESH,+                Compression::Lz4,+            )+            .unwrap();              // Open the SST file             let sst_meta = StaticSortedFileMetaData {                 sequence_number: 1,                 block_count: meta.block_count,             };-            let sst = StaticSortedFile::open(tempdir.path(), sst_meta).unwrap();+            let sst = StaticSortedFile::open(tempdir.path(), sst_meta, Compression::Lz4).unwrap();              // Create block caches             let key_block_cache: BlockCache = BlockCache::with(
turbopack/crates/turbo-persistence/src/arc_bytes.rs7 + / 1
@@ -9,6 +9,7 @@ use std::{ use memmap2::Mmap;  use crate::{+    Compression,     compression::decompress_into_arc,     shared_bytes::{SharedBytes, is_subslice_of}, };@@ -144,8 +145,13 @@ impl SharedBytes for ArcBytes {         }     } -    fn from_decompressed(uncompressed_length: u32, block: &[u8]) -> anyhow::Result<Self> {+    fn from_decompressed(+        compression: Compression,+        uncompressed_length: u32,+        block: &[u8],+    ) -> anyhow::Result<Self> {         Ok(ArcBytes::from(decompress_into_arc(+            compression,             uncompressed_length,             block,         )?))
turbopack/crates/turbo-persistence/src/bin/sst_inspect.rs17 + / 4
@@ -20,7 +20,7 @@ use fs_err::{self as fs, File}; use lzzzz::lz4::decompress; use memmap2::Mmap; use turbo_persistence::{-    BLOCK_HEADER_SIZE, MAX_INLINE_VALUE_SIZE, checksum_block,+    BLOCK_HEADER_SIZE, Compression, MAX_INLINE_VALUE_SIZE, checksum_block,     meta_file::MetaFile,     mmap_helper::advise_mmap_for_persistence,     read_current_version,@@ -133,6 +133,7 @@ impl SstStats { struct SstInfo {     sequence_number: u32,     block_count: u16,+    compression: Compression, }  /// Accumulates statistics for a single entry of the given type.@@ -266,7 +267,8 @@ fn collect_sst_info(db_path: &Path) -> Result<BTreeMap<u32, Vec<SstInfo>>> {     let mut meta_files: Vec<MetaFile> = meta_seqs         .iter()         .map(|&seq| {-            MetaFile::open(db_path, seq).with_context(|| format!("Failed to open {seq:08}.meta"))+            MetaFile::open(db_path, seq, None)+                .with_context(|| format!("Failed to open {seq:08}.meta"))         })         .collect::<Result<_>>()?; @@ -283,6 +285,7 @@ fn collect_sst_info(db_path: &Path) -> Result<BTreeMap<u32, Vec<SstInfo>>> {             family_sst_info.entry(family).or_default().push(SstInfo {                 sequence_number: entry.sequence_number(),                 block_count: entry.block_count(),+                compression: meta.compression(),             });         }     }@@ -304,6 +307,7 @@ fn read_block(     block_offsets_start: usize,     block_index: u16,     sequence_number: u32,+    compression: Compression, ) -> Result<RawBlock> {     let offset = block_offsets_start + block_index as usize * size_of::<u32>(); @@ -343,7 +347,13 @@ fn read_block(      let data = if was_compressed {         let mut buffer = vec![0u8; uncompressed_length as usize];-        let bytes_written = decompress(compressed_data, &mut buffer)?;+        let bytes_written = match compression {+            Compression::Lz4 => {+                decompress(compressed_data, &mut buffer).context("LZ4 decompression failed")?+            }+            Compression::Zstd3 => zstd::bulk::decompress_to_buffer(compressed_data, &mut buffer)+                .context("zstd decompression failed")?,+        };         assert_eq!(             bytes_written, uncompressed_length as usize,             "Decompressed length does not match expected"@@ -471,6 +481,7 @@ fn iter_key_block_entry_types(  /// Analyze an SST file and return entry type statistics fn analyze_sst_file(db_path: &Path, info: &SstInfo) -> Result<SstStats> {+    let compression = info.compression;     let filename = format!("{:08}.sst", info.sequence_number);     let path = db_path.join(&filename); @@ -496,6 +507,7 @@ fn analyze_sst_file(db_path: &Path, info: &SstInfo) -> Result<SstStats> {         block_offsets_start,         index_block_index,         info.sequence_number,+        compression,     )?;     let key_block_indices = parse_key_block_indices(&index_raw.data); @@ -512,6 +524,7 @@ fn analyze_sst_file(db_path: &Path, info: &SstInfo) -> Result<SstStats> {             block_offsets_start,             block_index,             info.sequence_number,+            compression,         ) {             Ok(raw) => raw,             Err(e) => {@@ -930,7 +943,7 @@ fn main() -> Result<()> {         db_path.display()     ); -    // Analyze and report by family+    // Analyze and report by family.     for (family, sst_list) in &family_sst_info {         let mut family_stats = SstStats::default();         let mut sst_stats_list: Vec<(u32, SstStats)> = Vec::new();
turbopack/crates/turbo-persistence/src/compression.rs114 + / 18
@@ -1,25 +1,59 @@-use std::{mem::MaybeUninit, rc::Rc, sync::Arc};+use std::{cell::RefCell, mem::MaybeUninit, rc::Rc, sync::Arc}; -use anyhow::{Context, Result};+use anyhow::{Context, Result, ensure}; use lzzzz::lz4::{self, decompress}; +/// Compression algorithm used for a family's SST blocks and blob values.+#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]+#[repr(u8)]+pub enum Compression {+    /// Fast LZ4 compression using the default acceleration level.+    #[default]+    Lz4 = 0,+    /// Zstandard compression at level 3.+    Zstd3 = 1,+}++thread_local! {+    /// Zstd decompression contexts are reusable and relatively expensive to create. Keep one per+    /// worker thread to avoid allocation on every block read without a global lock.+    static ZSTD_DECOMPRESSOR: RefCell<zstd::bulk::Decompressor<'static>> = RefCell::new(+        zstd::bulk::Decompressor::new().expect("zstd decompressor initialization should succeed")+    );+}+ /// Decompresses `block` into `dest`, verifying the output length matches `expected_len`.-fn decompress_block(block: &[u8], dest: &mut [u8], expected_len: u32) -> Result<()> {+fn decompress_block(+    compression: Compression,+    block: &[u8],+    dest: &mut [u8],+    expected_len: u32,+) -> Result<()> {     debug_assert!(         expected_len > 0,         "decompress_block called with uncompressed_length=0; uncompressed blocks should use \          zero-copy mmap path"     );-    let bytes_written = decompress(block, dest).with_context(|| {+    let bytes_written = match compression {+        Compression::Lz4 => decompress(block, dest).map_err(anyhow::Error::from),+        Compression::Zstd3 => ZSTD_DECOMPRESSOR.with_borrow_mut(|decompressor| {+            decompressor+                .decompress_to_buffer(block, dest)+                .map_err(anyhow::Error::from)+        }),+    }+    .with_context(|| {         format!(-            "Failed to decompress block ({} bytes compressed, {} bytes uncompressed)",+            "Failed to decompress {compression:?} block ({} bytes compressed, {} bytes \+             uncompressed)",             block.len(),             expected_len         )     })?;-    assert_eq!(-        bytes_written, expected_len as usize,-        "Decompressed length does not match expected length"+    ensure!(+        bytes_written == expected_len as usize,+        "Decompressed length does not match expected length: decompressed {bytes_written} bytes, \+         expected {expected_len}"     );     Ok(()) }@@ -28,27 +62,35 @@ fn decompress_block(block: &[u8], dest: &mut [u8], expected_len: u32) -> Result< /// /// The caller must ensure `uncompressed_length > 0` (i.e., the block is actually compressed). /// Uncompressed blocks should be handled via zero-copy mmap slices before calling this.-pub fn decompress_into_arc(uncompressed_length: u32, block: &[u8]) -> Result<Arc<[u8]>> {+pub(crate) fn decompress_into_arc(+    compression: Compression,+    uncompressed_length: u32,+    block: &[u8],+) -> Result<Arc<[u8]>> {     // Allocate directly into an Arc to avoid a copy. The buffer is uninitialized;     // decompression will overwrite it completely (verified by decompress_block).     let buffer: Arc<[MaybeUninit<u8>]> = Arc::new_uninit_slice(uncompressed_length as usize);-    // Safety: decompression will fully initialize the buffer (verified by the assert in+    // Safety: decompression will fully initialize the buffer (verified by the length check in     // decompress_block).     let mut buffer = unsafe { buffer.assume_init() };     // We just created this Arc so refcount is 1; get_mut always succeeds.     let dest = Arc::get_mut(&mut buffer).expect("Arc refcount should be 1");-    decompress_block(block, dest, uncompressed_length)?;+    decompress_block(compression, block, dest, uncompressed_length)?;     Ok(buffer) }  /// Like [`decompress_into_arc`] but returns an `Rc<[u8]>` for thread-local use.-pub fn decompress_into_rc(uncompressed_length: u32, block: &[u8]) -> Result<Rc<[u8]>> {+pub(crate) fn decompress_into_rc(+    compression: Compression,+    uncompressed_length: u32,+    block: &[u8],+) -> Result<Rc<[u8]>> {     let buffer: Rc<[MaybeUninit<u8>]> = Rc::new_uninit_slice(uncompressed_length as usize);-    // Safety: decompression will fully initialize the buffer (verified by the assert in+    // Safety: decompression will fully initialize the buffer (verified by the length check in     // decompress_block).     let mut buffer = unsafe { buffer.assume_init() };     let dest = Rc::get_mut(&mut buffer).expect("Rc refcount should be 1");-    decompress_block(block, dest, uncompressed_length)?;+    decompress_block(compression, block, dest, uncompressed_length)?;     Ok(buffer) } @@ -57,8 +99,62 @@ pub fn checksum_block(data: &[u8]) -> u32 {     crc32fast::hash(data) } -#[tracing::instrument(level = "trace", skip_all)]-pub fn compress_into_buffer(block: &[u8], buffer: &mut Vec<u8>) -> Result<()> {-    lz4::compress_to_vec(block, buffer, lz4::ACC_LEVEL_DEFAULT).context("Compression failed")?;-    Ok(())+/// Reusable compressor for a stream of blocks using the same family configuration.+pub(crate) struct Compressor {+    compression: Compression,+    zstd: Option<zstd::bulk::Compressor<'static>>,+}++impl Compressor {+    pub(crate) fn new(compression: Compression) -> Result<Self> {+        let zstd = match compression {+            Compression::Zstd3 => {+                Some(zstd::bulk::Compressor::new(3).context("Failed to create zstd compressor")?)+            }+            Compression::Lz4 => None,+        };+        Ok(Self { compression, zstd })+    }++    #[tracing::instrument(level = "trace", skip_all)]+    pub(crate) fn compress_into_buffer(+        &mut self,+        block: &[u8],+        buffer: &mut Vec<u8>,+    ) -> Result<()> {+        match self.compression {+            Compression::Lz4 => {+                lz4::compress_to_vec(block, buffer, lz4::ACC_LEVEL_DEFAULT)+                    .context("LZ4 compression failed")?;+            }+            Compression::Zstd3 => {+                buffer.reserve(zstd::zstd_safe::compress_bound(block.len()));+                self.zstd+                    .as_mut()+                    .expect("zstd compressor not initialized")+                    .compress_to_buffer(block, buffer)+                    .context("zstd compression failed")?;+            }+        }+        Ok(())+    }+}++#[cfg(test)]+mod tests {+    use super::*;++    #[test]+    fn compression_round_trips() {+        let input = b"turbo persistence compression ".repeat(1024);+        for compression in [Compression::Lz4, Compression::Zstd3] {+            let mut compressor = Compressor::new(compression).unwrap();+            let mut compressed = Vec::new();+            compressor+                .compress_into_buffer(&input, &mut compressed)+                .unwrap();+            let output = decompress_into_arc(compression, input.len() as u32, &compressed).unwrap();+            assert_eq!(&*output, input);+        }+    } }
turbopack/crates/turbo-persistence/src/db.rs34 + / 13
@@ -29,7 +29,7 @@ use tracing::span::EnteredSpan;  pub use crate::compaction::selector::CompactConfig; use crate::{-    DbConfig, FamilyKind, QueryKey,+    Compression, DbConfig, FamilyKind, QueryKey,     arc_bytes::ArcBytes,     compaction::selector::{Compactable, get_merge_segments},     compression::{checksum_block, decompress_into_arc},@@ -324,7 +324,7 @@ pub struct TurboPersistence<S: ParallelScheduler, const FAMILIES: usize> {     /// A cache for decompressed value blocks. Allocated lazily on first read via     /// [`Self::value_block_cache`]; see [`Self::key_block_cache`].     value_block_cache: OnceLock<BlockCache>,-    /// Per-family configuration for file limits.+    /// Per-family storage configuration.     config: DbConfig<FAMILIES>,     /// Statistics for the database.     #[cfg(feature = "stats")]@@ -634,7 +634,7 @@ impl<S: ParallelScheduler, const FAMILIES: usize> TurboPersistence<S, FAMILIES>         let mut meta_files = self             .parallel_scheduler             .parallel_map_collect::<_, _, Result<Vec<MetaFile>>>(&meta_files, |&seq| {-                let meta_file = MetaFile::open(&self.path, seq)?;+                let meta_file = MetaFile::open(&self.path, seq, Some(&self.config.family_configs))?;                 Ok(meta_file)             })?; @@ -653,7 +653,7 @@ impl<S: ParallelScheduler, const FAMILIES: usize> TurboPersistence<S, FAMILIES>      /// Reads and decompresses a blob file. This is not backed by any cache.     #[tracing::instrument(level = "info", name = "reading database blob", skip_all)]-    fn read_blob(&self, seq: u32) -> Result<ArcBytes> {+    fn read_blob(&self, seq: u32, compression: Compression) -> Result<ArcBytes> {         let path = self.path.join(format!("{seq:08}.blob"));         let file = File::open(&path)?;         let mmap = unsafe { Mmap::map(file.file()) }.with_context(|| {@@ -686,7 +686,7 @@ impl<S: ParallelScheduler, const FAMILIES: usize> TurboPersistence<S, FAMILIES>             );         } -        let buffer = decompress_into_arc(uncompressed_length, reader)?;+        let buffer = decompress_into_arc(compression, uncompressed_length, reader)?;         Ok(ArcBytes::from(buffer))     } @@ -926,7 +926,8 @@ impl<S: ParallelScheduler, const FAMILIES: usize> TurboPersistence<S, FAMILIES>             .parallel_map_collect_owned::<_, _, Result<Vec<_>>>(sync_items, |item| match item {                 SyncItem::Meta(seq, file) => {                     file.sync_data()?;-                    let meta_file = MetaFile::open(&self.path, seq)?;+                    let meta_file =+                        MetaFile::open(&self.path, seq, Some(&self.config.family_configs))?;                     Ok(SyncResult::Meta(meta_file))                 }                 SyncItem::Sst(file) => {@@ -1593,7 +1594,11 @@ impl<S: ParallelScheduler, const FAMILIES: usize> TurboPersistence<S, FAMILIES>                                     let meta_index = ssts_with_ranges[index].meta_index;                                     let index_in_meta = ssts_with_ranges[index].index_in_meta;                                     let entry = meta_files[meta_index].entry(index_in_meta);-                                    StaticSortedFileIter::open(path, entry.sst_metadata())+                                    StaticSortedFileIter::open(+                                        path,+                                        entry.sst_metadata(),+                                        self.config.family_configs[family as usize].compression,+                                    )                                 })                                 .collect::<Result<Vec<_>>>()?; @@ -1610,17 +1615,19 @@ impl<S: ParallelScheduler, const FAMILIES: usize> TurboPersistence<S, FAMILIES>                                 /// used set).                                 writer: Option<(u32, StreamingSstWriter<LookupEntry>)>,                                 flags: MetaEntryFlags,+                                compression: Compression,                                 new_sst_files:                                     Vec<(u32, File, StaticSortedFileBuilderMeta<'static>)>,                                 /// Hash of the last key added. Used to ensure we only split                                 /// SST files at key boundaries (not mid-key-group for MultiValue).                                 last_hash: Option<u64>,                             }                             impl Collector {-                                fn new(flags: MetaEntryFlags) -> Self {+                                fn new(flags: MetaEntryFlags, compression: Compression) -> Self {                                     Self {                                         writer: None,                                         flags,+                                        compression,                                         new_sst_files: Vec::new(),                                         last_hash: None,                                     }@@ -1641,6 +1648,7 @@ impl<S: ParallelScheduler, const FAMILIES: usize> TurboPersistence<S, FAMILIES>                                             &sst_path,                                             self.flags,                                             MAX_ENTRIES_PER_COMPACTED_FILE as u64,+                                            self.compression,                                         )?;                                         self.writer = Some((seq, writer));                                     }@@ -1700,8 +1708,12 @@ impl<S: ParallelScheduler, const FAMILIES: usize> TurboPersistence<S, FAMILIES>                                     }                                 }                             }-                            let mut used_collector = Collector::new(MetaEntryFlags::WARM);-                            let mut unused_collector = Collector::new(MetaEntryFlags::COLD);+                            let compression =+                                self.config.family_configs[family as usize].compression;+                            let mut used_collector =+                                Collector::new(MetaEntryFlags::WARM, compression);+                            let mut unused_collector =+                                Collector::new(MetaEntryFlags::COLD, compression);                             let mut current_key: Option<RcBytes> = None;                             let mut keys_written = 0; @@ -1834,7 +1846,10 @@ impl<S: ParallelScheduler, const FAMILIES: usize> TurboPersistence<S, FAMILIES>                     let mut blob_seq_numbers_to_delete = Vec::with_capacity(blob_delete_len);                      let meta_seq = sequence_number.fetch_add(1, Ordering::SeqCst) + 1;-                    let mut meta_file_builder = MetaFileBuilder::new(family);+                    let mut meta_file_builder = MetaFileBuilder::new(+                        family,+                        self.config.family_configs[family as usize].compression,+                    );                      let mut keys_written = 0;                     self.parallel_scheduler.block_in_place(|| {@@ -2091,7 +2106,10 @@ impl<S: ParallelScheduler, const FAMILIES: usize> TurboPersistence<S, FAMILIES>                                 LookupValue::Blob { sequence_number } => {                                     #[cfg(feature = "stats")]                                     self.stats.hits_blob.fetch_add(1, Ordering::Relaxed);-                                    let blob = self.read_blob(sequence_number)?;+                                    let blob = self.read_blob(+                                        sequence_number,+                                        self.config.family_configs[family].compression,+                                    )?;                                     if deleted_values.iter().any(|d| **d == *blob) {                                         continue;                                     }@@ -2230,7 +2248,10 @@ impl<S: ParallelScheduler, const FAMILIES: usize> TurboPersistence<S, FAMILIES>                     LookupValue::Blob { sequence_number } => {                         #[cfg(feature = "stats")]                         self.stats.hits_blob.fetch_add(1, Ordering::Relaxed);-                        let blob = self.read_blob(sequence_number)?;+                        let blob = self.read_blob(+                            sequence_number,+                            self.config.family_configs[family].compression,+                        )?;                         result_size += blob.len();                         Some(blob)                     }
turbopack/crates/turbo-persistence/src/lib.rs6 + / 4
@@ -29,7 +29,7 @@ mod write_batch; mod tests;  pub use arc_bytes::ArcBytes;-pub use compression::checksum_block;+pub use compression::{Compression, checksum_block}; pub use db::{     CommitStats, CompactConfig, CurrentDbVersion, MetaFileEntryInfo, MetaFileInfo,     TurboPersistence, read_current_version,@@ -54,12 +54,13 @@ pub enum FamilyKind { pub struct FamilyConfig {     pub name: &'static str,     pub kind: FamilyKind,+    pub compression: Compression, } -/// Database-wide configuration with per-family settings.+/// Database-wide configuration with per-family storage settings. ///-/// Each family (keyspace) can have different file size limits to optimize-/// for its specific access patterns and data characteristics.+/// Each family (keyspace) can select storage behavior suited to its access patterns and data+/// characteristics. #[derive(Clone, Debug)] pub struct DbConfig<const FAMILIES: usize> {     pub family_configs: [FamilyConfig; FAMILIES],@@ -71,6 +72,7 @@ impl<const FAMILIES: usize> Default for DbConfig<FAMILIES> {             family_configs: [FamilyConfig {                 name: "unknown",                 kind: FamilyKind::SingleValue,+                compression: Compression::Lz4,             }; FAMILIES],         }     }
turbopack/crates/turbo-persistence/src/meta_file.rs54 + / 12
@@ -5,7 +5,7 @@ use std::{     sync::OnceLock, }; -use anyhow::{Context, Result, bail};+use anyhow::{Context, Result, bail, ensure}; use bitfield::bitfield; use byteorder::{BE, ReadBytesExt}; use fs_err::File;@@ -14,7 +14,7 @@ use smallvec::SmallVec; use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout, Ref, big_endian as be};  use crate::{-    QueryKey,+    Compression, FamilyConfig, QueryKey,     lookup_entry::LookupValue,     mmap_helper::advise_mmap_for_persistence,     static_sorted_file::{BlockCache, SstLookupResult, StaticSortedFile, StaticSortedFileMetaData},@@ -117,6 +117,8 @@ pub struct MetaEntry {     ///     /// The `'static` lifetime is transmuted — the actual borrow is from `MetaFile::mmap`.     amqf: qfilter::FilterRef<'static>,+    /// Compression recorded in this entry's meta file.+    compression: Compression,     /// The static sorted file that is lazily loaded     sst: OnceLock<StaticSortedFile>, }@@ -153,12 +155,14 @@ impl MetaEntry {      fn sst(&self, meta: &MetaFile) -> Result<&StaticSortedFile> {         self.sst.get_or_try_init(|| {-            StaticSortedFile::open(&meta.db_path, self.sst_data).with_context(|| {-                format!(-                    "Unable to open static sorted file referenced from {:08}.meta",-                    meta.sequence_number()-                )-            })+            StaticSortedFile::open(&meta.db_path, self.sst_data, self.compression).with_context(+                || {+                    format!(+                        "Unable to open static sorted file referenced from {:08}.meta",+                        meta.sequence_number()+                    )+                },+            )         })     } @@ -244,6 +248,8 @@ pub struct MetaFile {     sequence_number: u32,     /// The key family of the SST files in this meta file.     family: u32,+    /// Compression recorded for this family.+    compression: Compression,     /// The entries of the file. Dropped before `mmap` (field declaration order).     entries: Vec<MetaEntry>,     /// The entries that have been marked as obsolete.@@ -266,14 +272,28 @@ pub struct MetaFile { impl MetaFile {     /// Opens a meta file at the given path. Memory maps the entire file and eagerly deserializes     /// all AMQF filters as zero-copy [`qfilter::FilterRef`]s that borrow from the mmap.-    pub fn open(db_path: &Path, sequence_number: u32) -> Result<Self> {+    pub fn open(+        db_path: &Path,+        sequence_number: u32,+        family_configs: Option<&[FamilyConfig]>,+    ) -> Result<Self> {         let filename = format!("{sequence_number:08}.meta");         let path = db_path.join(&filename);-        Self::open_internal(db_path.to_path_buf(), sequence_number, &path)-            .with_context(|| format!("Unable to open meta file {filename}"))+        Self::open_internal(+            db_path.to_path_buf(),+            sequence_number,+            &path,+            family_configs,+        )+        .with_context(|| format!("Unable to open meta file {filename}"))     } -    fn open_internal(db_path: PathBuf, sequence_number: u32, path: &Path) -> Result<Self> {+    fn open_internal(+        db_path: PathBuf,+        sequence_number: u32,+        path: &Path,+        family_configs: Option<&[FamilyConfig]>,+    ) -> Result<Self> {         let file = File::open(path)?;         let mmap = unsafe { MmapOptions::new().map(file.file()) }.context("Failed to mmap")?;         #[cfg(unix)]@@ -287,6 +307,22 @@ impl MetaFile {             bail!("Invalid magic number");         }         let family = reader.read_u32::<BE>()?;+        let compression = match reader.read_u8()? {+            value if value == Compression::Lz4 as u8 => Compression::Lz4,+            value if value == Compression::Zstd3 as u8 => Compression::Zstd3,+            value => bail!("Invalid compression algorithm {value}"),+        };+        if let Some(configs) = family_configs {+            let configured = configs+                .get(family as usize)+                .with_context(|| format!("No configuration for family {family}"))?+                .compression;+            ensure!(+                compression == configured,+                "Compression configuration mismatch for family {family}: meta file uses \+                 {compression:?}, runtime config uses {configured:?}"+            );+        }         let obsolete_count = reader.read_u32::<BE>()?;         let mut obsolete_sst_files = Vec::with_capacity(obsolete_count as usize);         for _ in 0..obsolete_count {@@ -344,6 +380,7 @@ impl MetaFile {                 flags,                 amqf_data_offset: start_of_amqf_data_offset..end_of_amqf_data_offset,                 amqf,+                compression,                 sst: OnceLock::new(),             });             start_of_amqf_data_offset = end_of_amqf_data_offset;@@ -356,6 +393,7 @@ impl MetaFile {             db_path,             sequence_number,             family,+            compression,             entries,             obsolete_entries: Vec::new(),             obsolete_sst_files,@@ -386,6 +424,10 @@ impl MetaFile {         self.family     } +    pub fn compression(&self) -> Compression {+        self.compression+    }+     /// The on-disk size of this meta file in bytes (the length of its memory map).     pub fn byte_size(&self) -> u64 {         self.mmap.len() as u64
turbopack/crates/turbo-persistence/src/meta_file_builder.rs5 + / 1
@@ -10,12 +10,14 @@ use qfilter::Filter; use zerocopy::IntoBytes;  use crate::{+    Compression,     meta_file::{EntryHeader, META_FILE_MAGIC},     static_sorted_file_builder::StaticSortedFileBuilderMeta, };  pub struct MetaFileBuilder<'a> {     family: u32,+    compression: Compression,     /// Entries in the meta file, tuples of (sequence_number, StaticSortedFileBuilderMetaResult)     entries: Vec<(u32, StaticSortedFileBuilderMeta<'a>)>,     /// Obsolete SST files, represented by their sequence numbers@@ -25,9 +27,10 @@ pub struct MetaFileBuilder<'a> { }  impl<'a> MetaFileBuilder<'a> {-    pub fn new(family: u32) -> Self {+    pub fn new(family: u32, compression: Compression) -> Self {         Self {             family,+            compression,             entries: Vec::new(),             obsolete_sst_files: Vec::new(),             used_key_hashes_amqf: None,@@ -59,6 +62,7 @@ impl<'a> MetaFileBuilder<'a> {         let mut file = CountingWriter::new(BufWriter::new(File::create(file)?));         file.write_u32::<BE>(META_FILE_MAGIC)?; // Magic number         file.write_u32::<BE>(self.family)?;+        file.write_u8(self.compression as u8)?;          self.obsolete_sst_files.sort();         file.write_u32::<BE>(self.obsolete_sst_files.len() as u32)?;
turbopack/crates/turbo-persistence/src/rc_bytes.rs7 + / 1
@@ -9,6 +9,7 @@ use std::{ use memmap2::Mmap;  use crate::{+    Compression,     compression::decompress_into_rc,     shared_bytes::{SharedBytes, is_subslice_of}, };@@ -124,8 +125,13 @@ impl SharedBytes for RcBytes {         }     } -    fn from_decompressed(uncompressed_length: u32, block: &[u8]) -> anyhow::Result<Self> {+    fn from_decompressed(+        compression: Compression,+        uncompressed_length: u32,+        block: &[u8],+    ) -> anyhow::Result<Self> {         Ok(RcBytes::from(decompress_into_rc(+            compression,             uncompressed_length,             block,         )?))
turbopack/crates/turbo-persistence/src/shared_bytes.rs7 + / 1
@@ -2,6 +2,8 @@ use std::ops::{Deref, Range};  use memmap2::Mmap; +use crate::Compression;+ /// Trait abstracting over `ArcBytes` and `RcBytes`. /// /// Both types are owned byte slices backed by either a ref-counted heap@@ -36,7 +38,11 @@ pub trait SharedBytes: Clone + Deref<Target = [u8]> + Sized {     unsafe fn from_mmap(mmap: &Self::MmapHandle, subslice: &[u8]) -> Self;      /// Creates an instance from a decompressed block.-    fn from_decompressed(uncompressed_length: u32, block: &[u8]) -> anyhow::Result<Self>;+    fn from_decompressed(+        compression: Compression,+        uncompressed_length: u32,+        block: &[u8],+    ) -> anyhow::Result<Self>; }  /// Returns `true` if `subslice` lies entirely within `backing`.
turbopack/crates/turbo-persistence/src/static_sorted_file.rs86 + / 36
@@ -17,7 +17,7 @@ use rustc_hash::FxHasher; use smallvec::SmallVec;  use crate::{-    QueryKey,+    Compression, QueryKey,     arc_bytes::ArcBytes,     be,     compression::checksum_block,@@ -155,6 +155,7 @@ trait ValueBlockCache<B: SharedBytes> {         mmap: &B::MmapHandle,         meta: &StaticSortedFileMetaData,         block_index: u16,+        compression: Compression,     ) -> Result<B>; } @@ -174,26 +175,39 @@ impl ValueBlockCache<ArcBytes> for ArcBlockCacheReader<'_> {         mmap: &Arc<Mmap>,         meta: &StaticSortedFileMetaData,         block_index: u16,+        compression: Compression,     ) -> Result<ArcBytes> {-        get_or_cache_block(mmap, meta, block_index, self.cache, self.verified_blocks)+        get_or_cache_block(+            mmap,+            meta,+            block_index,+            self.cache,+            self.verified_blocks,+            compression,+        )     } }  /// Iteration-path: lightweight single-entry cache for sequential reads.-impl ValueBlockCache<RcBytes> for &mut Option<(u16, RcBytes)> {+struct RcBlockCacheReader<'a> {+    cache: &'a mut Option<(u16, RcBytes)>,+}++impl ValueBlockCache<RcBytes> for RcBlockCacheReader<'_> {     fn get_or_read(         self,         mmap: &Rc<Mmap>,         meta: &StaticSortedFileMetaData,         block_index: u16,+        compression: Compression,     ) -> Result<RcBytes> {-        if let Some((idx, block)) = self.as_ref()+        if let Some((idx, block)) = self.cache.as_ref()             && *idx == block_index         {             return Ok(block.clone());         }-        let block: RcBytes = read_block_generic(mmap, meta, block_index)?;-        *self = Some((block_index, block.clone()));+        let block: RcBytes = read_block_generic(mmap, meta, block_index, compression)?;+        *self.cache = Some((block_index, block.clone()));         Ok(block)     } }@@ -226,12 +240,17 @@ pub struct StaticSortedFile {     /// bitmap the CRC would be re-computed on every access. `Relaxed` ordering     /// suffices: racing first-time verifications are idempotent.     verified_blocks: Box<[AtomicU64]>,+    compression: Compression, }  impl StaticSortedFile {-    /// Opens an SST file at the given path. This memory maps the file, but does not read it yet.-    /// It's lazy read on demand.-    pub fn open(db_path: &Path, meta: StaticSortedFileMetaData) -> Result<Self> {+    /// Opens an SST file at the given path with the compression algorithm specified by its meta+    /// file. This memory maps the file, but does not read it yet.+    pub fn open(+        db_path: &Path,+        meta: StaticSortedFileMetaData,+        compression: Compression,+    ) -> Result<Self> {         let filename = format!("{:08}.sst", meta.sequence_number);         let path = db_path.join(&filename);         let file = File::open(&path)?;@@ -257,6 +276,7 @@ impl StaticSortedFile {             meta,             mmap: Arc::new(mmap),             verified_blocks,+            compression,         })     } @@ -281,6 +301,7 @@ impl StaticSortedFile {             index_block_index,             key_block_cache,             &self.verified_blocks,+            self.compression,         )?;         let key_block_index = self.lookup_index_block(&index_block, key_hash)?; @@ -290,6 +311,7 @@ impl StaticSortedFile {             key_block_index,             key_block_cache,             &self.verified_blocks,+            self.compression,         )?;         let reader = ArcBlockCacheReader {             cache: value_block_cache,@@ -491,7 +513,15 @@ impl StaticSortedFile {         key_block_arc: &ArcBytes,         reader: ArcBlockCacheReader<'_>,     ) -> Result<LookupValue> {-        handle_key_match_generic(&self.mmap, &self.meta, ty, val, key_block_arc, reader)+        handle_key_match_generic(+            &self.mmap,+            &self.meta,+            ty,+            val,+            key_block_arc,+            self.compression,+            reader,+        )     } } @@ -509,6 +539,7 @@ fn get_or_cache_block(     block_index: u16,     cache: &BlockCache,     verified_blocks: &[AtomicU64],+    compression: Compression, ) -> Result<ArcBytes> {     let (uncompressed_length, checksum, block_data) = get_raw_block_slice(mmap, meta, block_index)         .with_context(|| {@@ -533,13 +564,15 @@ fn get_or_cache_block(                 // A cached block may have been evicted, so re-reading still                 // benefits from the bitmap to skip redundant CRC verification.                 verify_checksum_once(meta, block_data, checksum, block_index, verified_blocks)?;-                let block = ArcBytes::from_decompressed(uncompressed_length, block_data)-                    .with_context(|| {-                        format!(-                            "Failed to decompress block {} from {:08}.sst ({} bytes uncompressed)",-                            block_index, meta.sequence_number, uncompressed_length-                        )-                    })?;+                let block =+                    ArcBytes::from_decompressed(compression, uncompressed_length, block_data)+                        .with_context(|| {+                            format!(+                                "Failed to decompress block {} from {:08}.sst ({} bytes \+                                 uncompressed)",+                                block_index, meta.sequence_number, uncompressed_length+                            )+                        })?;                 let _ = guard.insert(block.clone());                 block             }@@ -673,6 +706,7 @@ fn read_block_generic<B: SharedBytes>(     mmap: &B::MmapHandle,     meta: &StaticSortedFileMetaData,     block_index: u16,+    compression: Compression, ) -> Result<B> {     let (uncompressed_length, expected_checksum, block) =         get_raw_block_slice(mmap, meta, block_index).with_context(|| {@@ -689,12 +723,13 @@ fn read_block_generic<B: SharedBytes>(         return Ok(unsafe { B::from_mmap(mmap, block) });     } -    let buffer = B::from_decompressed(uncompressed_length, block).with_context(|| {-        format!(-            "Failed to decompress block {} from {:08}.sst ({} bytes uncompressed)",-            block_index, meta.sequence_number, uncompressed_length-        )-    })?;+    let buffer =+        B::from_decompressed(compression, uncompressed_length, block).with_context(|| {+            format!(+                "Failed to decompress block {} from {:08}.sst ({} bytes uncompressed)",+                block_index, meta.sequence_number, uncompressed_length+            )+        })?;     Ok(buffer) } @@ -705,6 +740,7 @@ fn handle_key_match_generic<B: SharedBytes>(     ty: u8,     val: &[u8],     key_block: &B,+    compression: Compression,     reader: impl ValueBlockCache<B>, ) -> Result<LookupValue<B>> {     Ok(match ty {@@ -713,13 +749,13 @@ fn handle_key_match_generic<B: SharedBytes>(             let size = be::read_u16(&val[2..]) as usize;             let position = be::read_u32(&val[4..]) as usize;             let value = reader-                .get_or_read(mmap, meta, block)?+                .get_or_read(mmap, meta, block, compression)?                 .slice(position..position + size);             LookupValue::Slice { value }         }         KEY_BLOCK_ENTRY_TYPE_MEDIUM => {             let block = be::read_u16(val);-            let value = read_block_generic(mmap, meta, block)?;+            let value = read_block_generic(mmap, meta, block, compression)?;             LookupValue::Slice { value }         }         KEY_BLOCK_ENTRY_TYPE_BLOB => {@@ -763,6 +799,7 @@ pub struct StaticSortedFileIter {     /// value blocks sequentially and don't revisit earlier blocks, so caching     /// just the current one avoids redundant decompression.     value_block_cache: Option<(u16, RcBytes)>,+    compression: Compression, }  enum CurrentKeyBlockKind {@@ -796,10 +833,14 @@ impl Iterator for StaticSortedFileIter { }  impl StaticSortedFileIter {-    /// Opens an SST file for sequential iteration. Uses `MADV_SEQUENTIAL` for-    /// read-ahead and wraps the mmap in `Rc<Mmap>` directly (no `Arc`),-    /// eliminating all atomic refcounting during iteration.-    pub fn open(db_path: &Path, meta: StaticSortedFileMetaData) -> Result<Self> {+    /// Opens an SST file for sequential iteration with the compression algorithm specified by its+    /// meta file. Uses `MADV_SEQUENTIAL` for read-ahead and wraps the mmap in `Rc<Mmap>` directly+    /// (no `Arc`), eliminating all atomic refcounting during iteration.+    pub fn open(+        db_path: &Path,+        meta: StaticSortedFileMetaData,+        compression: Compression,+    ) -> Result<Self> {         let filename = format!("{:08}.sst", meta.sequence_number);         let path = db_path.join(&filename);         let file = File::open(&path)?;@@ -813,13 +854,17 @@ impl StaticSortedFileIter {         #[cfg(unix)]         mmap.advise(memmap2::Advice::Sequential)?;         advise_mmap_for_persistence(&mmap)?;-        Self::new(Rc::new(mmap), meta)+        Self::new(Rc::new(mmap), meta, compression)             .with_context(|| format!("Unable to open static sorted file {filename}"))     } -    fn new(mmap: Rc<Mmap>, meta: StaticSortedFileMetaData) -> Result<Self> {+    fn new(+        mmap: Rc<Mmap>,+        meta: StaticSortedFileMetaData,+        compression: Compression,+    ) -> Result<Self> {         let root_block_index = meta.block_count - 1;-        let block: RcBytes = read_block_generic(&mmap, &meta, root_block_index)?;+        let block: RcBytes = read_block_generic(&mmap, &meta, root_block_index, compression)?;         let block_type = block[0];          // The builder always writes an index block as the root block.@@ -838,7 +883,7 @@ impl StaticSortedFileIter {             - size_of::<u16>())             / INDEX_BLOCK_ENTRY_SIZE; -        let current_key_block = Self::parse_key_block(&mmap, &meta, first_child)?;+        let current_key_block = Self::parse_key_block(&mmap, &meta, first_child, compression)?;         Ok(StaticSortedFileIter {             mmap,             meta,@@ -847,6 +892,7 @@ impl StaticSortedFileIter {             index_pos: 1,             current_key_block,             value_block_cache: None,+            compression,         })     } @@ -855,8 +901,9 @@ impl StaticSortedFileIter {         mmap: &Rc<Mmap>,         meta: &StaticSortedFileMetaData,         block_index: u16,+        compression: Compression,     ) -> Result<CurrentKeyBlock> {-        let block: RcBytes = read_block_generic(mmap, meta, block_index)?;+        let block: RcBytes = read_block_generic(mmap, meta, block_index, compression)?;         let data = &*block;         ensure!(data.len() >= 4, "key block too short");         let block_type = data[0];@@ -959,7 +1006,10 @@ impl StaticSortedFileIter {                         ty,                         val,                         &kb.entries,-                        &mut self.value_block_cache,+                        self.compression,+                        RcBlockCacheReader {+                            cache: &mut self.value_block_cache,+                        },                     )?                     .into()                 };@@ -976,7 +1026,7 @@ impl StaticSortedFileIter {                 let block_index = be::read_u16(&self.index_entries[base..]);                 self.index_pos += 1;                 self.current_key_block =-                    Self::parse_key_block(&self.mmap, &self.meta, block_index)?;+                    Self::parse_key_block(&self.mmap, &self.meta, block_index, self.compression)?;             } else {                 return Ok(None);             }
turbopack/crates/turbo-persistence/src/static_sorted_file_builder.rs41 + / 11
@@ -10,7 +10,8 @@ use byteorder::{BE, ByteOrder, WriteBytesExt}; use fs_err::File;  use crate::{-    compression::{checksum_block, compress_into_buffer},+    Compression,+    compression::{Compressor, checksum_block},     constants::{MAX_INLINE_VALUE_SIZE, MAX_SMALL_VALUE_SIZE, MIN_SMALL_VALUE_BLOCK_SIZE},     meta_file::MetaEntryFlags,     static_sorted_file::{@@ -309,9 +310,10 @@ pub fn write_static_stored_file<E: Entry>(     entries: &[E],     file: &Path,     flags: MetaEntryFlags,+    compression: Compression, ) -> Result<(StaticSortedFileBuilderMeta<'static>, File)> {     debug_assert!(entries.iter().map(|e| e.key_hash()).is_sorted());-    let mut writer = StreamingSstWriter::new(file, flags, entries.len() as u64)?;+    let mut writer = StreamingSstWriter::new(file, flags, entries.len() as u64, compression)?;     for entry in entries {         writer.add(entry)?;     }@@ -363,9 +365,10 @@ fn write_block_to_file(     block_offsets: &mut Vec<u32>,     block: &[u8],     try_compress: bool,+    compressor: &mut Compressor, ) -> Result<u16> {     let (uncompressed_size, data_to_write): (u32, &[u8]) = if try_compress {-        compress_into_buffer(block, compress_buffer)?;+        compressor.compress_into_buffer(block, compress_buffer)?;         // Same threshold as LevelDB/RocksDB: require at least 12.5% savings.         if compress_buffer.len() < block.len() - (block.len() / 8) {             (block.len().try_into().unwrap(), compress_buffer.as_slice())@@ -505,6 +508,7 @@ pub struct StreamingSstWriter<E: Entry> {     file: Option<BufWriter<File>>,     compress_buffer: Vec<u8>,     block_offsets: Vec<u32>,+    compressor: Compressor,      /// Pending key entries waiting to be flushed as key blocks.     ///@@ -578,8 +582,14 @@ impl<E: Entry> StreamingSstWriter<E> {     /// Creates a new streaming SST writer.     ///     /// `max_entry_count` is used to pre-allocate buffers and estimate block counts.-    pub fn new(file: &Path, flags: MetaEntryFlags, max_entry_count: u64) -> Result<Self> {+    pub fn new(+        file: &Path,+        flags: MetaEntryFlags,+        max_entry_count: u64,+        compression: Compression,+    ) -> Result<Self> {         let file = BufWriter::new(File::create(file)?);+        let compressor = Compressor::new(compression)?;          // Estimate number of key blocks based on max entry count.         // Each key block holds up to MAX_KEY_BLOCK_ENTRIES entries.@@ -598,6 +608,7 @@ impl<E: Entry> StreamingSstWriter<E> {             file: Some(file),             compress_buffer: Vec::with_capacity(MIN_SMALL_VALUE_BLOCK_SIZE + MAX_SMALL_VALUE_SIZE),             block_offsets: Vec::with_capacity(estimated_total_blocks),+            compressor,             pending_keys: VecDeque::with_capacity(entries_per_value_block),             first_pending_small_index: 0,             #[cfg(debug_assertions)]@@ -681,6 +692,7 @@ impl<E: Entry> StreamingSstWriter<E> {                     &mut self.block_offsets,                     value,                     true,+                    &mut self.compressor,                 )                 .context("Failed to write value block")?;                 ValueRef::Medium { block_index }@@ -838,6 +850,7 @@ impl<E: Entry> StreamingSstWriter<E> {             &mut self.block_offsets,             &self.pending_small_value_block,             true,+            &mut self.compressor,         )         .context("Failed to write small value block")?; @@ -930,6 +943,7 @@ impl<E: Entry> StreamingSstWriter<E> {             &mut self.block_offsets,             &self.key_buffer,             try_compress,+            &mut self.compressor,         )         .context("Failed to write key block")?;         self.key_block_boundaries.push((first_hash, block_index));@@ -1432,6 +1446,7 @@ mod tests {                 sequence_number: seq,                 block_count: meta.block_count,             },+            Compression::Lz4,         )     } @@ -1443,7 +1458,8 @@ mod tests {         flags: MetaEntryFlags,     ) -> Result<StaticSortedFileBuilderMeta<'static>> {         let sst_path = dir.join(format!("{seq:08}.sst"));-        let mut writer = StreamingSstWriter::new(&sst_path, flags, entries.len() as u64)?;+        let mut writer =+            StreamingSstWriter::new(&sst_path, flags, entries.len() as u64, Compression::Lz4)?;         for entry in entries {             writer.add(entry)?;         }@@ -1679,7 +1695,8 @@ mod tests {         let dir = tempfile::tempdir().unwrap();         let sst_path = dir.path().join("test.sst");         let mut writer =-            StreamingSstWriter::new(&sst_path, MetaEntryFlags::default(), 100).unwrap();+            StreamingSstWriter::new(&sst_path, MetaEntryFlags::default(), 100, Compression::Lz4)+                .unwrap();          let max_entries = 50;         for i in 0..max_entries {@@ -1705,7 +1722,8 @@ mod tests {         let dir = tempfile::tempdir().unwrap();         let sst_path = dir.path().join("test.sst");         let mut writer =-            StreamingSstWriter::new(&sst_path, MetaEntryFlags::default(), 100).unwrap();+            StreamingSstWriter::new(&sst_path, MetaEntryFlags::default(), 100, Compression::Lz4)+                .unwrap();          let value = vec![0u8; 1000];         for i in 0..10 {@@ -1741,15 +1759,20 @@ mod tests {          // Write via convenience function         let batch_path = dir.path().join("00000001.sst");-        let (meta1, _) =-            write_static_stored_file(&entries, &batch_path, MetaEntryFlags::default())?;+        let (meta1, _) = write_static_stored_file(+            &entries,+            &batch_path,+            MetaEntryFlags::default(),+            Compression::Lz4,+        )?;          // Write via streaming API         let streaming_path = dir.path().join("00000002.sst");         let mut writer = StreamingSstWriter::new(             &streaming_path,             MetaEntryFlags::default(),             entries.len() as u64,+            Compression::Lz4,         )?;         for entry in &entries {             writer.add(entry)?;@@ -1769,13 +1792,15 @@ mod tests {                 sequence_number: 1,                 block_count: meta1.block_count,             },+            Compression::Lz4,         )?;         let sst2 = StaticSortedFile::open(             dir.path(),             StaticSortedFileMetaData {                 sequence_number: 2,                 block_count: meta2.block_count,             },+            Compression::Lz4,         )?;         let kc = make_cache();         let vc = make_cache();@@ -1830,8 +1855,13 @@ mod tests {     fn close_empty_writer_panics() {         let dir = tempfile::tempdir().unwrap();         let sst_path = dir.path().join("empty.sst");-        let writer =-            StreamingSstWriter::<TestEntry>::new(&sst_path, MetaEntryFlags::default(), 0).unwrap();+        let writer = StreamingSstWriter::<TestEntry>::new(+            &sst_path,+            MetaEntryFlags::default(),+            0,+            Compression::Lz4,+        )+        .unwrap();         writer.close().unwrap();     } 
turbopack/crates/turbo-persistence/src/tests.rs52 + / 9
@@ -4,7 +4,7 @@ use anyhow::Result; use rayon::iter::{IntoParallelIterator, ParallelIterator};  use crate::{-    DbConfig, FamilyConfig, FamilyKind,+    Compression, DbConfig, FamilyConfig, FamilyKind,     constants::{MAX_INLINE_VALUE_SIZE, MAX_MEDIUM_VALUE_SIZE, MAX_SMALL_VALUE_SIZE},     db::{CompactConfig, TurboPersistence, read_current_version},     lookup_entry::IterValue,@@ -1055,8 +1055,11 @@ fn batch_get_different_sizes() -> Result<()> {     let tempdir = tempfile::tempdir()?;     let path = tempdir.path(); -    let db = TurboPersistence::<_, 16>::open_with_parallel_scheduler(+    let mut config = DbConfig::default();+    config.family_configs[0].compression = Compression::Zstd3;+    let db = TurboPersistence::<_, 16>::open_with_config_and_parallel_scheduler(         path.to_path_buf(),+        config,         RayonParallelScheduler,     )?; @@ -1109,16 +1112,22 @@ fn batch_get_across_families() -> Result<()> {     let tempdir = tempfile::tempdir()?;     let path = tempdir.path(); -    let db = TurboPersistence::<_, 16>::open_with_parallel_scheduler(+    let mut config = DbConfig::default();+    // set zstd on an arbitrary family, lz4 is used by default+    config.family_configs[2].compression = Compression::Zstd3;+    let db = TurboPersistence::<_, 16>::open_with_config_and_parallel_scheduler(         path.to_path_buf(),+        config.clone(),         RayonParallelScheduler,     )?; -    // Write to multiple families+    // Write compressible values to multiple families so every configured codec is exercised.     let batch = db.write_batch()?;     for family in 0..4u32 {         for i in 0..20u8 {-            batch.put(family, vec![i], vec![family as u8, i].into())?;+            let mut value = vec![family as u8; 1024];+            value[0] = i;+            batch.put(family, vec![i], value.into())?;         }     }     db.commit_write_batch(batch)?;@@ -1132,7 +1141,13 @@ fn batch_get_across_families() -> Result<()> {         for (i, result) in results.iter().enumerate() {             assert_eq!(                 result.as_deref(),-                Some(&vec![family as u8, i as u8][..]),+                Some(+                    &{+                        let mut value = vec![family as u8; 1024];+                        value[0] = i as u8;+                        value+                    }[..]+                ),                 "Failed at family {family}, index {i}"             );         }@@ -1147,6 +1162,29 @@ fn batch_get_across_families() -> Result<()> {     assert_ne!(results_f0[0].as_deref(), results_f1[0].as_deref());      db.shutdown()?;+    drop(db);++    // Reopen with the same family configuration recorded in the meta files.+    let db = TurboPersistence::<_, 16>::open_with_config_and_parallel_scheduler(+        path.to_path_buf(),+        config,+        RayonParallelScheduler,+    )?;+    let value = db.get(2, &vec![7u8])?.expect("zstd family value exists");+    assert_eq!(value[0], 7);+    assert!(value[1..].iter().all(|byte| *byte == 2));+    db.shutdown()?;+    drop(db);++    // Reopening with the wrong codec must fail while validating the meta files.+    assert!(+        TurboPersistence::<RayonParallelScheduler, 16>::open_with_config_and_parallel_scheduler(+            path.to_path_buf(),+            DbConfig::default(),+            RayonParallelScheduler,+        )+        .is_err()+    );     Ok(()) } @@ -1155,8 +1193,11 @@ fn batch_get_after_compaction() -> Result<()> {     let tempdir = tempfile::tempdir()?;     let path = tempdir.path(); -    let db = TurboPersistence::<_, 16>::open_with_parallel_scheduler(+    let mut config = DbConfig::default();+    config.family_configs[0].compression = Compression::Zstd3;+    let db = TurboPersistence::<_, 16>::open_with_config_and_parallel_scheduler(         path.to_path_buf(),+        config,         RayonParallelScheduler,     )?; @@ -1174,7 +1215,7 @@ fn batch_get_after_compaction() -> Result<()> {     let keys_to_fetch: Vec<Vec<u8>> = (0..100u8).map(|i| vec![i]).collect();     let results_before = db.batch_get(0, &keys_to_fetch)?; -    // Compact database+    // Compact database using zstd to cover recompression with a non-default codec.     db.full_compact()?;      // Fetch after compaction@@ -1525,6 +1566,7 @@ fn multi_value_config() -> DbConfig<1> {     config.family_configs[0] = FamilyConfig {         name: "test",         kind: FamilyKind::MultiValue,+        compression: Compression::Lz4,     };     config }@@ -2104,6 +2146,7 @@ fn compaction_deletes_blob_multi_value_tombstone() -> Result<()> {         family_configs: [FamilyConfig {             name: "test",             kind: FamilyKind::MultiValue,+            compression: Compression::Lz4,         }],     }; @@ -2468,7 +2511,7 @@ fn count_tombstones(                 sequence_number: entry.sequence_number,                 block_count: entry.block_count,             };-            for item in StaticSortedFileIter::open(path, sst)? {+            for item in StaticSortedFileIter::open(path, sst, Compression::Lz4)? {                 if matches!(                     item?.value,                     IterValue::KeyDeleted | IterValue::KeyValueDeleted { .. }
turbopack/crates/turbo-persistence/src/write_batch.rs26 + / 16
@@ -18,7 +18,7 @@ use crate::{     FamilyConfig, FamilyKind, ValueBuffer,     collector::Collector,     collector_entry::CollectorEntry,-    compression::{checksum_block, compress_into_buffer},+    compression::{Compressor, checksum_block},     constants::{MAX_INLINE_VALUE_SIZE, MAX_MEDIUM_VALUE_SIZE, THREAD_LOCAL_SIZE_SHIFT},     db::WriteOperationGuard,     key::StoreKey,@@ -79,7 +79,7 @@ pub struct WriteBatch<'db, K: StoreKey + Send, S: ParallelScheduler, const FAMIL     parallel_scheduler: S,     /// The database path     db_path: PathBuf,-    /// Per-family configuration (kind: SingleValue/MultiValue).+    /// Per-family storage configuration.     #[cfg_attr(not(feature = "verify_sst_content"), allow(dead_code))]     family_configs: [FamilyConfig; FAMILIES],     /// The current sequence number counter. Increased for every new SST file or blob file.@@ -238,7 +238,7 @@ impl<'db, K: StoreKey + Send + Sync, S: ParallelScheduler, const FAMILIES: usize         if value.len() <= MAX_MEDIUM_VALUE_SIZE {             collector.put(key, value);         } else {-            let blob = self.create_blob(&value)?;+            let blob = self.create_blob(family, &value)?;             collector.put_blob(key, blob.seq);             state.new_blob_files.push(blob);         }@@ -318,14 +318,13 @@ impl<'db, K: StoreKey + Send + Sync, S: ParallelScheduler, const FAMILIES: usize             })?;          // Now we flush the global collector(s).-        let mut collector_state = self.collectors[usize_from_u32(family)].lock();+        let family_usize = usize_from_u32(family);+        let mut collector_state = self.collectors[family_usize].lock();+        let family_config = self.family_configs[family_usize];         match &mut *collector_state {             GlobalCollectorState::Unsharded(collector) => {                 if !collector.is_empty() {-                    let sst = self.create_sst_file(-                        family,-                        collector.sorted(self.family_configs[usize_from_u32(family)].kind),-                    )?;+                    let sst = self.create_sst_file(family, collector.sorted(family_config.kind))?;                     collector.clear();                     self.new_sst_files.lock().push(sst);                 }@@ -340,10 +339,8 @@ impl<'db, K: StoreKey + Send + Sync, S: ParallelScheduler, const FAMILIES: usize                 self.parallel_scheduler                     .try_parallel_for_each_mut(&mut shards, |collector| {                         if !collector.is_empty() {-                            let sst = self.create_sst_file(-                                family,-                                collector.sorted(self.family_configs[usize_from_u32(family)].kind),-                            )?;+                            let sst =+                                self.create_sst_file(family, collector.sorted(family_config.kind))?;                             collector.clear();                             self.new_sst_files.lock().push(sst);                             collector.drop_contents();@@ -457,7 +454,10 @@ impl<'db, K: StoreKey + Send + Sync, S: ParallelScheduler, const FAMILIES: usize                 |(family, sst_files)| {                     let family = family as u32;                     let mut entries = 0;-                    let mut builder = MetaFileBuilder::new(family);+                    let mut builder = MetaFileBuilder::new(+                        family,+                        self.family_configs[usize_from_u32(family)].compression,+                    );                     for (seq, sst) in sst_files {                         entries += sst.entries;                         builder.add(seq, sst);@@ -484,10 +484,12 @@ impl<'db, K: StoreKey + Send + Sync, S: ParallelScheduler, const FAMILIES: usize      /// Creates a new blob file with the given value.     #[tracing::instrument(level = "trace", skip(self, value), fields(value_len = value.len()))]-    fn create_blob(&self, value: &[u8]) -> Result<NewFile> {+    fn create_blob(&self, family: u32, value: &[u8]) -> Result<NewFile> {         let seq = self.current_sequence_number.fetch_add(1, Ordering::SeqCst) + 1;         let mut compressed = Vec::new();-        compress_into_buffer(value, &mut compressed)+        let compression = self.family_configs[usize_from_u32(family)].compression;+        Compressor::new(compression)?+            .compress_into_buffer(value, &mut compressed)             .context("Compression of value for blob file failed")?;          let mut buffer = Vec::with_capacity(8 + compressed.len());@@ -516,7 +518,14 @@ impl<'db, K: StoreKey + Send + Sync, S: ParallelScheduler, const FAMILIES: usize         let path = self.db_path.join(format!("{seq:08}.sst"));         let (meta, file) = self             .parallel_scheduler-            .block_in_place(|| write_static_stored_file(entries, &path, MetaEntryFlags::FRESH))+            .block_in_place(|| {+                write_static_stored_file(+                    entries,+                    &path,+                    MetaEntryFlags::FRESH,+                    self.family_configs[usize_from_u32(family)].compression,+                )+            })             .with_context(|| format!("Unable to write SST file {seq:08}.sst"))?;          #[cfg(feature = "verify_sst_content")]@@ -540,6 +549,7 @@ impl<'db, K: StoreKey + Send + Sync, S: ParallelScheduler, const FAMILIES: usize                     sequence_number: seq,                     block_count: meta.block_count,                 },+                self.family_configs[usize_from_u32(family)].compression,             )?;             let cache2 = BlockCache::with(                 10,
turbopack/crates/turbo-tasks-backend/src/database/key_value_database.rs9 + / 2
@@ -1,4 +1,4 @@-use turbo_persistence::{FamilyConfig, FamilyKind};+use turbo_persistence::{Compression, FamilyConfig, FamilyKind};  #[derive(Debug, Clone, Copy)] pub enum KeySpace {@@ -35,14 +35,21 @@ impl KeySpace {     /// Returns the persistence configuration for this keyspace.     pub const fn family_config(&self) -> FamilyConfig {         match self {-            KeySpace::Infra | KeySpace::TaskMeta | KeySpace::TaskData => FamilyConfig {+            KeySpace::Infra | KeySpace::TaskMeta => FamilyConfig {                 name: self.name(),                 kind: FamilyKind::SingleValue,+                compression: Compression::Lz4,+            },+            KeySpace::TaskData => FamilyConfig {+                name: self.name(),+                kind: FamilyKind::SingleValue,+                compression: Compression::Zstd3,             },             KeySpace::TaskCache => FamilyConfig {                 name: self.name(),                 // TaskCache uses hash-based lookups with potential collisions.                 kind: FamilyKind::MultiValue,+                compression: Compression::Lz4,             },         }     }