Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 16 additions & 7 deletions turbopack/crates/turbo-persistence/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,12 +122,14 @@ The hashes are sorted.

A Key block contains n keys, which specify n key value pairs.

The block type determines whether the key hash is stored per entry:
The block type determines whether the key hash is stored per entry, and with it the order the
entries are stored in:

- Block type 1 (with hash): Full 8-byte hash stored per entry
- Block type 2 (no hash): No hash stored (for keys ≤ 32 bytes)
- Block type 1 (with hash): Full 8-byte hash stored per entry. Entries are sorted by
`(key hash, key)`.
- Block type 2 (no hash): No hash stored (for keys ≤ 32 bytes). Entries are sorted by **key**.

During lookup, if block type is 2, the full hash is recomputed from the key data.
See [Entry ordering](#entry-ordering) for why the two differ.

Depending on the `type` field entry has a different format:

Expand Down Expand Up @@ -167,7 +169,13 @@ Depending on the `type` field entry has a different format:
Both ranged kinds are open-ended, so a decoder must test the key-value tombstone range **before**
the inline range.

The entries are sorted by key hash and key.
##### Entry ordering

Logically keys are ordered by hash (this is how we chose file and block assignments). However, within a single key block, however, the order is chosen per block type:

- **With hash (types 1 and 3):** sorted by `(key hash, key)`.
- **No hash (types 2 and 4):** sorted by **key** alone.


##### Key-value tombstones

Expand Down Expand Up @@ -236,10 +244,11 @@ Reading start from the current sequence number and goes downwards.
- Check AMQF from SST file for key existence -> if not continue
- let block = 0
- loop
- Index Block: find key range that contains the key by binary search
- Index Block: find key range that contains the key by binary search using the **hash** of the key
- found -> set block, continue
- not found -> break
- Key Block: find key by binary search
- Key Block: find key by binary search, comparing `(hash, key)` in blocks that store a hash and
the key alone in blocks that do not (see [Entry ordering](#entry-ordering))
- found -> lookup value from value block, return
- read value as inline, or by using the block index in the key to find the value elsewhere in the file.
- not found -> break
Expand Down
4 changes: 2 additions & 2 deletions turbopack/crates/turbo-persistence/benches/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1161,8 +1161,8 @@ impl Entry for BenchEntry {
8
}

fn write_key_to(&self, buf: &mut Vec<u8>) {
buf.extend_from_slice(&self.key);
fn key_bytes(&self) -> &[u8] {
&self.key
}

fn value(&self) -> EntryValue<'_> {
Expand Down
4 changes: 2 additions & 2 deletions turbopack/crates/turbo-persistence/src/collector_entry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,8 +145,8 @@ impl<K: StoreKey> Entry for CollectorEntry<K> {
self.key.data.len()
}

fn write_key_to(&self, buf: &mut Vec<u8>) {
self.key.data.write_to(buf);
fn key_bytes(&self) -> &[u8] {
self.key.data.as_slice()
}

fn value(&self) -> EntryValue<'_> {
Expand Down
79 changes: 46 additions & 33 deletions turbopack/crates/turbo-persistence/src/key.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,105 +118,118 @@ impl<T: KeyBase> KeyBase for &'_ T {
/// comparison with a byte slice (total order).
pub trait QueryKey: KeyBase {
fn cmp(&self, key: &[u8]) -> std::cmp::Ordering;
fn eq(&self, key: &[u8]) -> bool;
}

impl QueryKey for &'_ [u8] {
fn cmp(&self, key: &[u8]) -> std::cmp::Ordering {
Ord::cmp(self, &key)
}

fn eq(&self, key: &[u8]) -> bool {
PartialEq::eq(*self, key)
}
}

impl<const N: usize> QueryKey for [u8; N] {
fn cmp(&self, key: &[u8]) -> std::cmp::Ordering {
Ord::cmp(&self[..], key)
}
fn eq(&self, key: &[u8]) -> bool {
PartialEq::eq(self, key)
}
}

impl QueryKey for Vec<u8> {
fn cmp(&self, key: &[u8]) -> std::cmp::Ordering {
Ord::cmp(&**self, key)
}
fn eq(&self, key: &[u8]) -> bool {
PartialEq::eq(self.as_slice(), key)
}
}

impl QueryKey for Box<[u8]> {
fn cmp(&self, key: &[u8]) -> std::cmp::Ordering {
Ord::cmp(&**self, key)
}
fn eq(&self, key: &[u8]) -> bool {
PartialEq::eq(&**self, key)
}
}

impl QueryKey for u8 {
fn cmp(&self, key: &[u8]) -> std::cmp::Ordering {
Ord::cmp(&[*self][..], key)
}
fn eq(&self, key: &[u8]) -> bool {
PartialEq::eq(&[*self][..], key)
}
}

impl<A: QueryKey, B: QueryKey> QueryKey for (A, B) {
fn cmp(&self, mut key: &[u8]) -> std::cmp::Ordering {
fn cmp(&self, key: &[u8]) -> std::cmp::Ordering {
let (a, b) = self;
let len = a.len();
let key_len = key.len();
let key_part = &key[..min(key_len, len)];
match a.cmp(key_part) {
std::cmp::Ordering::Equal => {
key = &key[len..];
b.cmp(key)
}
ord => ord,
}
let (key_part, value_part) = key.split_at(min(key_len, len));
a.cmp(key_part).then_with(|| b.cmp(value_part))
}
fn eq(&self, key: &[u8]) -> bool {
let (a, b) = self;
let len = a.len();
let key_len = key.len();
let (key_part, value_part) = &key.split_at(min(key_len, len));
a.eq(key_part) && b.eq(value_part)
}
}

impl<T: QueryKey> QueryKey for &'_ T {
fn cmp(&self, key: &[u8]) -> std::cmp::Ordering {
(*self).cmp(key)
}
fn eq(&self, key: &[u8]) -> bool {
(*self).eq(key)
}
}

/// A trait for keys that can be stored in the database. They need to allow hashing and comparison.
pub trait StoreKey: KeyBase + Ord {
fn write_to(&self, buf: &mut Vec<u8>);
/// The key's bytes.
fn as_slice(&self) -> &[u8];

fn write_to(&self, buf: &mut Vec<u8>) {
buf.extend_from_slice(self.as_slice());
}
}

impl<const N: usize> StoreKey for [u8; N] {
fn write_to(&self, buf: &mut Vec<u8>) {
buf.extend_from_slice(&self[..]);
fn as_slice(&self) -> &[u8] {
&self[..]
}
}

impl StoreKey for Vec<u8> {
fn write_to(&self, buf: &mut Vec<u8>) {
buf.extend_from_slice(self);
fn as_slice(&self) -> &[u8] {
self
}
}

impl StoreKey for Box<[u8]> {
fn write_to(&self, buf: &mut Vec<u8>) {
buf.extend_from_slice(self);
fn as_slice(&self) -> &[u8] {
self
}
}

impl StoreKey for &'_ [u8] {
fn write_to(&self, buf: &mut Vec<u8>) {
buf.extend_from_slice(self);
}
}

impl StoreKey for u8 {
fn write_to(&self, buf: &mut Vec<u8>) {
buf.push(*self);
}
}

impl<A: StoreKey, B: StoreKey> StoreKey for (A, B) {
fn write_to(&self, buf: &mut Vec<u8>) {
self.0.write_to(buf);
self.1.write_to(buf);
fn as_slice(&self) -> &[u8] {
self
}
}

impl<T: StoreKey> StoreKey for &'_ T {
fn write_to(&self, buf: &mut Vec<u8>) {
(*self).write_to(buf);
fn as_slice(&self) -> &[u8] {
(*self).as_slice()
}
}

Expand Down
4 changes: 2 additions & 2 deletions turbopack/crates/turbo-persistence/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,8 @@ pub use key::{KeyBase, QueryKey, StoreKey, hash_key};
pub use meta_file::MetaEntryFlags;
pub use parallel_scheduler::{ParallelScheduler, SerialScheduler};
pub use static_sorted_file::{
BlockCache, BlockCacheLifecycle, BlockWeighter, SstLookupResult, StaticSortedFile,
StaticSortedFileMetaData,
BlockCache, BlockCacheLifecycle, BlockWeighter, KeyBlockLayout, SstLookupResult,
StaticSortedFile, StaticSortedFileMetaData,
};
pub use static_sorted_file_builder::{
BLOCK_HEADER_SIZE, Entry, EntryValue, StreamingSstWriter, write_static_stored_file,
Expand Down
4 changes: 2 additions & 2 deletions turbopack/crates/turbo-persistence/src/lookup_entry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,8 @@ impl Entry for LookupEntry {
self.key.len()
}

fn write_key_to(&self, buf: &mut Vec<u8>) {
buf.extend_from_slice(&self.key);
fn key_bytes(&self) -> &[u8] {
&self.key
}

fn value(&self) -> EntryValue<'_> {
Expand Down
Loading
Loading