Skip to content

Commit c866f48

Browse files
committed
Auto merge of #6776 - Eh2406:resolver-simplification, r=alexcrichton
Resolver: A dep is equivalent to one of the things it can resolve to. This is a series of small changes to the resolver, each one on its own is not worth the cherne, but somehow all together we can add a new optimization rule. The result is that the test in #6283 is no longer exponencial (still a large polynomial, cubick?) and so N can be bumped from 3 to 20. This also means that we pass with all the cases reported in #6258. Resolution is NP-Hard, so we are moving the slow cases around. To reduce the chance that we will be flooded by new bad cases I run the 4 proptests overnight, and they did not find a new exponencial case. I would recommend reviewing this commit by commit. As each change is pretty simple on its own, but the mixed diff is harder to follow. This is submitted as one big PR as that is @alexcrichton's preference. A special thanks to @nex3, our conversation was important in convincing me that several of these changes would be needed even in an eventual PubGrub based system. And, the question "why would PubGrub not have a problem with #6283" was wat crystallized this optimization opportunity in my mind.
2 parents 6bdb9d3 + 91b5a9d commit c866f48

File tree

6 files changed

+339
-112
lines changed

6 files changed

+339
-112
lines changed

src/cargo/core/resolver/conflict_cache.rs

Lines changed: 61 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,41 +18,55 @@ enum ConflictStoreTrie {
1818

1919
impl ConflictStoreTrie {
2020
/// Finds any known set of conflicts, if any,
21-
/// which are activated in `cx` and pass the `filter` specified?
21+
/// which are activated in `cx` and contain `PackageId` specified.
22+
/// If more then one are activated, then it will return
23+
/// one that will allow for the most jump-back.
2224
fn find_conflicting(
2325
&self,
2426
cx: &Context,
2527
must_contain: Option<PackageId>,
26-
) -> Option<&ConflictMap> {
28+
) -> Option<(&ConflictMap, usize)> {
2729
match self {
2830
ConflictStoreTrie::Leaf(c) => {
2931
if must_contain.is_none() {
3032
// `is_conflicting` checks that all the elements are active,
3133
// but we have checked each one by the recursion of this function.
32-
debug_assert!(cx.is_conflicting(None, c));
33-
Some(c)
34+
debug_assert!(cx.is_conflicting(None, c).is_some());
35+
Some((c, 0))
3436
} else {
3537
// We did not find `must_contain`, so we need to keep looking.
3638
None
3739
}
3840
}
3941
ConflictStoreTrie::Node(m) => {
42+
let mut out = None;
4043
for (&pid, store) in must_contain
4144
.map(|f| m.range(..=f))
4245
.unwrap_or_else(|| m.range(..))
4346
{
4447
// If the key is active, then we need to check all of the corresponding subtrie.
45-
if cx.is_active(pid) {
46-
if let Some(o) =
48+
if let Some(age_this) = cx.is_active(pid) {
49+
if let Some((o, age_o)) =
4750
store.find_conflicting(cx, must_contain.filter(|&f| f != pid))
4851
{
49-
return Some(o);
52+
let age = if must_contain == Some(pid) {
53+
// all the results will include `must_contain`
54+
// so the age of must_contain is not relevant to find the best result.
55+
age_o
56+
} else {
57+
std::cmp::max(age_this, age_o)
58+
};
59+
let out_age = out.get_or_insert((o, age)).1;
60+
if out_age > age {
61+
// we found one that can jump-back further so replace the out.
62+
out = Some((o, age));
63+
}
5064
}
5165
}
5266
// Else, if it is not active then there is no way any of the corresponding
5367
// subtrie will be conflicting.
5468
}
55-
None
69+
out
5670
}
5771
}
5872
}
@@ -88,6 +102,28 @@ impl ConflictStoreTrie {
88102
*self = ConflictStoreTrie::Leaf(con)
89103
}
90104
}
105+
106+
fn contains(&self, mut iter: impl Iterator<Item = PackageId>, con: &ConflictMap) -> bool {
107+
match (self, iter.next()) {
108+
(ConflictStoreTrie::Leaf(c), None) => {
109+
if cfg!(debug_assertions) {
110+
let a: Vec<_> = con.keys().collect();
111+
let b: Vec<_> = c.keys().collect();
112+
assert_eq!(a, b);
113+
}
114+
true
115+
}
116+
(ConflictStoreTrie::Leaf(_), Some(_)) => false,
117+
(ConflictStoreTrie::Node(_), None) => false,
118+
(ConflictStoreTrie::Node(m), Some(n)) => {
119+
if let Some(next) = m.get(&n) {
120+
next.contains(iter, con)
121+
} else {
122+
false
123+
}
124+
}
125+
}
126+
}
91127
}
92128

93129
pub(super) struct ConflictCache {
@@ -137,7 +173,9 @@ impl ConflictCache {
137173
}
138174
}
139175
/// Finds any known set of conflicts, if any,
140-
/// which are activated in `cx` and pass the `filter` specified?
176+
/// which are activated in `cx` and contain `PackageId` specified.
177+
/// If more then one are activated, then it will return
178+
/// one that will allow for the most jump-back.
141179
pub fn find_conflicting(
142180
&self,
143181
cx: &Context,
@@ -147,7 +185,8 @@ impl ConflictCache {
147185
let out = self
148186
.con_from_dep
149187
.get(dep)?
150-
.find_conflicting(cx, must_contain);
188+
.find_conflicting(cx, must_contain)
189+
.map(|(c, _)| c);
151190
if cfg!(debug_assertions) {
152191
if let Some(f) = must_contain {
153192
if let Some(c) = &out {
@@ -189,6 +228,18 @@ impl ConflictCache {
189228
.insert(dep.clone());
190229
}
191230
}
231+
232+
/// Check if a conflict was previously added of the form:
233+
/// `dep` is known to be unresolvable if
234+
/// all the `PackageId` entries are activated.
235+
pub fn contains(&self, dep: &Dependency, con: &ConflictMap) -> bool {
236+
if let Some(cst) = self.con_from_dep.get(dep) {
237+
cst.contains(con.keys().cloned(), &con)
238+
} else {
239+
false
240+
}
241+
}
242+
192243
pub fn dependencies_conflicting_with(&self, pid: PackageId) -> Option<&HashSet<Dependency>> {
193244
self.dep_from_pid.get(&pid)
194245
}

src/cargo/core/resolver/context.rs

Lines changed: 82 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use std::collections::{HashMap, HashSet};
2+
use std::num::NonZeroU64;
23
use std::rc::Rc;
34

45
// "ensure" seems to require "bail" be in scope (macro hygiene issue?).
@@ -52,8 +53,47 @@ pub struct Context {
5253
pub warnings: RcList<String>,
5354
}
5455

55-
/// list all the activated versions of a particular crate name from a source
56-
pub type Activations = im_rc::HashMap<(InternedString, SourceId), Rc<Vec<Summary>>>;
56+
/// When backtracking it can be useful to know how far back to go.
57+
/// The `ContextAge` of a `Context` is a monotonically increasing counter of the number
58+
/// of decisions made to get to this state.
59+
/// Several structures store the `ContextAge` when it was added,
60+
/// to be used in `find_candidate` for backtracking.
61+
pub type ContextAge = usize;
62+
63+
/// Find the activated version of a crate based on the name, source, and semver compatibility.
64+
/// By storing this in a hash map we ensure that there is only one
65+
/// semver compatible version of each crate.
66+
/// This all so stores the `ContextAge`.
67+
pub type Activations =
68+
im_rc::HashMap<(InternedString, SourceId, SemverCompatibility), (Summary, ContextAge)>;
69+
70+
/// A type that represents when cargo treats two Versions as compatible.
71+
/// Versions `a` and `b` are compatible if their left-most nonzero digit is the
72+
/// same.
73+
#[derive(Clone, Copy, Eq, PartialEq, Hash)]
74+
pub enum SemverCompatibility {
75+
Major(NonZeroU64),
76+
Minor(NonZeroU64),
77+
Patch(u64),
78+
}
79+
80+
impl From<&semver::Version> for SemverCompatibility {
81+
fn from(ver: &semver::Version) -> Self {
82+
if let Some(m) = NonZeroU64::new(ver.major) {
83+
return SemverCompatibility::Major(m);
84+
}
85+
if let Some(m) = NonZeroU64::new(ver.minor) {
86+
return SemverCompatibility::Minor(m);
87+
}
88+
SemverCompatibility::Patch(ver.patch)
89+
}
90+
}
91+
92+
impl PackageId {
93+
pub fn as_activations_key(&self) -> (InternedString, SourceId, SemverCompatibility) {
94+
(self.name(), self.source_id(), self.version().into())
95+
}
96+
}
5797

5898
impl Context {
5999
pub fn new(check_public_visible_dependencies: bool) -> Context {
@@ -78,22 +118,28 @@ impl Context {
78118
/// Returns `true` if this summary with the given method is already activated.
79119
pub fn flag_activated(&mut self, summary: &Summary, method: &Method<'_>) -> CargoResult<bool> {
80120
let id = summary.package_id();
81-
let prev = self
82-
.activations
83-
.entry((id.name(), id.source_id()))
84-
.or_insert_with(|| Rc::new(Vec::new()));
85-
if !prev.iter().any(|c| c == summary) {
86-
self.resolve_graph.push(GraphNode::Add(id));
87-
if let Some(link) = summary.links() {
88-
ensure!(
89-
self.links.insert(link, id).is_none(),
90-
"Attempting to resolve a dependency with more then one crate with the \
91-
links={}.\nThis will not build as is. Consider rebuilding the .lock file.",
92-
&*link
121+
let age: ContextAge = self.age();
122+
match self.activations.entry(id.as_activations_key()) {
123+
im_rc::hashmap::Entry::Occupied(o) => {
124+
debug_assert_eq!(
125+
&o.get().0,
126+
summary,
127+
"cargo does not allow two semver compatible versions"
93128
);
94129
}
95-
Rc::make_mut(prev).push(summary.clone());
96-
return Ok(false);
130+
im_rc::hashmap::Entry::Vacant(v) => {
131+
self.resolve_graph.push(GraphNode::Add(id));
132+
if let Some(link) = summary.links() {
133+
ensure!(
134+
self.links.insert(link, id).is_none(),
135+
"Attempting to resolve a dependency with more then one crate with the \
136+
links={}.\nThis will not build as is. Consider rebuilding the .lock file.",
137+
&*link
138+
);
139+
}
140+
v.insert((summary.clone(), age));
141+
return Ok(false);
142+
}
97143
}
98144
debug!("checking if {} is already activated", summary.package_id());
99145
let (features, use_default) = match *method {
@@ -149,31 +195,37 @@ impl Context {
149195
Ok(deps)
150196
}
151197

152-
pub fn prev_active(&self, dep: &Dependency) -> &[Summary] {
153-
self.activations
154-
.get(&(dep.package_name(), dep.source_id()))
155-
.map(|v| &v[..])
156-
.unwrap_or(&[])
198+
/// Returns the `ContextAge` of this `Context`.
199+
/// For now we use (len of activations) as the age.
200+
/// See the `ContextAge` docs for more details.
201+
pub fn age(&self) -> ContextAge {
202+
self.activations.len()
157203
}
158204

159-
pub fn is_active(&self, id: PackageId) -> bool {
205+
/// If the package is active returns the `ContextAge` when it was added
206+
pub fn is_active(&self, id: PackageId) -> Option<ContextAge> {
160207
self.activations
161-
.get(&(id.name(), id.source_id()))
162-
.map(|v| v.iter().any(|s| s.package_id() == id))
163-
.unwrap_or(false)
208+
.get(&id.as_activations_key())
209+
.and_then(|(s, l)| if s.package_id() == id { Some(*l) } else { None })
164210
}
165211

166212
/// Checks whether all of `parent` and the keys of `conflicting activations`
167213
/// are still active.
214+
/// If so returns the `ContextAge` when the newest one was added.
168215
pub fn is_conflicting(
169216
&self,
170217
parent: Option<PackageId>,
171218
conflicting_activations: &ConflictMap,
172-
) -> bool {
173-
conflicting_activations
174-
.keys()
175-
.chain(parent.as_ref())
176-
.all(|&id| self.is_active(id))
219+
) -> Option<usize> {
220+
let mut max = 0;
221+
for &id in conflicting_activations.keys().chain(parent.as_ref()) {
222+
if let Some(age) = self.is_active(id) {
223+
max = std::cmp::max(max, age);
224+
} else {
225+
return None;
226+
}
227+
}
228+
Some(max)
177229
}
178230

179231
/// Returns all dependencies and the features we want from them.

0 commit comments

Comments
 (0)