Skip to content

Commit 0ff9d29

Browse files
committed
fix: use nohash in the resolver for the activation keys
1 parent cecea5c commit 0ff9d29

File tree

8 files changed

+108
-27
lines changed

8 files changed

+108
-27
lines changed

Cargo.lock

+7
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+2
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ libgit2-sys = "0.17.0"
6767
libloading = "0.8.5"
6868
memchr = "2.7.4"
6969
miow = "0.6.0"
70+
nohash-hasher = "0.2.0"
7071
opener = "0.7.1"
7172
openssl = "=0.10.57" # See rust-lang/cargo#13546 and openssl/openssl#23376 for pinning
7273
openssl-sys = "=0.9.92" # See rust-lang/cargo#13546 and openssl/openssl#23376 for pinning
@@ -182,6 +183,7 @@ jobserver.workspace = true
182183
lazycell.workspace = true
183184
libgit2-sys.workspace = true
184185
memchr.workspace = true
186+
nohash-hasher.workspace = true
185187
opener.workspace = true
186188
os_info.workspace = true
187189
pasetors.workspace = true

crates/resolver-tests/src/sat.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -395,7 +395,7 @@ impl SatResolver {
395395
&mut solver,
396396
var_for_is_packages_used
397397
.iter()
398-
.map(|(p, &v)| (p.as_activations_key(), v)),
398+
.map(|(p, &v)| (p.activation_key(), v)),
399399
);
400400

401401
for pkg in by_name.values().flatten() {

src/cargo/core/activation_key.rs

+52-8
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,61 @@
1+
use std::collections::HashSet;
2+
use std::hash::{Hash, Hasher};
13
use std::num::NonZeroU64;
4+
use std::sync::{Mutex, OnceLock};
25

36
use crate::core::SourceId;
47
use crate::util::interning::InternedString;
58

6-
/// Find the activated version of a crate based on the name, source, and semver compatibility.
7-
/// By storing this in a hash map we ensure that there is only one
8-
/// semver compatible version of each crate.
9-
/// This all so stores the `ContextAge`.
10-
pub type ActivationsKey = (InternedString, SourceId, SemverCompatibility);
9+
static ACTIVATION_KEY_CACHE: OnceLock<Mutex<HashSet<&'static ActivationKeyInner>>> =
10+
OnceLock::new();
1111

12-
/// A type that represents when cargo treats two Versions as compatible.
13-
/// Versions `a` and `b` are compatible if their left-most nonzero digit is the
14-
/// same.
12+
type ActivationKeyInner = (InternedString, SourceId, SemverCompatibility);
13+
14+
/// The activated version of a crate is based on the name, source, and semver compatibility.
15+
#[derive(Clone, Copy, Eq)]
16+
pub struct ActivationKey {
17+
inner: &'static ActivationKeyInner,
18+
}
19+
20+
impl From<ActivationKeyInner> for ActivationKey {
21+
fn from(inner: ActivationKeyInner) -> Self {
22+
let mut cache = ACTIVATION_KEY_CACHE
23+
.get_or_init(|| Default::default())
24+
.lock()
25+
.unwrap();
26+
let inner = cache.get(&inner).cloned().unwrap_or_else(|| {
27+
let inner = Box::leak(Box::new(inner));
28+
cache.insert(inner);
29+
inner
30+
});
31+
Self { inner }
32+
}
33+
}
34+
35+
impl ActivationKey {
36+
/// This function is used for the `Eq` and `Hash` impls to implement a "no hash" hashable value.
37+
/// This is possible since all `ActivationKey` are already interned in a `HashSet`.
38+
fn key(&self) -> u64 {
39+
std::ptr::from_ref(self.inner) as u64
40+
}
41+
}
42+
43+
impl PartialEq for ActivationKey {
44+
fn eq(&self, other: &Self) -> bool {
45+
self.key() == other.key()
46+
}
47+
}
48+
49+
impl nohash_hasher::IsEnabled for ActivationKey {}
50+
51+
impl Hash for ActivationKey {
52+
fn hash<H: Hasher>(&self, state: &mut H) {
53+
state.write_u64(self.key());
54+
}
55+
}
56+
57+
/// A type that represents when cargo treats two versions as compatible.
58+
/// Versions `a` and `b` are compatible if their left-most nonzero digit is the same.
1559
#[derive(Clone, Copy, Eq, PartialEq, Hash, Debug, PartialOrd, Ord)]
1660
pub enum SemverCompatibility {
1761
Major(NonZeroU64),

src/cargo/core/mod.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
pub use self::activation_key::ActivationsKey;
1+
pub use self::activation_key::ActivationKey;
22
pub use self::dependency::{Dependency, SerializedDependency};
33
pub use self::features::{CliUnstable, Edition, Feature, Features};
44
pub use self::manifest::{EitherManifest, VirtualManifest};

src/cargo/core/package_id.rs

+24-4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use std::cmp::Ordering;
12
use std::collections::HashSet;
23
use std::fmt::{self, Formatter};
34
use std::hash;
@@ -10,7 +11,7 @@ use std::sync::OnceLock;
1011
use serde::de;
1112
use serde::ser;
1213

13-
use crate::core::ActivationsKey;
14+
use crate::core::ActivationKey;
1415
use crate::core::PackageIdSpec;
1516
use crate::core::SourceId;
1617
use crate::util::interning::InternedString;
@@ -24,13 +25,31 @@ pub struct PackageId {
2425
inner: &'static PackageIdInner,
2526
}
2627

27-
#[derive(PartialOrd, Eq, Ord)]
2828
struct PackageIdInner {
2929
name: InternedString,
3030
version: semver::Version,
3131
source_id: SourceId,
32+
// This field is used as a cache to improve the resolver speed,
33+
// and is not included in the `Eq`, `Hash` and `Ord` impls.
34+
activation_key: ActivationKey,
3235
}
3336

37+
impl Ord for PackageIdInner {
38+
fn cmp(&self, other: &Self) -> Ordering {
39+
let self_key = (self.name, &self.version, self.source_id);
40+
let other_key = (other.name, &other.version, other.source_id);
41+
self_key.cmp(&other_key)
42+
}
43+
}
44+
45+
impl PartialOrd for PackageIdInner {
46+
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
47+
Some(self.cmp(&other))
48+
}
49+
}
50+
51+
impl Eq for PackageIdInner {}
52+
3453
// Custom equality that uses full equality of SourceId, rather than its custom equality.
3554
//
3655
// The `build` part of the version is usually ignored (like a "comment").
@@ -136,6 +155,7 @@ impl PackageId {
136155

137156
pub fn new(name: InternedString, version: semver::Version, source_id: SourceId) -> PackageId {
138157
let inner = PackageIdInner {
158+
activation_key: (name, source_id, (&version).into()).into(),
139159
name,
140160
version,
141161
source_id,
@@ -161,8 +181,8 @@ impl PackageId {
161181
pub fn source_id(self) -> SourceId {
162182
self.inner.source_id
163183
}
164-
pub fn as_activations_key(self) -> ActivationsKey {
165-
(self.name(), self.source_id(), self.version().into())
184+
pub fn activation_key(self) -> ActivationKey {
185+
self.inner.activation_key
166186
}
167187

168188
pub fn with_source_id(self, source: SourceId) -> PackageId {

src/cargo/core/resolver/context.rs

+19-10
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use super::dep_cache::RegistryQueryer;
22
use super::errors::ActivateResult;
33
use super::types::{ConflictMap, ConflictReason, FeaturesSet, ResolveOpts};
44
use super::RequestedFeatures;
5-
use crate::core::{ActivationsKey, Dependency, PackageId, Summary};
5+
use crate::core::{ActivationKey, Dependency, PackageId, Summary};
66
use crate::util::interning::InternedString;
77
use crate::util::Graph;
88
use anyhow::format_err;
@@ -26,8 +26,13 @@ pub struct ResolverContext {
2626
pub parents: Graph<PackageId, im_rc::HashSet<Dependency, rustc_hash::FxBuildHasher>>,
2727
}
2828

29-
pub type Activations =
30-
im_rc::HashMap<ActivationsKey, (Summary, ContextAge), rustc_hash::FxBuildHasher>;
29+
/// By storing activation keys in a `HashMap` we ensure that there is only one
30+
/// semver compatible version of each crate.
31+
type Activations = im_rc::HashMap<
32+
ActivationKey,
33+
(Summary, ContextAge),
34+
nohash_hasher::BuildNoHashHasher<ActivationKey>,
35+
>;
3136

3237
/// When backtracking it can be useful to know how far back to go.
3338
/// The `ContextAge` of a `Context` is a monotonically increasing counter of the number
@@ -62,7 +67,7 @@ impl ResolverContext {
6267
) -> ActivateResult<bool> {
6368
let id = summary.package_id();
6469
let age: ContextAge = self.age;
65-
match self.activations.entry(id.as_activations_key()) {
70+
match self.activations.entry(id.activation_key()) {
6671
im_rc::hashmap::Entry::Occupied(o) => {
6772
debug_assert_eq!(
6873
&o.get().0,
@@ -101,7 +106,7 @@ impl ResolverContext {
101106
// versions came from a `[patch]` source.
102107
if let Some((_, dep)) = parent {
103108
if dep.source_id() != id.source_id() {
104-
let key = (id.name(), dep.source_id(), id.version().into());
109+
let key = (id.name(), dep.source_id(), id.version().into()).into();
105110
let prev = self.activations.insert(key, (summary.clone(), age));
106111
if let Some((previous_summary, _)) = prev {
107112
return Err(
@@ -145,9 +150,13 @@ impl ResolverContext {
145150

146151
/// If the package is active returns the `ContextAge` when it was added
147152
pub fn is_active(&self, id: PackageId) -> Option<ContextAge> {
148-
self.activations
149-
.get(&id.as_activations_key())
150-
.and_then(|(s, l)| if s.package_id() == id { Some(*l) } else { None })
153+
let (summary, age) = self.activations.get(&id.activation_key())?;
154+
155+
if summary.package_id() == id {
156+
Some(*age)
157+
} else {
158+
None
159+
}
151160
}
152161

153162
/// Checks whether all of `parent` and the keys of `conflicting activations`
@@ -163,8 +172,8 @@ impl ResolverContext {
163172
max = std::cmp::max(max, self.is_active(parent)?);
164173
}
165174

166-
for id in conflicting_activations.keys() {
167-
max = std::cmp::max(max, self.is_active(*id)?);
175+
for &id in conflicting_activations.keys() {
176+
max = std::cmp::max(max, self.is_active(id)?);
168177
}
169178
Some(max)
170179
}

src/cargo/core/resolver/mod.rs

+2-3
Original file line numberDiff line numberDiff line change
@@ -198,8 +198,7 @@ fn activate_deps_loop(
198198
let mut backtrack_stack = Vec::new();
199199
let mut remaining_deps = RemainingDeps::new();
200200

201-
// `past_conflicting_activations` is a cache of the reasons for each time we
202-
// backtrack.
201+
// `past_conflicting_activations` is a cache of the reasons for each time we backtrack.
203202
let mut past_conflicting_activations = conflict_cache::ConflictCache::new();
204203

205204
// Activate all the initial summaries to kick off some work.
@@ -775,7 +774,7 @@ impl RemainingCandidates {
775774
//
776775
// Here we throw out our candidate if it's *compatible*, yet not
777776
// equal, to all previously activated versions.
778-
if let Some((a, _)) = cx.activations.get(&b_id.as_activations_key()) {
777+
if let Some((a, _)) = cx.activations.get(&b_id.activation_key()) {
779778
if *a != b {
780779
conflicting_prev_active
781780
.entry(a.package_id())

0 commit comments

Comments
 (0)