Skip to content

Commit 3a06fde

Browse files
committed
Auto merge of #5168 - Eh2406:faster_resolver, r=alexcrichton
Faster resolver: Cache past conflicting_activations, prevent doing the same work repeatedly. This work is inspired by @alexcrichton's [comment](#4066 (comment)) that a slow resolver can be caused by all versions of a dependency being yanked. Witch stuck in my brain as I did not understand why it would happen. If a dependency has no candidates then it will be the most constrained and will trigger backtracking in the next tick. Eventually I found a reproducible test case. If the bad dependency is deep in the tree of dependencies then we activate and backtrack `O(versions^depth)` times. Even tho it is fast to identify the problem that is a lot of work. **The set up:** 1. Every time we backtrack cache the (dep, `conflicting_activations`). 2. Build on the work in #5000, Fail to activate if any of its dependencies will just backtrack to this frame. I.E. for each dependency check if any of its cached `conflicting_activations` are already all activated. If so we can just skip to the next candidate. We also add that bad `conflicting_activations` to our set of `conflicting_activations`, so that we can... **The pay off:** If we fail to find any candidates that we can activate in lite of 2, then we cannot be activated in this context, add our (dep, `conflicting_activations`) to the cache so that next time our parent will not bother trying us. I hear you saying "but the error messages, what about the error messages?" So if we are at the end `!has_another` then we disable this optimization. After we mark our dep as being not activatable then we activate anyway. It won't resolve but it will have the same error message as before this PR. If we have been activated for the error messages then skip straight to the last candidate, as that is the only backtrack that will end with the user. I added a test in the vain of #4834. With the old code the time to run was `O(BRANCHING_FACTOR ^ DEPTH)` and took ~3min with DEPTH = 10; BRANCHING_FACTOR = 5; with the new code it runs almost instantly with 200 and 100.
2 parents 3cfb23b + 58d7cf1 commit 3a06fde

File tree

2 files changed

+205
-38
lines changed

2 files changed

+205
-38
lines changed

src/cargo/core/resolver/mod.rs

+127-38
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,7 @@ fn activate(cx: &mut Context,
455455
let deps = cx.build_deps(registry, parent, &candidate, method)?;
456456
let frame = DepsFrame {
457457
parent: candidate,
458+
just_for_error_messages: false,
458459
remaining_siblings: RcVecIter::new(Rc::new(deps)),
459460
};
460461
Ok(Some((frame, now.elapsed())))
@@ -501,6 +502,7 @@ impl<T> Iterator for RcVecIter<T> where T: Clone {
501502
#[derive(Clone)]
502503
struct DepsFrame {
503504
parent: Summary,
505+
just_for_error_messages: bool,
504506
remaining_siblings: RcVecIter<DepInfo>,
505507
}
506508

@@ -520,6 +522,7 @@ impl DepsFrame {
520522

521523
impl PartialEq for DepsFrame {
522524
fn eq(&self, other: &DepsFrame) -> bool {
525+
self.just_for_error_messages == other.just_for_error_messages &&
523526
self.min_candidates() == other.min_candidates()
524527
}
525528
}
@@ -534,14 +537,16 @@ impl PartialOrd for DepsFrame {
534537

535538
impl Ord for DepsFrame {
536539
fn cmp(&self, other: &DepsFrame) -> Ordering {
537-
// the frame with the sibling that has the least number of candidates
538-
// needs to get the bubbled up to the top of the heap we use below, so
539-
// reverse the order of the comparison here.
540-
other.min_candidates().cmp(&self.min_candidates())
540+
self.just_for_error_messages.cmp(&other.just_for_error_messages).then_with(||
541+
// the frame with the sibling that has the least number of candidates
542+
// needs to get bubbled up to the top of the heap we use below, so
543+
// reverse comparison here.
544+
self.min_candidates().cmp(&other.min_candidates()).reverse()
545+
)
541546
}
542547
}
543548

544-
#[derive(Clone, PartialOrd, Ord, PartialEq, Eq)]
549+
#[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq)]
545550
enum ConflictReason {
546551
Semver,
547552
Links(String),
@@ -763,7 +768,6 @@ impl RemainingCandidates {
763768
.ok_or_else(|| self.conflicting_prev_active.clone())
764769
}
765770
}
766-
767771
/// Recursively activates the dependencies for `top`, in depth-first order,
768772
/// backtracking across possible candidates for each dependency as necessary.
769773
///
@@ -784,6 +788,18 @@ fn activate_deps_loop(
784788
// use (those with more candidates).
785789
let mut backtrack_stack = Vec::new();
786790
let mut remaining_deps = BinaryHeap::new();
791+
// `past_conflicting_activations`is a cache of the reasons for each time we backtrack.
792+
// for example after several backtracks we may have:
793+
// past_conflicting_activations[`foo = "^1.0.2"`] = vac![map!{`foo=1.0.1`: Semver}, map!{`foo=1.0.1`: Semver}];
794+
// This can be read as "we cannot find a candidate for dep `foo = "^1.0.2"` if either
795+
// `foo=1.0.1` OR `foo=1.0.0` are activated"
796+
// for another example after several backtracks we may have:
797+
// past_conflicting_activations[`foo = ">=0.8.2, <=0.9.3"`] = vac![map!{`foo=0.8.1`: Semver, `foo=0.9.4`: Semver}];
798+
// This can be read as "we cannot find a candidate for dep `foo = ">=0.8.2, <=0.9.3"` if both
799+
// `foo=0.8.1` AND `foo=0.9.4` are activated" (better data structures are welcome but this works for now.)
800+
// This is used to make sure we don't queue work we know will fail.
801+
// See the discussion in https://github.com/rust-lang/cargo/pull/5168 for why this is so important
802+
let mut past_conflicting_activations: HashMap<Dependency, Vec<HashMap<PackageId, ConflictReason>>> = HashMap::new();
787803
for &(ref summary, ref method) in summaries {
788804
debug!("initial activation: {}", summary.package_id());
789805
let candidate = Candidate {
@@ -838,6 +854,8 @@ fn activate_deps_loop(
838854
}
839855
}
840856

857+
let just_here_for_the_error_messages = deps_frame.just_for_error_messages;
858+
841859
let frame = match deps_frame.remaining_siblings.next() {
842860
Some(sibling) => {
843861
let parent = Summary::clone(&deps_frame.parent);
@@ -852,9 +870,30 @@ fn activate_deps_loop(
852870
trace!("{}[{}]>{} {} candidates", parent.name(), cur, dep.name(), candidates.len());
853871
trace!("{}[{}]>{} {} prev activations", parent.name(), cur, dep.name(), cx.prev_active(&dep).len());
854872

873+
let just_here_for_the_error_messages = just_here_for_the_error_messages
874+
&& past_conflicting_activations
875+
.get(&dep)
876+
.and_then(|past_bad| {
877+
past_bad.iter().find(|conflicting| {
878+
conflicting
879+
.iter()
880+
// note: a lot of redundant work in is_active for similar debs
881+
.all(|(con, _)| cx.is_active(con))
882+
})
883+
})
884+
.is_some();
885+
855886
let mut remaining_candidates = RemainingCandidates::new(&candidates);
856887
let mut successfully_activated = false;
888+
// `conflicting_activations` stores all the reasons we were unable to activate candidates.
889+
// One of these reasons will have to go away for backtracking to find a place to restart.
890+
// It is also the list of things to explain in the error message if we fail to resolve.
857891
let mut conflicting_activations = HashMap::new();
892+
// When backtracking we don't fully update `conflicting_activations` especially for the
893+
// cases that we didn't make a backtrack frame in the first place.
894+
// This `backtracked` var stores whether we are continuing from a restored backtrack frame
895+
// so that we can skip caching `conflicting_activations` in `past_conflicting_activations`
896+
let mut backtracked = false;
858897

859898
while !successfully_activated {
860899
let next = remaining_candidates.next(cx.prev_active(&dep), &cx.links);
@@ -875,20 +914,37 @@ fn activate_deps_loop(
875914
conflicting_activations.extend(conflicting);
876915
// This dependency has no valid candidate. Backtrack until we
877916
// find a dependency that does have a candidate to try, and try
878-
// to activate that one. This resets the `remaining_deps` to
879-
// their state at the found level of the `backtrack_stack`.
917+
// to activate that one.
880918
trace!("{}[{}]>{} -- no candidates", parent.name(), cur, dep.name());
919+
920+
if !just_here_for_the_error_messages && !backtracked {
921+
// if `just_here_for_the_error_messages` then skip as it is already known to be bad.
922+
// if `backtracked` then `conflicting_activations` may not be complete so skip.
923+
let past = past_conflicting_activations.entry(dep.clone()).or_insert_with(Vec::new);
924+
if !past.contains(&conflicting_activations) {
925+
trace!("{}[{}]>{} adding a skip {:?}", parent.name(), cur, dep.name(), conflicting_activations);
926+
past.push(conflicting_activations.clone());
927+
}
928+
}
929+
881930
find_candidate(
882931
&mut backtrack_stack,
883-
&mut cx,
884-
&mut remaining_deps,
885-
&mut parent,
886-
&mut cur,
887-
&mut dep,
888-
&mut features,
889-
&mut remaining_candidates,
890-
&mut conflicting_activations,
891-
).ok_or_else(|| {
932+
&parent,
933+
&conflicting_activations,
934+
).map(|(candidate, has_another, frame)| {
935+
// This resets the `remaining_deps` to
936+
// their state at the found level of the `backtrack_stack`.
937+
cur = frame.cur;
938+
cx = frame.context_backup;
939+
remaining_deps = frame.deps_backup;
940+
remaining_candidates = frame.remaining_candidates;
941+
parent = frame.parent;
942+
dep = frame.dep;
943+
features = frame.features;
944+
conflicting_activations = frame.conflicting_activations;
945+
backtracked = true;
946+
(candidate, has_another)
947+
}).ok_or_else(|| {
892948
activation_error(
893949
&cx,
894950
registry.registry,
@@ -901,6 +957,10 @@ fn activate_deps_loop(
901957
})
902958
})?;
903959

960+
if just_here_for_the_error_messages && !backtracked && has_another {
961+
continue
962+
}
963+
904964
// We have a candidate. Clone a `BacktrackFrame`
905965
// so we can add it to the `backtrack_stack` if activation succeeds.
906966
// We clone now in case `activate` changes `cx` and then fails.
@@ -919,6 +979,7 @@ fn activate_deps_loop(
919979
None
920980
};
921981

982+
let pid = candidate.summary.package_id().clone();
922983
let method = Method::Required {
923984
dev_deps: false,
924985
features: &features,
@@ -930,8 +991,50 @@ fn activate_deps_loop(
930991
successfully_activated = res.is_ok();
931992

932993
match res {
933-
Ok(Some((frame, dur))) => {
934-
remaining_deps.push(frame);
994+
Ok(Some((mut frame, dur))) => {
995+
// at this point we have technical already activated
996+
// but we may want to scrap it if it is not going to end well
997+
let mut has_past_conflicting_dep = just_here_for_the_error_messages;
998+
if !has_past_conflicting_dep {
999+
if let Some(conflicting) = frame.remaining_siblings.clone().filter_map(|(_, (deb, _, _))| {
1000+
past_conflicting_activations.get(&deb).and_then(|past_bad| {
1001+
// for each dependency check all of its cashed conflicts
1002+
past_bad.iter().find(|conflicting| {
1003+
conflicting
1004+
.iter()
1005+
// note: a lot of redundant work in is_active for similar debs
1006+
.all(|(con, _)| cx.is_active(con))
1007+
})
1008+
})
1009+
}).next() {
1010+
// if any of them match than it will just backtrack to us
1011+
// so let's save the effort.
1012+
conflicting_activations.extend(conflicting.clone());
1013+
has_past_conflicting_dep = true;
1014+
}
1015+
}
1016+
if !has_another && has_past_conflicting_dep && !backtracked {
1017+
// we have not activated ANY candidates and
1018+
// we are out of choices so add it to the cache
1019+
// so our parent will know that we don't work
1020+
let past = past_conflicting_activations.entry(dep.clone()).or_insert_with(Vec::new);
1021+
if !past.contains(&conflicting_activations) {
1022+
trace!("{}[{}]>{} adding a meta-skip {:?}", parent.name(), cur, dep.name(), conflicting_activations);
1023+
past.push(conflicting_activations.clone());
1024+
}
1025+
}
1026+
// if not has_another we we activate for the better error messages
1027+
frame.just_for_error_messages = has_past_conflicting_dep;
1028+
if !has_past_conflicting_dep || (!has_another && (just_here_for_the_error_messages || find_candidate(
1029+
&mut backtrack_stack.clone(),
1030+
&parent,
1031+
&conflicting_activations,
1032+
).is_none())) {
1033+
remaining_deps.push(frame);
1034+
} else {
1035+
trace!("{}[{}]>{} skipping {} ", parent.name(), cur, dep.name(), pid.version());
1036+
successfully_activated = false;
1037+
}
9351038
deps_time += dur;
9361039
}
9371040
Ok(None) => (),
@@ -968,17 +1071,11 @@ fn activate_deps_loop(
9681071
/// If the outcome could differ, resets `cx` and `remaining_deps` to that
9691072
/// level and returns the next candidate.
9701073
/// If all candidates have been exhausted, returns None.
971-
fn find_candidate(
1074+
fn find_candidate<'a>(
9721075
backtrack_stack: &mut Vec<BacktrackFrame>,
973-
cx: &mut Context,
974-
remaining_deps: &mut BinaryHeap<DepsFrame>,
975-
parent: &mut Summary,
976-
cur: &mut usize,
977-
dep: &mut Dependency,
978-
features: &mut Rc<Vec<String>>,
979-
remaining_candidates: &mut RemainingCandidates,
980-
conflicting_activations: &mut HashMap<PackageId, ConflictReason>,
981-
) -> Option<(Candidate, bool)> {
1076+
parent: &Summary,
1077+
conflicting_activations: &HashMap<PackageId, ConflictReason>,
1078+
) -> Option<(Candidate, bool, BacktrackFrame)> {
9821079
while let Some(mut frame) = backtrack_stack.pop() {
9831080
let next= frame.remaining_candidates.next(frame.context_backup.prev_active(&frame.dep), &frame.context_backup.links);
9841081
if frame.context_backup.is_active(parent.package_id())
@@ -990,15 +1087,7 @@ fn find_candidate(
9901087
continue;
9911088
}
9921089
if let Ok((candidate, has_another)) = next {
993-
*cur = frame.cur;
994-
*cx = frame.context_backup;
995-
*remaining_deps = frame.deps_backup;
996-
*parent = frame.parent;
997-
*dep = frame.dep;
998-
*features = frame.features;
999-
*remaining_candidates = frame.remaining_candidates;
1000-
*conflicting_activations = frame.conflicting_activations;
1001-
return Some((candidate, has_another));
1090+
return Some((candidate, has_another, frame));
10021091
}
10031092
}
10041093
None

tests/testsuite/resolve.rs

+78
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,84 @@ fn resolving_with_constrained_sibling_backtrack_parent() {
468468
("constrained", "1.0.0")])));
469469
}
470470

471+
#[test]
472+
fn resolving_with_many_equivalent_backtracking() {
473+
let mut reglist = Vec::new();
474+
475+
const DEPTH: usize = 200;
476+
const BRANCHING_FACTOR: usize = 100;
477+
478+
// Each level depends on the next but the last level does not exist.
479+
// Without cashing we need to test every path to the last level O(BRANCHING_FACTOR ^ DEPTH)
480+
// and this test will time out. With cashing we need to discover that none of these
481+
// can be activated O(BRANCHING_FACTOR * DEPTH)
482+
for l in 0..DEPTH {
483+
let name = format!("level{}", l);
484+
let next = format!("level{}", l + 1);
485+
for i in 1..BRANCHING_FACTOR {
486+
let vsn = format!("1.0.{}", i);
487+
reglist.push(pkg!((name.as_str(), vsn.as_str()) => [dep_req(next.as_str(), "*")]));
488+
}
489+
}
490+
491+
let reg = registry(reglist.clone());
492+
493+
let res = resolve(&pkg_id("root"), vec![
494+
dep_req("level0", "*"),
495+
], &reg);
496+
497+
assert!(res.is_err());
498+
499+
// It is easy to write code that quickly returns an error.
500+
// Lets make sure we can find a good answer if it is there.
501+
reglist.push(pkg!(("level0", "1.0.0")));
502+
503+
let reg = registry(reglist.clone());
504+
505+
let res = resolve(&pkg_id("root"), vec![
506+
dep_req("level0", "*"),
507+
], &reg).unwrap();
508+
509+
assert_that(&res, contains(names(&[("root", "1.0.0"),
510+
("level0", "1.0.0")])));
511+
512+
// Make sure we have not special case no candidates.
513+
reglist.push(pkg!(("constrained", "1.1.0")));
514+
reglist.push(pkg!(("constrained", "1.0.0")));
515+
reglist.push(pkg!((format!("level{}", DEPTH).as_str(), "1.0.0") => [dep_req("constrained", "=1.0.0")]));
516+
517+
let reg = registry(reglist.clone());
518+
519+
let res = resolve(&pkg_id("root"), vec![
520+
dep_req("level0", "*"),
521+
dep_req("constrained", "*"),
522+
], &reg).unwrap();
523+
524+
assert_that(&res, contains(names(&[("root", "1.0.0"),
525+
("level0", "1.0.0"),
526+
("constrained", "1.1.0")])));
527+
528+
let reg = registry(reglist.clone());
529+
530+
let res = resolve(&pkg_id("root"), vec![
531+
dep_req("level0", "1.0.1"),
532+
dep_req("constrained", "*"),
533+
], &reg).unwrap();
534+
535+
assert_that(&res, contains(names(&[("root", "1.0.0"),
536+
(format!("level{}", DEPTH).as_str(), "1.0.0"),
537+
("constrained", "1.0.0")])));
538+
539+
let reg = registry(reglist.clone());
540+
541+
let res = resolve(&pkg_id("root"), vec![
542+
dep_req("level0", "1.0.1"),
543+
dep_req("constrained", "1.1.0"),
544+
], &reg);
545+
546+
assert!(res.is_err());
547+
}
548+
471549
#[test]
472550
fn resolving_with_constrained_sibling_backtrack_activation() {
473551
// It makes sense to resolve most-constrained deps first, but

0 commit comments

Comments
 (0)