vercel/next.js · #97808

Upgrade Turbopack to hashbrown 0.15

lukesandberg · merged Aug 27, 202611 files · 312 + / 347
Cargo.lock39 + / 18
@@ -405,7 +405,7 @@ version = "0.1.0" dependencies = [  "bincode 2.0.1",  "codspeed-criterion-compat",- "hashbrown 0.14.5",+ "hashbrown 0.15.4",  "rustc-hash 2.1.1",  "serde",  "shrink-to-fit",@@ -2148,6 +2148,20 @@ dependencies = [  "parking_lot_core", ] +[[package]]+name = "dashmap"+version = "7.0.0-rc2"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "e4a1e35a65fe0538a60167f0ada6e195ad5d477f6ddae273943596d4a1a5730b"+dependencies = [+ "cfg-if",+ "crossbeam-utils",+ "equivalent",+ "hashbrown 0.15.4",+ "lock_api",+ "parking_lot_core",+]+ [[package]] name = "data-encoding" version = "2.4.0"@@ -2554,7 +2568,7 @@ checksum = "d4029edd3e734da6fe05b6cd7bd2960760a616bd2ddd0d59a0124746d6272af0" dependencies = [  "cfg-if",  "libc",- "redox_syscall",+ "redox_syscall 0.3.5",  "windows-sys 0.48.0", ] @@ -3172,7 +3186,6 @@ checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [  "ahash 0.8.12",  "allocator-api2",- "serde", ]  [[package]]@@ -4416,11 +4429,10 @@ checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5"  [[package]] name = "lock_api"-version = "0.4.10"+version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index"-checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16"+checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [- "autocfg",  "scopeguard",  "serde", ]@@ -5591,15 +5603,15 @@ dependencies = [  [[package]] name = "parking_lot_core"-version = "0.9.8"+version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index"-checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447"+checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [  "cfg-if",  "libc",- "redox_syscall",+ "redox_syscall 0.5.18",  "smallvec",- "windows-targets 0.48.5",+ "windows-link 0.2.1", ]  [[package]]@@ -6508,6 +6520,15 @@ dependencies = [  "bitflags 1.3.2", ] +[[package]]+name = "redox_syscall"+version = "0.5.18"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"+dependencies = [+ "bitflags 2.9.1",+]+ [[package]] name = "ref-cast" version = "1.0.24"@@ -10007,7 +10028,7 @@ dependencies = [  "byteorder",  "codspeed-criterion-compat",  "crc32fast",- "dashmap 6.1.0",+ "dashmap 7.0.0-rc2",  "either",  "fs-err",  "jiff",@@ -10079,7 +10100,7 @@ dependencies = [  "bincode 2.0.1",  "codspeed-criterion-compat",  "concurrent-queue",- "dashmap 6.1.0",+ "dashmap 7.0.0-rc2",  "either",  "erased-serde",  "event-listener",@@ -10122,10 +10143,10 @@ dependencies = [  "bitfield",  "codspeed-criterion-compat",  "crossbeam-utils",- "dashmap 6.1.0",+ "dashmap 7.0.0-rc2",  "fs-err",  "futures",- "hashbrown 0.14.5",+ "hashbrown 0.15.4",  "indexmap 2.14.0",  "indoc",  "jiff",@@ -10228,7 +10249,7 @@ dependencies = [  "bytes",  "codspeed-criterion-compat",  "concurrent-queue",- "dashmap 6.1.0",+ "dashmap 7.0.0-rc2",  "fs-err",  "futures",  "include_dir",@@ -10654,7 +10675,7 @@ dependencies = [  "bumpalo",  "bytes-str",  "codspeed-criterion-compat",- "dashmap 6.1.0",+ "dashmap 7.0.0-rc2",  "data-encoding",  "either",  "forked_react_compiler",@@ -10854,7 +10875,7 @@ dependencies = [  "bincode 2.0.1",  "bytes",  "const_format",- "dashmap 6.1.0",+ "dashmap 7.0.0-rc2",  "either",  "futures",  "futures-retry",@@ -11012,7 +11033,7 @@ dependencies = [  "anyhow",  "either",  "flate2",- "hashbrown 0.14.5",+ "hashbrown 0.15.4",  "indexmap 2.14.0",  "itertools 0.10.5",  "postcard",
Cargo.toml2 + / 2
@@ -267,7 +267,7 @@ criterion = { package = "codspeed-criterion-compat", version = "4.3.0" } ctor = "1.0.7" crossbeam-channel = "0.5.8" crossbeam-utils = "0.8"-dashmap = "6.1.0"+dashmap = "7.0.0-rc2" data-encoding = "2.3.3" dhat = { version = "0.3.2" } dunce = "1.0.3"@@ -279,7 +279,7 @@ fs-err = "3.1.1" futures = "0.3.31" futures-retry = "0.6.0" futures-util = "0.3.31"-hashbrown = "0.14.5"+hashbrown = "0.15.4" image = { version = "0.25.8", default-features = false } indexmap = "2.14.0" indoc = "2.0.0"
turbopack/crates/turbo-tasks-auto-hash-map/Cargo.toml1 + / 1
@@ -10,7 +10,7 @@ workspace = true  [dependencies] bincode = { workspace = true }-hashbrown = { workspace = true, features = ["serde"]}+hashbrown = { workspace = true, features = ["raw-entry", "serde"]} rustc-hash = { workspace = true } serde = { workspace = true, features = ["derive"] } shrink-to-fit = { workspace = true, features = ["hashbrown"] }
turbopack/crates/turbo-tasks-backend/Cargo.toml1 + / 1
@@ -43,7 +43,7 @@ bitfield = { workspace = true } crossbeam-utils = { workspace = true } dashmap = { workspace = true, features = ["raw-api"]} fs-err = { workspace = true }-hashbrown = { workspace = true, features = ["raw"] }+hashbrown = { workspace = true } indexmap = { workspace = true } jiff = "0.2.10" lzzzz = { workspace = true, optional = true }
turbopack/crates/turbo-tasks-backend/src/backend/mod.rs80 + / 67
@@ -22,6 +22,7 @@ use std::{  use anyhow::{Context, Result, bail}; use auto_hash_map::{AutoMap, AutoSet};+use hashbrown::hash_table::Entry; use indexmap::IndexSet; use parking_lot::Mutex; use rustc_hash::{FxHashMap, FxHashSet, FxHasher};@@ -80,7 +81,7 @@ use crate::{     error::TaskError,     kv_backing_storage::TurboBackingStorage,     utils::{-        dash_map_raw_entry::{RawEntry, get_shard, raw_entry_in_shard, raw_get_in_shard},+        dash_map_entry::{get_in_shard, get_shard, with_entry_in_shard},         shard_amount::compute_shard_amount,         stopwatch::Stopwatch,     },@@ -1552,7 +1553,7 @@ impl TurboTasksBackend {         // Use a read lock rather than a write lock to avoid contention. connect_child         // may re-enter task_cache with a write lock, so we must not hold a write lock here.         if let Some(task_id) =-            raw_get_in_shard(shard, hash, |k| k.eq_components(native_fn, this, arg_ref))+            get_in_shard(shard, hash, |k| k.eq_components(native_fn, this, arg_ref))         {             self.track_cache_hit_by_fn(native_fn);             operation::ConnectChildOperation::run(parent_task, task_id, ctx);@@ -1569,76 +1570,88 @@ impl TurboTasksBackend {             self.track_cache_hit_by_fn(native_fn);             // Step 3a: Insert into in-memory cache using the pre-located shard.             // Use the existing Arc from storage to avoid a duplicate allocation.-            match raw_entry_in_shard(shard, self.storage.task_cache.hasher(), hash, |k| {-                k.eq_components(native_fn, this, arg_ref)-            }) {-                RawEntry::Occupied(_) => {}-                RawEntry::Vacant(e) => {-                    e.insert(stored_type, task_id);-                }-            };+            with_entry_in_shard(+                shard,+                self.storage.task_cache.hasher(),+                hash,+                arg,+                |k, arg| k.eq_components(native_fn, this, arg.as_ref()),+                |entry, _arg| {+                    if let Entry::Vacant(entry) = entry {+                        entry.insert((stored_type, task_id));+                    }+                },+            );             task_id         } else {-            match raw_entry_in_shard(shard, self.storage.task_cache.hasher(), hash, |k| {-                k.eq_components(native_fn, this, arg_ref)-            }) {-                RawEntry::Occupied(e) => {-                    // Another thread beat us to creating this task — use their task_id.-                    // They will handle logging the new task as modified.-                    let task_id = *e.get();-                    drop(e);-                    self.track_cache_hit_by_fn(native_fn);-                    task_id-                }-                RawEntry::Vacant(e) => {-                    // Only now do we force the allocation.-                    // NOTE: if our caller had to perform resolution, then this will have already-                    // been boxed and take_box just takes it.-                    let task_type = CachedTaskTypeArc::new(CachedTaskType {-                        native_fn,-                        this,-                        arg: arg.take_box(),-                    });-                    let task_id = if transient {-                        self.transient_task_id_factory.get()-                    } else {-                        self.persisted_task_id_factory.get()-                    };-                    // Initialize storage BEFORE making task_id visible in the cache.-                    // This ensures any thread that reads task_id from the cache sees-                    // the storage entry already initialized (restored flags set).-                    self.storage-                        .initialize_new_task(task_id, Some(task_type.clone()));-                    // insert() consumes e, releasing the shard write lock.-                    e.insert(task_type, task_id);-                    self.track_cache_miss_by_fn(native_fn);-                    // Update the aggregation number before connecting the child-                    // We don't need this on any of the task recovery paths above because the-                    // aggregation number will already be set.-                    if is_root {-                        AggregationUpdateQueue::run(-                            AggregationUpdateJob::UpdateAggregationNumber {-                                task_id,-                                base_aggregation_number: u32::MAX,-                                distance: None,-                            },-                            &mut ctx,-                        );-                    } else if native_fn.is_session_dependent && self.should_track_dependencies() {-                        const SESSION_DEPENDENT_AGGREGATION_NUMBER: u32 = u32::MAX >> 2;-                        AggregationUpdateQueue::run(-                            AggregationUpdateJob::UpdateAggregationNumber {-                                task_id,-                                base_aggregation_number: SESSION_DEPENDENT_AGGREGATION_NUMBER,-                                distance: None,-                            },-                            &mut ctx,-                        );-                    };+            let (task_id, created) = with_entry_in_shard(+                shard,+                self.storage.task_cache.hasher(),+                hash,+                arg,+                |k, arg| k.eq_components(native_fn, this, arg.as_ref()),+                |entry, arg| match entry {+                    Entry::Occupied(entry) => {+                        // Another thread beat us to creating this task — use their task_id.+                        // They will handle logging the new task as modified.+                        (entry.get().1, false)+                    }+                    Entry::Vacant(entry) => {+                        // Only now do we force the allocation.+                        // NOTE: if our caller had to perform resolution, then this will have+                        // already been boxed and take_box just takes it.+                        let task_type = CachedTaskTypeArc::new(CachedTaskType {+                            native_fn,+                            this,+                            arg: arg.take_box(),+                        });+                        let task_id = if transient {+                            self.transient_task_id_factory.get()+                        } else {+                            self.persisted_task_id_factory.get()+                        };+                        // Initialize storage BEFORE making task_id visible in the cache.+                        // This ensures any thread that reads task_id from the cache sees+                        // the storage entry already initialized (restored flags set).+                        self.storage+                            .initialize_new_task(task_id, Some(task_type.clone()));+                        entry.insert((task_type, task_id));+                        (task_id, true)+                    }+                },+            ); -                    task_id+            // The entry closure has returned, so the task_cache shard lock is released before+            // cache tracking or aggregation updates can re-enter the backend.+            if created {+                self.track_cache_miss_by_fn(native_fn);+                // Update the aggregation number before connecting the child. We don't need this on+                // recovery paths because the aggregation number will already be set.+                if is_root {+                    AggregationUpdateQueue::run(+                        AggregationUpdateJob::UpdateAggregationNumber {+                            task_id,+                            base_aggregation_number: u32::MAX,+                            distance: None,+                        },+                        &mut ctx,+                    );+                } else if native_fn.is_session_dependent && self.should_track_dependencies() {+                    const SESSION_DEPENDENT_AGGREGATION_NUMBER: u32 = u32::MAX >> 2;+                    AggregationUpdateQueue::run(+                        AggregationUpdateJob::UpdateAggregationNumber {+                            task_id,+                            base_aggregation_number: SESSION_DEPENDENT_AGGREGATION_NUMBER,+                            distance: None,+                        },+                        &mut ctx,+                    );                 }+            } else {+                self.track_cache_hit_by_fn(native_fn);             }++            task_id         };          operation::ConnectChildOperation::run(parent_task, task_id, ctx);
turbopack/crates/turbo-tasks-backend/src/backend/storage.rs24 + / 43
@@ -9,8 +9,7 @@ use std::{     }, }; -use dashmap::SharedValue;-use hashbrown::raw::RawIntoIter;+use hashbrown::hash_table; use thread_local::ThreadLocal; use tracing::span::Id; use turbo_bincode::TurboBincodeBuffer;@@ -24,8 +23,8 @@ use crate::{     database::key_value_database::KeySpace,     utils::{         dash_map_drop_contents::drop_contents,-        dash_map_multi::{RefMut, get_multiple_mut},-        dash_map_raw_entry::{TryLockAndRemove, try_lock_and_remove},+        dash_map_entry::{TryLockAndRemove, try_lock_and_remove},+        dash_map_multi::{RefMut, get_disjoint_mut},     }, }; @@ -340,25 +339,18 @@ impl Storage {             let work = {                 let mut shard_guard = shard.write();                 if drain_entries {-                    // SAFETY: shard_guard outlives the iterator and we hold it for the whole scan.-                    for bucket in unsafe { shard_guard.iter() } {-                        // Read the key and modified flag, then drop the borrow before any erase.-                        // SAFETY: the guard outlives the bucket reference.-                        let (key, modified_task) = {-                            let (key, shared_value) = unsafe { bucket.as_ref() };-                            (*key, shared_value.get().flags.any_modified())-                        };+                    shard_guard.retain(|(key, task)| {+                        let modified_task = task.flags.any_modified();                         if modified_task {                             debug_assert!(                                 !key.is_transient(),                                 "found a modified transient task: {key:?}"                             );-                        } else {-                            // Unmodified entries are not part of the snapshot. Erase and free them-                            // now so the table we move out below holds only modified entries.-                            unsafe { shard_guard.erase(bucket) };                         }-                    }+                        // Unmodified entries are not part of the snapshot. Remove and free them+                        // now so the table we move out below holds only modified entries.+                        modified_task+                    });                     if shard_guard.is_empty() {                         // The shard held only unmodified entries, which we've now erased and freed.                         // No iterator is created for an empty shard.@@ -369,15 +361,12 @@ impl Storage {                     ShardWork::Drain(std::mem::take(&mut *shard_guard).into_iter())                 } else {                     let mut modified = Vec::with_capacity(modified_count as usize);-                    // SAFETY: shard_guard outlives the iterator and we hold it for the whole scan.-                    for bucket in unsafe { shard_guard.iter() } {-                        // SAFETY: the guard outlives the bucket reference.-                        let (key, shared_value) = unsafe { bucket.as_ref() };+                    for (key, task) in shard_guard.iter() {                         // Only check modified flags — transient tasks never have modified flags set                         // (track_modification guards against it), so this naturally excludes them.                         // new_task always comes with modified flags (set_persistent_task_type calls                         // track_modification), so any_modified() is sufficient.-                        if shared_value.get().flags.any_modified() {+                        if task.flags.any_modified() {                             debug_assert!(                                 !key.is_transient(),                                 "found a modified transient task: {key:?}"@@ -473,7 +462,7 @@ impl Storage {             let snap_shard = &snapshot_shards[shard_idx];              // Acquire in documented order: map first, snapshots second.-            let map_guard = map_shard.write();+            let mut map_guard = map_shard.write();             let mut snap_guard = snap_shard.write();              for (key, _) in snap_guard.drain() {@@ -482,11 +471,8 @@ impl Storage {                 // through `self.map.get_mut`, which would attempt to re-acquire this shard's                 // write lock and would also obscure the pairing.                 let hash = self.map.hasher().hash_one(key);-                if let Some(bucket) = map_guard.find(hash, |(k, _)| *k == key) {-                    // SAFETY: We hold `map_shard`'s write lock for the duration of this-                    // access, so the bucket pointer is valid and no other thread can alias it.-                    let (_, shared_value) = unsafe { bucket.as_mut() };-                    self.promote_during_snapshot_flags(shared_value.get_mut(), shard_idx);+                if let Some((_, task)) = map_guard.find_mut(hash, |(k, _)| *k == key) {+                    self.promote_during_snapshot_flags(task, shard_idx);                 }             }             // If we are saving a non-trivial amount of memory just clear it out.@@ -523,7 +509,7 @@ impl Storage {         key1: TaskId,         key2: TaskId,     ) -> (StorageWriteGuard<'_>, StorageWriteGuard<'_>) {-        let (a, b) = get_multiple_mut(&self.map, key1, key2, || Box::new(TaskStorage::new()));+        let (a, b) = get_disjoint_mut(&self.map, key1, key2, || Box::new(TaskStorage::new()));         (             StorageWriteGuard {                 storage: self,@@ -579,21 +565,18 @@ impl Storage {             // avoid a lock cycle with get_or_create_persistent_task, which takes task_cache             // before map. Allocated lazily on first conflict.             let mut deferred_task_cache_removals: Vec<CachedTaskTypeArc> = Vec::new();-            // SAFETY: We hold the write lock for the duration of iteration.-            for bucket in unsafe { shard.iter() } {-                // SAFETY: The write lock guard outlives the bucket reference.-                let (task_id, task) = unsafe { bucket.as_mut() };+            shard.retain(|(task_id, task)| {                 if task_id.is_transient() {                     evicted.unevictable_reasons[UnevictableReason::Transient.index()] += 1;-                    continue;+                    return true;                 }-                let (key_evictability, value_evictability) = task.get().evictability();+                let (key_evictability, value_evictability) = task.evictability();                 match key_evictability {                     KeyEvictability::Evictable => {                         // The task type is persisted to backing storage (new_task = false),                         // so task_cache is a pure perf cache. Remove it now; it will be                         // re-populated by task_by_type() on the next cache miss.-                        let task_type = task.get().get_persistent_task_type().unwrap();+                        let task_type = task.get_persistent_task_type().unwrap();                         // Only try to acquire the lock, if we cannot just remove at the end                         // Because `get_or_create_task` acquires 'task_cache' then `storage.map` and                         // we do the opposite we need to be defensive here.  Attempting here is just@@ -616,12 +599,10 @@ impl Storage {                 }                 match value_evictability {                     ValueEvictability::Evictable { meta, data } => {-                        match task.get_mut().drop_partial(data, meta) {+                        match task.drop_partial(data, meta) {                             DropPartialOutcome::Empty => {-                                unsafe {-                                    shard.erase(bucket);-                                }                                 evicted.full += 1;+                                return false;                             }                             DropPartialOutcome::HasResidue => {                                 if data && meta {@@ -639,7 +620,8 @@ impl Storage {                         evicted.unevictable_reasons[reason.index()] += 1;                     }                 }-            }+                true+            });             // Shrink the shard if it's less than half full, to reclaim slack capacity             // after bulk evictions. We already hold the write lock, so this is free             // from a locking perspective. TaskId hashing is cheap (it's just an integer).@@ -924,7 +906,7 @@ enum ShardWork {     /// (modified-only) shard table out of the map. The iterator owns that table and drains it     /// directly, freeing each task box as it is serialized. No second map lookup, no flag     /// bookkeeping (the whole map is discarded right after this snapshot).-    Drain(RawIntoIter<(TaskId, SharedValue<Box<TaskStorage>>)>),+    Drain(hash_table::IntoIter<(TaskId, Box<TaskStorage>)>), }  pub struct SnapshotShard<'l, P> {@@ -1008,7 +990,6 @@ where                 // bookkeeping the normal path does, since the entire map is discarded right after                 // this snapshot.                 let (task_id, inner) = entries.next()?;-                let inner = inner.into_inner();                 Some(serialize_task(task_id, &inner))                 // we don't need to update any bits because everything is getting dropped.             }
turbopack/crates/turbo-tasks-backend/src/utils/dash_map_entry.rsadded97 + / 0
@@ -0,0 +1,97 @@+use std::hash::{BuildHasher, Hash};++use crossbeam_utils::CachePadded;+use dashmap::{DashMap, RwLock};+use hashbrown::{HashTable, hash_table};++/// The type of a single shard inside a [`DashMap`].+///+/// `dashmap::HashMap<K, V>` is a private alias for `HashTable<(K, V)>`.+pub type Shard<K, V> = CachePadded<RwLock<HashTable<(K, V)>>>;++/// Returns a reference to the shard that owns the given pre-computed hash,+/// without locking anything.+///+/// Pass the returned reference to [`get_in_shard`] and+/// [`with_entry_in_shard`] so that the shard is only located once even when a+/// read-lock miss is followed by a write-lock retry.+pub fn get_shard<K: Eq + Hash, V, S: BuildHasher + Clone>(+    map: &DashMap<K, V, S>,+    hash: u64,+) -> &Shard<K, V> {+    let idx = map.determine_shard(hash as usize);+    &map.shards()[idx]+}++/// Read-only heterogeneous lookup using a pre-located shard reference.+/// Returns `Some(value)` on hit, `None` on miss. Uses only a read lock.+pub fn get_in_shard<K: Eq + Hash, V: Copy>(+    shard: &Shard<K, V>,+    hash: u64,+    eq: impl Fn(&K) -> bool,+) -> Option<V> {+    let guard = shard.read();+    guard.find(hash, |(k, _v)| eq(k)).map(|(_k, v)| *v)+}++/// Runs `then` with the native Hashbrown entry for a pre-located DashMap shard.+///+/// The shard write lock is held only for the duration of `then`. The caller+/// controls the precise point where the entry is consumed and the lock is+/// released by returning from the closure.+pub fn with_entry_in_shard<K: Eq + Hash, V, S: BuildHasher + Clone, Q: ?Sized, R>(+    shard: &Shard<K, V>,+    map_hasher: &S,+    hash: u64,+    query: &mut Q,+    eq: impl Fn(&K, &Q) -> bool,+    then: impl FnOnce(hash_table::Entry<'_, (K, V)>, &mut Q) -> R,+) -> R {+    let mut guard = shard.write();+    let entry = guard.entry(+        hash,+        |(k, _v)| eq(k, query),+        |(k, _v)| map_hasher.hash_one(k),+    );+    then(entry, query)+}++/// Outcome of [`try_lock_and_remove`].+pub enum TryLockAndRemove {+    /// The shard lock was acquired and a matching entry was removed.+    Removed,+    /// The shard lock was acquired but no matching entry was present.+    NotFound,+    /// The shard lock was contended; the caller should retry later after releasing+    /// any other locks they are holding.+    WouldBlock,+}++/// Remove `key` from `map` without blocking on shard contention.+///+/// Intended for call sites that already hold another lock and want to avoid a+/// cyclic wait. On contention (`WouldBlock`), the caller is expected to defer the+/// removal and retry after dropping the other lock.+pub fn try_lock_and_remove<+    K: Eq + Hash + AsRef<Q>,+    V,+    Q: Eq + Hash + ?Sized,+    S: BuildHasher + Clone,+>(+    map: &DashMap<K, V, S>,+    key: &Q,+) -> TryLockAndRemove {+    let hasher = map.hasher();+    let hash = hasher.hash_one(key);+    let shard_idx = map.determine_shard(hash as usize);+    let Some(mut shard) = map.shards()[shard_idx].try_write() else {+        return TryLockAndRemove::WouldBlock;+    };+    match shard.find_entry(hash, |(k, _v)| k.as_ref() == key) {+        Ok(entry) => {+            entry.remove();+            TryLockAndRemove::Removed+        }+        Err(_) => TryLockAndRemove::NotFound,+    }+}
turbopack/crates/turbo-tasks-backend/src/utils/dash_map_multi.rs66 + / 70
@@ -2,23 +2,25 @@ use std::{     hash::{BuildHasher, Hash},     marker::PhantomData,     ops::{Deref, DerefMut},+    ptr::NonNull,     sync::Arc, }; -use dashmap::{DashMap, RwLockWriteGuard, SharedValue};-use hashbrown::raw::{Bucket, RawTable};+use dashmap::{DashMap, RawRwLock};+use hashbrown::HashTable;+use parking_lot::lock_api::RwLockWriteGuard; -type RwLockWriteTableGuard<'a, K, V> = RwLockWriteGuard<'a, RawTable<(K, SharedValue<V>)>>;+type RwLockWriteTableGuard<'a, K, V> = RwLockWriteGuard<'a, RawRwLock, HashTable<(K, V)>>;  pub enum RefMut<'a, K, V> {     Base(dashmap::mapref::one::RefMut<'a, K, V>),     Simple {         _guard: RwLockWriteTableGuard<'a, K, V>,-        bucket: Bucket<(K, SharedValue<V>)>,+        entry: NonNull<(K, V)>,     },     Shared {         _guard: Arc<RwLockWriteTableGuard<'a, K, V>>,-        bucket: Bucket<(K, SharedValue<V>)>,+        entry: NonNull<(K, V)>,         // Ensures that RefMut is !Send, preventing holding RefMut across .await points in async         // code, which can cause deadlocks. See safety comment on `unsafe impl Sync for RefMut`         // below.@@ -34,13 +36,13 @@ pub enum RefMut<'a, K, V> { // while every other tokio worker piles up trying to take the same lock — leaving no thread free // to poll the parked future. Marking the type `!Send` makes the borrow checker reject those call // sites at compile time.-// SAFETY (Sync): `RefMut` contains a raw `Bucket` pointer into a `DashMap` shard's `RawTable`.+// SAFETY (Sync): `RefMut` contains a non-null pointer into a `DashMap` shard's `HashTable`. // Sharing `&RefMut` is safe because:-// - `Simple` variant: The `Bucket` is accessed under an exclusive `RwLockWriteGuard` on a single+// - `Simple` variant: The entry is accessed under an exclusive `RwLockWriteGuard` on a single //   shard. The guard provides exclusive access to all data in that shard.-// - `Shared` variant: The `Bucket` is accessed under an `Arc<RwLockWriteGuard>`. The-//   `get_multiple_mut` function asserts that bucket pointers do not alias, so each `RefMut` has-//   exclusive access to its bucket even when sharing a guard.+// - `Shared` variant: The entry is accessed under an `Arc<RwLockWriteGuard>`. The+//   `get_disjoint_mut` function validates that the keys differ before obtaining both references+//   through `HashTable::get_many_unchecked_mut`. // - `K: Sync + V: Sync` bounds ensure the key and value types are safe to share across threads. unsafe impl<K: Eq + Hash + Sync, V: Sync> Sync for RefMut<'_, K, V> {} @@ -60,28 +62,24 @@ impl<K: Eq + Hash, V> RefMut<'_, K, V> {     pub fn pair(&self) -> (&K, &V) {         match self {             RefMut::Base(r) => r.pair(),-            RefMut::Simple { bucket, .. } | RefMut::Shared { bucket, .. } => {-                // SAFETY:-                // - The bucket is still valid, as we're holding a write guard on the shard-                // - These bucket pointers are convertible to references-                //-                // https://doc.rust-lang.org/std/ptr/index.html#pointer-to-reference-conversion-                let entry = unsafe { bucket.as_ref() };-                (&entry.0, entry.1.get())+            RefMut::Simple { entry, .. } | RefMut::Shared { entry, .. } => {+                // SAFETY: The entry remains valid while the shard write guard is held.+                let entry = unsafe { entry.as_ref() };+                (&entry.0, &entry.1)             }         }     }      pub fn pair_mut(&mut self) -> (&K, &mut V) {         match self {             RefMut::Base(r) => r.pair_mut(),-            RefMut::Simple { bucket, .. } | RefMut::Shared { bucket, .. } => {+            RefMut::Simple { entry, .. } | RefMut::Shared { entry, .. } => {                 // SAFETY: Same as above in `pair`, plus aliasing is prevented via:                 // 1. The lifetime of `&mut self`.-                // 2. `Simple` values come from separate shards (no aliasing possible)-                // 3. `Shared` values are asserted in `get_multiple_mut` to have unique pointers-                let entry = unsafe { bucket.as_mut() };-                (&entry.0, entry.1.get_mut())+                // 2. `Simple` values come from separate shards (no aliasing possible).+                // 3. `Shared` values were validated as disjoint before the pointers were created.+                let entry = unsafe { entry.as_mut() };+                (&entry.0, &mut entry.1)             }         }     }@@ -110,7 +108,7 @@ where     } } -pub fn get_multiple_mut<K, V>(+pub fn get_disjoint_mut<K, V>(     map: &DashMap<K, V, impl BuildHasher + Clone>,     key1: K,     key2: K,@@ -135,49 +133,43 @@ where      let shards = map.shards();     if s1 == s2 {-        let mut guard = shards[s1].write();--        // we need to call `find_or_find_insert_slot` to avoid overwriting existing entries, but we-        // can't use the returned bucket until after we get `bucket2` (below)-        let _ = guard-            .find_or_find_insert_slot(h1, eq1, hash_entry)-            .unwrap_or_else(|slot| unsafe {-                // SAFETY: This slot was previously returned by `find_or_find_insert_slot`, and no-                // mutation of the table has occurred since that call.-                guard.insert_in_slot(h1, slot, (key1.clone(), SharedValue::new(insert_with())))-            });+        // Equal keys would resolve to a single entry below. This must be a release-mode assertion+        // because the unchecked lookup relies on it for memory safety.+        assert!(+            key1 != key2,+            "`get_disjoint_mut` was called with equal keys, which breaks mutable referencing rules"+        ); -        let bucket2 = guard-            .find_or_find_insert_slot(h2, eq2, hash_entry)-            .unwrap_or_else(|slot| unsafe {-                // SAFETY: See previous call above-                guard.insert_in_slot(h2, slot, (key2.clone(), SharedValue::new(insert_with())))-            });+        let mut guard = shards[s1].write(); -        // Getting `bucket2` might invalidate the bucket pointer of the first entry, *even if no-        // insert happens* as `RawTable::find_or_find_insert_slot` will *sometimes* resize the-        // table, as it unconditionally reserves space for a potential insertion.-        let bucket1 = guard.find(h1, eq1).expect(-            "failed to find bucket of previously inserted item, is the hash or eq implementation \-             incorrect?",-        );+        if guard.find(h1, eq1).is_none() {+            guard.insert_unique(h1, (key1.clone(), insert_with()), hash_entry);+        }+        if guard.find(h2, eq2).is_none() {+            guard.insert_unique(h2, (key2.clone(), insert_with()), hash_entry);+        } -        // this assertion is needed for memory safety reasons-        assert!(-            !std::ptr::eq(bucket1.as_ptr(), bucket2.as_ptr()),-            "`get_multiple_mut` was called with equal keys, which breaks mutable referencing rules"-        );+        // SAFETY: `key1 != key2` was asserted above. Since `K: Eq`, the two equality closures+        // cannot select the same entry, even when the hashes collide.+        let [entry1, entry2] =+            unsafe {+                guard.get_many_unchecked_mut([h1, h2], |index, entry| {+                    if index == 0 { eq1(entry) } else { eq2(entry) }+                })+            };+        let entry1 = NonNull::from(entry1.expect("the first entry was inserted above"));+        let entry2 = NonNull::from(entry2.expect("the second entry was inserted above"));          let guard = Arc::new(guard);         (             RefMut::Shared {                 _guard: guard.clone(),-                bucket: bucket1,+                entry: entry1,                 phantom: PhantomData,             },             RefMut::Shared {                 _guard: guard,-                bucket: bucket2,+                entry: entry2,                 phantom: PhantomData,             },         )@@ -197,27 +189,31 @@ where             }         }; -        let bucket1 = guard1-            .find_or_find_insert_slot(h1, eq1, hash_entry)-            .unwrap_or_else(|slot| unsafe {-                // SAFETY: See first insert_in_slot call-                guard1.insert_in_slot(h1, slot, (key1.clone(), SharedValue::new(insert_with())))-            });-        let bucket2 = guard2-            .find_or_find_insert_slot(h2, eq2, hash_entry)-            .unwrap_or_else(|slot| unsafe {-                // SAFETY: See first insert_in_slot call-                guard2.insert_in_slot(h2, slot, (key2.clone(), SharedValue::new(insert_with())))-            });+        if guard1.find(h1, eq1).is_none() {+            guard1.insert_unique(h1, (key1.clone(), insert_with()), hash_entry);+        }+        if guard2.find(h2, eq2).is_none() {+            guard2.insert_unique(h2, (key2.clone(), insert_with()), hash_entry);+        }+        let entry1 = NonNull::from(+            guard1+                .find_mut(h1, eq1)+                .expect("the first entry was inserted"),+        );+        let entry2 = NonNull::from(+            guard2+                .find_mut(h2, eq2)+                .expect("the second entry was inserted"),+        );          (             RefMut::Simple {                 _guard: guard1,-                bucket: bucket1,+                entry: entry1,             },             RefMut::Simple {                 _guard: guard2,-                bucket: bucket2,+                entry: entry2,             },         )     }@@ -250,7 +246,7 @@ mod tests {             for indices in indices {                 s.spawn(|| {                     for i in indices {-                        let (mut a, mut b) = get_multiple_mut(map, i, i + 1, || 0);+                        let (mut a, mut b) = get_disjoint_mut(map, i, i + 1, || 0);                         *a += 1;                         *b += 1;                     }
turbopack/crates/turbo-tasks-backend/src/utils/dash_map_raw_entry.rsremoved0 + / 143
@@ -1,143 +0,0 @@-use std::hash::{BuildHasher, Hash};--use crossbeam_utils::CachePadded;-use dashmap::{DashMap, RwLock, RwLockWriteGuard, SharedValue};-use hashbrown::raw::{Bucket, InsertSlot, RawTable};--/// The type of a single shard inside a [`DashMap`].-///-/// `dashmap::HashMap<K, V>` is a private alias for `RawTable<(K, SharedValue<V>)>`.-pub type Shard<K, V> = CachePadded<RwLock<RawTable<(K, SharedValue<V>)>>>;--/// Returns a reference to the shard that owns the given pre-computed hash,-/// without locking anything.-///-/// Pass the returned reference to [`raw_get_in_shard`] and-/// [`raw_entry_in_shard`] so that the shard is only located once even when a-/// read-lock miss is followed by a write-lock retry.-pub fn get_shard<K: Eq + Hash, V, S: BuildHasher + Clone>(-    map: &DashMap<K, V, S>,-    hash: u64,-) -> &Shard<K, V> {-    let idx = map.determine_shard(hash as usize);-    &map.shards()[idx]-}--/// Read-only heterogeneous lookup using a pre-located shard reference.-/// Returns `Some(value)` on hit, `None` on miss. Uses only a read lock.-pub fn raw_get_in_shard<K: Eq + Hash, V: Copy>(-    shard: &Shard<K, V>,-    hash: u64,-    eq: impl Fn(&K) -> bool,-) -> Option<V> {-    let guard = shard.read();-    // Safety: We have a read lock on the shard.-    guard-        .find(hash, |(k, _v)| eq(k))-        .map(|bucket| *unsafe { bucket.as_ref() }.1.get())-}--/// Write-lock entry lookup using a pre-located shard reference and-/// heterogeneous equality.-///-/// Takes a pre-located `shard` (from [`get_shard`]) and `hash` so the shard is-/// not located a second time on a read-miss / write-retry path.-pub fn raw_entry_in_shard<'l, K: Eq + Hash, V, S: BuildHasher + Clone>(-    shard: &'l Shard<K, V>,-    map_hasher: &S,-    hash: u64,-    eq: impl Fn(&K) -> bool,-) -> RawEntry<'l, K, V> {-    let mut guard = shard.write();-    let result =-        guard.find_or_find_insert_slot(hash, |(k, _v)| eq(k), |(k, _v)| map_hasher.hash_one(k));-    match result {-        Ok(bucket) => RawEntry::Occupied(OccupiedEntry {-            bucket,-            shard: guard,-        }),-        Err(insert_slot) => RawEntry::Vacant(VacantEntry {-            hash,-            insert_slot,-            shard: guard,-        }),-    }-}--/// Outcome of [`try_lock_and_remove`].-pub enum TryLockAndRemove {-    /// The shard lock was acquired and a matching entry was removed.-    Removed,-    /// The shard lock was acquired but no matching entry was present.-    NotFound,-    /// The shard lock was contended; the caller should retry later after releasing-    /// any other locks they are holding.-    WouldBlock,-}--/// Remove `key` from `map` without blocking on shard contention.-///-/// Intended for call sites that already hold another lock and want to avoid a-/// cyclic wait. On contention (`WouldBlock`), the caller is expected to defer the-/// removal and retry after dropping the other lock.-pub fn try_lock_and_remove<-    K: Eq + Hash + AsRef<Q>,-    V,-    Q: Eq + Hash + ?Sized,-    S: BuildHasher + Clone,->(-    map: &DashMap<K, V, S>,-    key: &Q,-) -> TryLockAndRemove {-    let hasher = map.hasher();-    let hash = hasher.hash_one(key);-    let shard_idx = map.determine_shard(hash as usize);-    let Some(mut shard) = map.shards()[shard_idx].try_write() else {-        return TryLockAndRemove::WouldBlock;-    };-    // SAFETY: we hold the write lock for the duration of the find/erase.-    match shard.find(hash, |(k, _v)| k.as_ref() == key) {-        Some(bucket) => {-            unsafe { shard.erase(bucket) };-            TryLockAndRemove::Removed-        }-        None => TryLockAndRemove::NotFound,-    }-}--pub enum RawEntry<'l, K, V> {-    Occupied(OccupiedEntry<'l, K, V>),-    Vacant(VacantEntry<'l, K, V>),-}--pub struct OccupiedEntry<'l, K, V> {-    bucket: Bucket<(K, SharedValue<V>)>,-    #[allow(dead_code, reason = "kept to ensure the lock lives long enough")]-    shard: RwLockWriteGuard<'l, RawTable<(K, SharedValue<V>)>>,-}--impl<'l, K, V> OccupiedEntry<'l, K, V> {-    pub fn get(&self) -> &V {-        // Safety: We have a write lock on the shard, so no other references to the value can-        // exist.-        unsafe { self.bucket.as_ref().1.get() }-    }-}--pub struct VacantEntry<'l, K, V> {-    hash: u64,-    insert_slot: InsertSlot,-    shard: RwLockWriteGuard<'l, RawTable<(K, SharedValue<V>)>>,-}--impl<'l, K, V> VacantEntry<'l, K, V> {-    pub fn insert(mut self, key: K, value: V) {-        let shared_value = SharedValue::new(value);-        // Safety: The insert slot is valid and the map has not been modified since we obtained it-        // (we hold the write lock).-        unsafe {-            self.shard-                .insert_in_slot(self.hash, self.insert_slot, (key, shared_value));-        }-    }-}
turbopack/crates/turbo-tasks-backend/src/utils/mod.rs1 + / 1
@@ -1,6 +1,6 @@ pub mod dash_map_drop_contents;+pub mod dash_map_entry; pub mod dash_map_multi;-pub mod dash_map_raw_entry; pub mod markdown_table; pub mod ptr_eq_arc; pub mod shard_amount;
turbopack/crates/turbopack-trace-server/Cargo.toml1 + / 1
@@ -19,7 +19,7 @@ bench = false anyhow = { workspace = true } either = { workspace = true } flate2 = { workspace = true }-hashbrown = { workspace = true, features = ["raw"] }+hashbrown = { workspace = true, features = ["raw-entry"] } indexmap = { workspace = true, features = ["serde"] } itertools = { workspace = true } postcard = { workspace = true }