Skip to content

Commit 4c8ab5e

Browse files
committed
remove the O(n) lookup
1 parent e1ff48b commit 4c8ab5e

File tree

4 files changed

+91
-37
lines changed

4 files changed

+91
-37
lines changed

src/cargo/core/resolver/conflict_cache.rs

Lines changed: 87 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,81 @@
1-
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
1+
use std::collections::{BTreeMap, HashMap, HashSet};
22

33
use super::types::ConflictReason;
44
use core::resolver::Context;
55
use core::{Dependency, PackageId};
66

7+
enum ConflictStore {
8+
Con(BTreeMap<PackageId, ConflictReason>),
9+
Map(HashMap<PackageId, ConflictStore>),
10+
}
11+
12+
impl ConflictStore {
13+
/// Finds any known set of conflicts, if any,
14+
/// which are activated in `cx` and pass the `filter` specified?
15+
pub fn find_conflicting<F>(
16+
&self,
17+
cx: &Context,
18+
filter: &F,
19+
) -> Option<&BTreeMap<PackageId, ConflictReason>>
20+
where
21+
for<'r> F: Fn(&'r &BTreeMap<PackageId, ConflictReason>) -> bool,
22+
{
23+
match self {
24+
ConflictStore::Con(c) => {
25+
if filter(&c) {
26+
Some(c)
27+
} else {
28+
None
29+
}
30+
}
31+
ConflictStore::Map(m) => {
32+
for (pid, store) in m {
33+
if cx.is_active(pid) {
34+
if let Some(o) = store.find_conflicting(cx, filter) {
35+
return Some(o);
36+
}
37+
}
38+
}
39+
None
40+
}
41+
}
42+
}
43+
}
44+
745
pub(super) struct ConflictCache {
846
// `con_from_dep` is a cache of the reasons for each time we
947
// backtrack. For example after several backtracks we may have:
1048
//
11-
// con_from_dep[`foo = "^1.0.2"`] = vec![
12-
// map!{`foo=1.0.1`: Semver},
13-
// map!{`foo=1.0.0`: Semver},
14-
// ];
49+
// con_from_dep[`foo = "^1.0.2"`] = map!{
50+
// `foo=1.0.1`: map!{`foo=1.0.1`: Semver},
51+
// `foo=1.0.0`: map!{`foo=1.0.0`: Semver},
52+
// };
1553
//
1654
// This can be read as "we cannot find a candidate for dep `foo = "^1.0.2"`
1755
// if either `foo=1.0.1` OR `foo=1.0.0` are activated".
1856
//
1957
// Another example after several backtracks we may have:
2058
//
21-
// con_from_dep[`foo = ">=0.8.2, <=0.9.3"`] = vec![
22-
// map!{`foo=0.8.1`: Semver, `foo=0.9.4`: Semver},
23-
// ];
59+
// con_from_dep[`foo = ">=0.8.2, <=0.9.3"`] = map!{
60+
// `foo=0.8.1`: map!{
61+
// `foo=0.9.4`: map!{`foo=0.8.1`: Semver, `foo=0.9.4`: Semver},
62+
// }
63+
// };
2464
//
2565
// This can be read as "we cannot find a candidate for dep `foo = ">=0.8.2,
2666
// <=0.9.3"` if both `foo=0.8.1` AND `foo=0.9.4` are activated".
2767
//
2868
// This is used to make sure we don't queue work we know will fail. See the
2969
// discussion in https://github.com/rust-lang/cargo/pull/5168 for why this
30-
// is so important, and there can probably be a better data structure here
31-
// but for now this works well enough!
70+
// is so important. The nested HashMaps act as a kind of btree, that lets us
71+
// look up a witch entry's are still active without
72+
// linearly scanning through the full list.
3273
//
3374
// Also, as a final note, this map is *not* ever removed from. This remains
3475
// as a global cache which we never delete from. Any entry in this map is
3576
// unconditionally true regardless of our resolution history of how we got
3677
// here.
37-
con_from_dep: HashMap<Dependency, BTreeSet<BTreeMap<PackageId, ConflictReason>>>,
78+
con_from_dep: HashMap<Dependency, ConflictStore>,
3879
// `dep_from_pid` is an inverse-index of `con_from_dep`.
3980
// For every `PackageId` this lists the `Dependency`s that mention it in `dep_from_pid`.
4081
dep_from_pid: HashMap<PackageId, HashSet<Dependency>>,
@@ -56,14 +97,9 @@ impl ConflictCache {
5697
filter: F,
5798
) -> Option<&BTreeMap<PackageId, ConflictReason>>
5899
where
59-
for<'r> F: FnMut(&'r &BTreeMap<PackageId, ConflictReason>) -> bool,
100+
for<'r> F: Fn(&'r &BTreeMap<PackageId, ConflictReason>) -> bool,
60101
{
61-
self.con_from_dep
62-
.get(dep)?
63-
.iter()
64-
.rev() // more general cases are normally found letter. So start the search there.
65-
.filter(filter)
66-
.find(|conflicting| cx.is_conflicting(None, conflicting))
102+
self.con_from_dep.get(dep)?.find_conflicting(cx, &filter)
67103
}
68104
pub fn conflicting(
69105
&self,
@@ -77,25 +113,43 @@ impl ConflictCache {
77113
/// `dep` is known to be unresolvable if
78114
/// all the `PackageId` entries are activated
79115
pub fn insert(&mut self, dep: &Dependency, con: &BTreeMap<PackageId, ConflictReason>) {
80-
let past = self
116+
let mut past = self
81117
.con_from_dep
82118
.entry(dep.clone())
83-
.or_insert_with(BTreeSet::new);
84-
if !past.contains(con) {
85-
trace!(
86-
"{} = \"{}\" adding a skip {:?}",
87-
dep.package_name(),
88-
dep.version_req(),
89-
con
90-
);
91-
past.insert(con.clone());
92-
for c in con.keys() {
93-
self.dep_from_pid
94-
.entry(c.clone())
95-
.or_insert_with(HashSet::new)
96-
.insert(dep.clone());
119+
.or_insert_with(|| ConflictStore::Map(HashMap::new()));
120+
121+
for pid in con.keys() {
122+
match past {
123+
ConflictStore::Con(_) => {
124+
// We already have a subset of this in the ConflictStore
125+
return;
126+
}
127+
ConflictStore::Map(p) => {
128+
past = p
129+
.entry(pid.clone())
130+
.or_insert_with(|| ConflictStore::Map(HashMap::new()));
131+
}
97132
}
98133
}
134+
135+
if let ConflictStore::Con(_) = past {
136+
// We already have this in the ConflictStore
137+
return;
138+
}
139+
*past = ConflictStore::Con(con.clone());
140+
trace!(
141+
"{} = \"{}\" adding a skip {:?}",
142+
dep.package_name(),
143+
dep.version_req(),
144+
con
145+
);
146+
147+
for c in con.keys() {
148+
self.dep_from_pid
149+
.entry(c.clone())
150+
.or_insert_with(HashSet::new)
151+
.insert(dep.clone());
152+
}
99153
}
100154
pub fn dependencies_conflicting_with(&self, pid: &PackageId) -> Option<&HashSet<Dependency>> {
101155
self.dep_from_pid.get(pid)

src/cargo/core/resolver/context.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ impl Context {
133133
.unwrap_or(&[])
134134
}
135135

136-
fn is_active(&self, id: &PackageId) -> bool {
136+
pub fn is_active(&self, id: &PackageId) -> bool {
137137
self.activations
138138
.get(&(id.name(), id.source_id().clone()))
139139
.map(|v| v.iter().any(|s| s.package_id() == id))

src/cargo/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#![cfg_attr(test, deny(warnings))]
2-
2+
#![feature(nll)]
33
// Clippy isn't enforced by CI, and know that @alexcrichton isn't a fan :)
44
#![cfg_attr(feature = "cargo-clippy", allow(boxed_local))] // bug rust-lang-nursery/rust-clippy#1123
55
#![cfg_attr(feature = "cargo-clippy", allow(cyclomatic_complexity))] // large project

tests/testsuite/resolve.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1175,8 +1175,8 @@ fn hard_equality() {
11751175

11761176
#[test]
11771177
fn large_conflict_cache() {
1178-
let mut input = vec![
1179-
pkg!(("last", "0.0.0") => [dep("bad")]) // just to make sure last is less constrained
1178+
let mut input = vec![
1179+
pkg!(("last", "0.0.0") => [dep("bad")]), // just to make sure last is less constrained
11801180
];
11811181
let mut root_deps = vec![dep("last")];
11821182
const NUM_VERSIONS: u8 = 3;

0 commit comments

Comments
 (0)