Skip to content

Commit 43b64a2

Browse files
committed
fix: use nohash in the resolver for the activation keys
1 parent 93db5bf commit 43b64a2

File tree

7 files changed

+138
-53
lines changed

7 files changed

+138
-53
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
@@ -66,6 +66,7 @@ libgit2-sys = "0.17.0"
6666
libloading = "0.8.5"
6767
memchr = "2.7.4"
6868
miow = "0.6.0"
69+
nohash-hasher = "0.2.0"
6970
opener = "0.7.1"
7071
openssl = "=0.10.57" # See rust-lang/cargo#13546 and openssl/openssl#23376 for pinning
7172
openssl-sys = "=0.9.92" # See rust-lang/cargo#13546 and openssl/openssl#23376 for pinning
@@ -179,6 +180,7 @@ jobserver.workspace = true
179180
lazycell.workspace = true
180181
libgit2-sys.workspace = true
181182
memchr.workspace = true
183+
nohash-hasher.workspace = true
182184
opener.workspace = true
183185
os_info.workspace = true
184186
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().no_hash(), v)),
399399
);
400400

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

src/cargo/core/package_id.rs

+25-1
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,6 +11,7 @@ use std::sync::OnceLock;
1011
use serde::de;
1112
use serde::ser;
1213

14+
use crate::core::resolver::activation_key::ActivationKey;
1315
use crate::core::PackageIdSpec;
1416
use crate::core::SourceId;
1517
use crate::util::interning::InternedString;
@@ -23,13 +25,31 @@ pub struct PackageId {
2325
inner: &'static PackageIdInner,
2426
}
2527

26-
#[derive(PartialOrd, Eq, Ord)]
2728
struct PackageIdInner {
2829
name: InternedString,
2930
version: semver::Version,
3031
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,
3135
}
3236

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+
3353
// Custom equality that uses full equality of SourceId, rather than its custom equality.
3454
//
3555
// The `build` part of the version is usually ignored (like a "comment").
@@ -135,6 +155,7 @@ impl PackageId {
135155

136156
pub fn new(name: InternedString, version: semver::Version, source_id: SourceId) -> PackageId {
137157
let inner = PackageIdInner {
158+
activation_key: (name, source_id, (&version).into()).into(),
138159
name,
139160
version,
140161
source_id,
@@ -160,6 +181,9 @@ impl PackageId {
160181
pub fn source_id(self) -> SourceId {
161182
self.inner.source_id
162183
}
184+
pub fn activation_key(self) -> ActivationKey {
185+
self.inner.activation_key
186+
}
163187

164188
pub fn with_source_id(self, source: SourceId) -> PackageId {
165189
PackageId::new(self.inner.name, self.inner.version.clone(), source)
+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
use std::collections::HashSet;
2+
use std::hash::{Hash, Hasher};
3+
use std::num::NonZeroU64;
4+
use std::sync::{Mutex, OnceLock};
5+
6+
use crate::core::SourceId;
7+
use crate::util::interning::InternedString;
8+
9+
static ACTIVATION_KEY_CACHE: OnceLock<Mutex<HashSet<&'static ActivationKeyInner>>> =
10+
OnceLock::new();
11+
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, PartialEq, Eq, PartialOrd, Ord)]
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+
/// A type that represents when cargo treats two versions as compatible.
36+
/// Versions `a` and `b` are compatible if their left-most nonzero digit is the same.
37+
#[derive(Clone, Copy, Eq, PartialEq, Hash, Debug, PartialOrd, Ord)]
38+
pub enum SemverCompatibility {
39+
Major(NonZeroU64),
40+
Minor(NonZeroU64),
41+
Patch(u64),
42+
}
43+
44+
impl From<&semver::Version> for SemverCompatibility {
45+
fn from(ver: &semver::Version) -> Self {
46+
if let Some(m) = NonZeroU64::new(ver.major) {
47+
return SemverCompatibility::Major(m);
48+
}
49+
if let Some(m) = NonZeroU64::new(ver.minor) {
50+
return SemverCompatibility::Minor(m);
51+
}
52+
SemverCompatibility::Patch(ver.patch)
53+
}
54+
}
55+
56+
impl ActivationKey {
57+
/// Returns a value that implements a "no hash" hashable value.
58+
/// This is possible since all `ActivationKey` are already interned in a `HashSet`.
59+
pub fn no_hash(self) -> ActivationKeyNoHash {
60+
ActivationKeyNoHash(std::ptr::from_ref(self.inner) as u64)
61+
}
62+
}
63+
64+
#[derive(Clone, Copy, Eq, PartialEq)]
65+
pub struct ActivationKeyNoHash(u64);
66+
67+
impl nohash_hasher::IsEnabled for ActivationKeyNoHash {}
68+
69+
impl Hash for ActivationKeyNoHash {
70+
fn hash<H: Hasher>(&self, state: &mut H) {
71+
state.write_u64(self.0);
72+
}
73+
}

src/cargo/core/resolver/context.rs

+27-48
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1+
use super::activation_key::ActivationKeyNoHash;
12
use super::dep_cache::RegistryQueryer;
23
use super::errors::ActivateResult;
34
use super::types::{ConflictMap, ConflictReason, FeaturesSet, ResolveOpts};
45
use super::RequestedFeatures;
5-
use crate::core::{Dependency, PackageId, SourceId, Summary};
6+
use crate::core::{Dependency, PackageId, Summary};
67
use crate::util::interning::InternedString;
78
use crate::util::Graph;
89
use anyhow::format_err;
910
use std::collections::HashMap;
10-
use std::num::NonZeroU64;
1111
use tracing::debug;
1212

1313
// A `Context` is basically a bunch of local resolution information which is
@@ -22,56 +22,26 @@ pub struct ResolverContext {
2222
pub resolve_features: im_rc::HashMap<PackageId, FeaturesSet, rustc_hash::FxBuildHasher>,
2323
/// get the package that will be linking to a native library by its links attribute
2424
pub links: im_rc::HashMap<InternedString, PackageId, rustc_hash::FxBuildHasher>,
25-
2625
/// a way to look up for a package in activations what packages required it
2726
/// and all of the exact deps that it fulfilled.
2827
pub parents: Graph<PackageId, im_rc::HashSet<Dependency, rustc_hash::FxBuildHasher>>,
2928
}
3029

30+
/// By storing activation keys in a `HashMap` we ensure that there is only one
31+
/// semver compatible version of each crate.
32+
type Activations = im_rc::HashMap<
33+
ActivationKeyNoHash,
34+
(Summary, ContextAge),
35+
nohash_hasher::BuildNoHashHasher<ActivationKeyNoHash>,
36+
>;
37+
3138
/// When backtracking it can be useful to know how far back to go.
3239
/// The `ContextAge` of a `Context` is a monotonically increasing counter of the number
3340
/// of decisions made to get to this state.
3441
/// Several structures store the `ContextAge` when it was added,
3542
/// to be used in `find_candidate` for backtracking.
3643
pub type ContextAge = usize;
3744

38-
/// Find the activated version of a crate based on the name, source, and semver compatibility.
39-
/// By storing this in a hash map we ensure that there is only one
40-
/// semver compatible version of each crate.
41-
/// This all so stores the `ContextAge`.
42-
pub type ActivationsKey = (InternedString, SourceId, SemverCompatibility);
43-
44-
pub type Activations =
45-
im_rc::HashMap<ActivationsKey, (Summary, ContextAge), rustc_hash::FxBuildHasher>;
46-
47-
/// A type that represents when cargo treats two Versions as compatible.
48-
/// Versions `a` and `b` are compatible if their left-most nonzero digit is the
49-
/// same.
50-
#[derive(Clone, Copy, Eq, PartialEq, Hash, Debug, PartialOrd, Ord)]
51-
pub enum SemverCompatibility {
52-
Major(NonZeroU64),
53-
Minor(NonZeroU64),
54-
Patch(u64),
55-
}
56-
57-
impl From<&semver::Version> for SemverCompatibility {
58-
fn from(ver: &semver::Version) -> Self {
59-
if let Some(m) = NonZeroU64::new(ver.major) {
60-
return SemverCompatibility::Major(m);
61-
}
62-
if let Some(m) = NonZeroU64::new(ver.minor) {
63-
return SemverCompatibility::Minor(m);
64-
}
65-
SemverCompatibility::Patch(ver.patch)
66-
}
67-
}
68-
69-
impl PackageId {
70-
pub fn as_activations_key(self) -> ActivationsKey {
71-
(self.name(), self.source_id(), self.version().into())
72-
}
73-
}
74-
7545
impl ResolverContext {
7646
pub fn new() -> ResolverContext {
7747
ResolverContext {
@@ -98,7 +68,7 @@ impl ResolverContext {
9868
) -> ActivateResult<bool> {
9969
let id = summary.package_id();
10070
let age: ContextAge = self.age;
101-
match self.activations.entry(id.as_activations_key()) {
71+
match self.activations.entry(id.activation_key().no_hash()) {
10272
im_rc::hashmap::Entry::Occupied(o) => {
10373
debug_assert_eq!(
10474
&o.get().0,
@@ -137,8 +107,13 @@ impl ResolverContext {
137107
// versions came from a `[patch]` source.
138108
if let Some((_, dep)) = parent {
139109
if dep.source_id() != id.source_id() {
140-
let key = (id.name(), dep.source_id(), id.version().into());
141-
let prev = self.activations.insert(key, (summary.clone(), age));
110+
let new_id =
111+
PackageId::new(id.name(), id.version().clone(), dep.source_id());
112+
113+
let prev = self
114+
.activations
115+
.insert(new_id.activation_key().no_hash(), (summary.clone(), age));
116+
142117
if let Some((previous_summary, _)) = prev {
143118
return Err(
144119
(previous_summary.package_id(), ConflictReason::Semver).into()
@@ -181,9 +156,13 @@ impl ResolverContext {
181156

182157
/// If the package is active returns the `ContextAge` when it was added
183158
pub fn is_active(&self, id: PackageId) -> Option<ContextAge> {
184-
self.activations
185-
.get(&id.as_activations_key())
186-
.and_then(|(s, l)| if s.package_id() == id { Some(*l) } else { None })
159+
let (summary, age) = self.activations.get(&id.activation_key().no_hash())?;
160+
161+
if summary.package_id() == id {
162+
Some(*age)
163+
} else {
164+
None
165+
}
187166
}
188167

189168
/// Checks whether all of `parent` and the keys of `conflicting activations`
@@ -199,8 +178,8 @@ impl ResolverContext {
199178
max = std::cmp::max(max, self.is_active(parent)?);
200179
}
201180

202-
for id in conflicting_activations.keys() {
203-
max = std::cmp::max(max, self.is_active(*id)?);
181+
for &id in conflicting_activations.keys() {
182+
max = std::cmp::max(max, self.is_active(id)?);
204183
}
205184
Some(max)
206185
}

src/cargo/core/resolver/mod.rs

+3-3
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ pub use self::resolve::{Resolve, ResolveVersion};
8585
pub use self::types::{ResolveBehavior, ResolveOpts};
8686
pub use self::version_prefs::{VersionOrdering, VersionPreferences};
8787

88+
pub(crate) mod activation_key;
8889
mod conflict_cache;
8990
mod context;
9091
mod dep_cache;
@@ -198,8 +199,7 @@ fn activate_deps_loop(
198199
let mut backtrack_stack = Vec::new();
199200
let mut remaining_deps = RemainingDeps::new();
200201

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

205205
// Activate all the initial summaries to kick off some work.
@@ -775,7 +775,7 @@ impl RemainingCandidates {
775775
//
776776
// Here we throw out our candidate if it's *compatible*, yet not
777777
// equal, to all previously activated versions.
778-
if let Some((a, _)) = cx.activations.get(&b_id.as_activations_key()) {
778+
if let Some((a, _)) = cx.activations.get(&b_id.activation_key().no_hash()) {
779779
if *a != b {
780780
conflicting_prev_active
781781
.entry(a.package_id())

0 commit comments

Comments
 (0)