Skip to content

Commit e670baf

Browse files
committed
Return and track affected and culprit on conflicts
Whenever we either discard a version due to its dependencies or perform conflict resolution, we return the last conflict that led to discarding them. In cargo, we use this information for prioritization, which speeds up resolution (`cargo run -r -- -m pub --with-solana --filter solana-archiver-lib -t 16` goes from 90s to 20s on my machine). Configurations that are noticeably slower for the solana test case: * All incompatibilities unit propagation * Only the last root cause in unit propagation * No incompatibilities from unit propagation * No incompatibilities from `add_version` * Only affect counts (without culprit counts) * Backtracking with the same heuristic as astral-sh/uv#9843 (backtracking once after affected hits 5) In uv, we use this to re-prioritize and backtrack when a package decision accumulated to many conflicts. Since we have our own solver loop, we add the incompatibility to our own tracking instead. Built on pubgrub-rs#291 ## Benchmarks Main: ``` index commit hash: 82086e46740d7a9303216bfac093e7268a95121f index commit time: 2024-11-30T18:18:14Z index size: 32 solana in index: 32 Pub CPU time: 1215.49s == 20.26min Cargo CPU time: skipped Cargo check lock CPU time: skipped Pub check lock CPU time: skipped Wall time: 80.58s == 1.34min ``` With pubgrub-rs#291: ``` index commit hash: 82086e46740d7a9303216bfac093e7268a95121f index commit time: 2024-11-30T18:18:14Z index size: 32 solana in index: 32 Pub CPU time: 467.73s == 7.80min Cargo CPU time: skipped Cargo check lock CPU time: skipped Pub check lock CPU time: skipped Wall time: 34.76s == 0.58min ``` This PR: ``` index commit hash: 82086e46740d7a9303216bfac093e7268a95121f index commit time: 2024-11-30T18:18:14Z index size: 32 solana in index: 32 Pub CPU time: 271.79s == 4.53min Cargo CPU time: skipped Cargo check lock CPU time: skipped Pub check lock CPU time: skipped Wall time: 20.17s == 0.34min ```
1 parent 0a9ace7 commit e670baf

File tree

7 files changed

+171
-52
lines changed

7 files changed

+171
-52
lines changed

examples/caching_dependency_provider.rs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22

33
use std::cell::RefCell;
44

5-
use pubgrub::{resolve, Dependencies, DependencyProvider, OfflineDependencyProvider, Ranges};
5+
use pubgrub::{
6+
resolve, Dependencies, DependencyProvider, OfflineDependencyProvider,
7+
PackageResolutionStatistics, Ranges,
8+
};
69

710
type NumVS = Ranges<u32>;
811

@@ -57,8 +60,14 @@ impl<DP: DependencyProvider<M = String>> DependencyProvider for CachingDependenc
5760

5861
type Priority = DP::Priority;
5962

60-
fn prioritize(&self, package: &DP::P, ranges: &DP::VS) -> Self::Priority {
61-
self.remote_dependencies.prioritize(package, ranges)
63+
fn prioritize(
64+
&self,
65+
package: &Self::P,
66+
range: &Self::VS,
67+
package_statistics: &PackageResolutionStatistics,
68+
) -> Self::Priority {
69+
self.remote_dependencies
70+
.prioritize(package, range, package_statistics)
6271
}
6372

6473
type Err = DP::Err;

src/internal/core.rs

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ use std::collections::HashSet as Set;
77
use std::sync::Arc;
88

99
use crate::internal::{
10-
Arena, DecisionLevel, HashArena, Id, IncompDpId, Incompatibility, PartialSolution, Relation,
11-
SatisfierSearch, SmallVec,
10+
Arena, DecisionLevel, HashArena, Id, IncompDpId, IncompId, Incompatibility, PartialSolution,
11+
Relation, SatisfierSearch, SmallVec,
1212
};
1313
use crate::{DependencyProvider, DerivationTree, Map, NoSolutionError, VersionSet};
1414

@@ -79,7 +79,7 @@ impl<DP: DependencyProvider> State<DP> {
7979
package: Id<DP::P>,
8080
version: DP::V,
8181
dependencies: impl IntoIterator<Item = (DP::P, DP::VS)>,
82-
) {
82+
) -> Option<IncompId<DP::P, DP::VS, DP::M>> {
8383
let dep_incompats =
8484
self.add_incompatibility_from_dependencies(package, version.clone(), dependencies);
8585
self.partial_solution.add_package_version_dependencies(
@@ -124,8 +124,16 @@ impl<DP: DependencyProvider> State<DP> {
124124

125125
/// Unit propagation is the core mechanism of the solving algorithm.
126126
/// CF <https://github.com/dart-lang/pub/blob/master/doc/solver.md#unit-propagation>
127+
///
128+
/// For each package with a satisfied incompatibility, returns the package and the root cause
129+
/// incompatibility.
127130
#[cold]
128-
pub fn unit_propagation(&mut self, package: Id<DP::P>) -> Result<(), NoSolutionError<DP>> {
131+
#[allow(clippy::type_complexity)] // Type definitions don't support impl trait.
132+
pub fn unit_propagation(
133+
&mut self,
134+
package: Id<DP::P>,
135+
) -> Result<Vec<(Id<DP::P>, IncompDpId<DP>)>, NoSolutionError<DP>> {
136+
let mut root_causes = Vec::new();
129137
self.unit_propagation_buffer.clear();
130138
self.unit_propagation_buffer.push(package);
131139
while let Some(current_package) = self.unit_propagation_buffer.pop() {
@@ -183,6 +191,7 @@ impl<DP: DependencyProvider> State<DP> {
183191
.map_err(|terminal_incompat_id| {
184192
self.build_derivation_tree(terminal_incompat_id)
185193
})?;
194+
root_causes.push((package, root_cause));
186195
self.unit_propagation_buffer.clear();
187196
self.unit_propagation_buffer.push(package_almost);
188197
// Add to the partial solution with incompat as cause.
@@ -198,7 +207,7 @@ impl<DP: DependencyProvider> State<DP> {
198207
}
199208
}
200209
// If there are no more changed packages, unit propagation is done.
201-
Ok(())
210+
Ok(root_causes)
202211
}
203212

204213
/// Return the root cause or the terminal incompatibility.

src/internal/partial_solution.rs

Lines changed: 43 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,7 @@ impl<DP: DependencyProvider> PartialSolution<DP> {
295295
#[cold]
296296
pub fn pick_highest_priority_pkg(
297297
&mut self,
298-
prioritizer: impl Fn(Id<DP::P>, &DP::VS) -> DP::Priority,
298+
mut prioritizer: impl FnMut(Id<DP::P>, &DP::VS) -> DP::Priority,
299299
) -> Option<Id<DP::P>> {
300300
let check_all = self.changed_this_decision_level
301301
== self.current_decision_level.0.saturating_sub(1) as usize;
@@ -331,7 +331,21 @@ impl<DP: DependencyProvider> PartialSolution<DP> {
331331
.map(|(&p, pa)| match &pa.assignments_intersection {
332332
AssignmentsIntersection::Decision((_, v, _)) => (p, v.clone()),
333333
AssignmentsIntersection::Derivations(_) => {
334-
panic!("Derivations in the Decision part")
334+
let mut context = String::new();
335+
for (id, assignment) in self
336+
.package_assignments
337+
.iter()
338+
.take(self.current_decision_level.0 as usize)
339+
{
340+
context.push_str(&format!(
341+
" * {:?} {:?}\n",
342+
id, assignment.assignments_intersection
343+
));
344+
}
345+
panic!(
346+
"Derivations in the Decision part. Decision level {}\n{}",
347+
self.current_decision_level.0, context
348+
)
335349
}
336350
})
337351
}
@@ -414,34 +428,39 @@ impl<DP: DependencyProvider> PartialSolution<DP> {
414428
version: DP::V,
415429
new_incompatibilities: std::ops::Range<IncompId<DP::P, DP::VS, DP::M>>,
416430
store: &Arena<Incompatibility<DP::P, DP::VS, DP::M>>,
417-
) {
431+
) -> Option<IncompId<DP::P, DP::VS, DP::M>> {
418432
if !self.has_ever_backtracked {
419-
// Nothing has yet gone wrong during this resolution. This call is unlikely to be the first problem.
433+
// Fast path: Nothing has yet gone wrong during this resolution. This call is unlikely to be the first problem.
420434
// So let's live with a little bit of risk and add the decision without checking the dependencies.
421435
// The worst that can happen is we will have to do a full backtrack which only removes this one decision.
422436
log::info!("add_decision: {package:?} @ {version} without checking dependencies");
423437
self.add_decision(package, version);
438+
return None;
439+
}
440+
441+
// Check if any of the dependencies preclude deciding on this crate version.
442+
let package_term = Term::exact(version.clone());
443+
let relation = |incompat: IncompId<DP::P, DP::VS, DP::M>| {
444+
store[incompat].relation(|p| {
445+
// The current package isn't part of the package assignments yet.
446+
if p == package {
447+
Some(&package_term)
448+
} else {
449+
self.term_intersection_for_package(p)
450+
}
451+
})
452+
};
453+
if let Some(satisfied) = Id::range_to_iter(new_incompatibilities)
454+
.find(|incompat| relation(*incompat) == Relation::Satisfied)
455+
{
456+
log::info!(
457+
"rejecting decision {package:?} @ {version} because its dependencies conflict"
458+
);
459+
Some(satisfied)
424460
} else {
425-
// Check if any of the new dependencies preclude deciding on this crate version.
426-
let exact = Term::exact(version.clone());
427-
let not_satisfied = |incompat: &Incompatibility<DP::P, DP::VS, DP::M>| {
428-
incompat.relation(|p| {
429-
if p == package {
430-
Some(&exact)
431-
} else {
432-
self.term_intersection_for_package(p)
433-
}
434-
}) != Relation::Satisfied
435-
};
436-
437-
// Check none of the dependencies (new_incompatibilities)
438-
// would create a conflict (be satisfied).
439-
if store[new_incompatibilities].iter().all(not_satisfied) {
440-
log::info!("add_decision: {package:?} @ {version}");
441-
self.add_decision(package, version);
442-
} else {
443-
log::info!("not adding {package:?} @ {version} because of its dependencies",);
444-
}
461+
log::info!("adding decision: {package:?} @ {version}");
462+
self.add_decision(package, version);
463+
None
445464
}
446465
}
447466

src/lib.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@
7171
//! and [SemanticVersion] for versions.
7272
//! This may be done quite easily by implementing the three following functions.
7373
//! ```
74-
//! # use pubgrub::{DependencyProvider, Dependencies, SemanticVersion, Ranges, DependencyConstraints, Map};
74+
//! # use pubgrub::{DependencyProvider, Dependencies, SemanticVersion, Ranges, DependencyConstraints, Map, PackageResolutionStatistics};
7575
//! # use std::error::Error;
7676
//! # use std::borrow::Borrow;
7777
//! # use std::convert::Infallible;
@@ -86,7 +86,7 @@
8686
//! }
8787
//!
8888
//! type Priority = usize;
89-
//! fn prioritize(&self, package: &String, range: &SemVS) -> Self::Priority {
89+
//! fn prioritize(&self, package: &String, range: &SemVS, conflicts_counts: &PackageResolutionStatistics) -> Self::Priority {
9090
//! unimplemented!()
9191
//! }
9292
//!
@@ -227,7 +227,7 @@ pub use report::{
227227
DefaultStringReportFormatter, DefaultStringReporter, DerivationTree, Derived, External,
228228
ReportFormatter, Reporter,
229229
};
230-
pub use solver::{resolve, Dependencies, DependencyProvider};
230+
pub use solver::{resolve, Dependencies, DependencyProvider, PackageResolutionStatistics};
231231
pub use term::Term;
232232
pub use type_aliases::{DependencyConstraints, Map, SelectedDependencies, Set};
233233
pub use version::{SemanticVersion, VersionParseError};

src/provider.rs

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use std::cmp::Reverse;
22
use std::collections::BTreeMap;
33
use std::convert::Infallible;
44

5+
use crate::solver::PackageResolutionStatistics;
56
use crate::{Dependencies, DependencyConstraints, DependencyProvider, Map, Package, VersionSet};
67

78
/// A basic implementation of [DependencyProvider].
@@ -92,15 +93,23 @@ impl<P: Package, VS: VersionSet> DependencyProvider for OfflineDependencyProvide
9293
.and_then(|versions| versions.keys().rev().find(|v| range.contains(v)).cloned()))
9394
}
9495

95-
type Priority = Reverse<usize>;
96+
type Priority = (u32, Reverse<usize>);
9697

9798
#[inline]
98-
fn prioritize(&self, package: &P, range: &VS) -> Self::Priority {
99-
Reverse(
100-
self.dependencies
101-
.get(package)
102-
.map(|versions| versions.keys().filter(|v| range.contains(v)).count())
103-
.unwrap_or(0),
99+
fn prioritize(
100+
&self,
101+
package: &Self::P,
102+
range: &Self::VS,
103+
package_statistics: &PackageResolutionStatistics,
104+
) -> Self::Priority {
105+
(
106+
package_statistics.conflict_count(),
107+
Reverse(
108+
self.dependencies
109+
.get(package)
110+
.map(|versions| versions.keys().filter(|v| range.contains(v)).count())
111+
.unwrap_or(0),
112+
),
104113
)
105114
}
106115

src/solver.rs

Lines changed: 67 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,31 @@ use log::{debug, info};
6868
use crate::internal::{Id, Incompatibility, State};
6969
use crate::{DependencyConstraints, Map, Package, PubGrubError, SelectedDependencies, VersionSet};
7070

71+
/// Statistics on how often a package conflicted with other packages.
72+
#[derive(Debug, Default, Clone)]
73+
pub struct PackageResolutionStatistics {
74+
unit_propagation_affected: u32,
75+
unit_propagation_culprit: u32,
76+
dependencies_affected: u32,
77+
dependencies_culprit: u32,
78+
}
79+
80+
impl PackageResolutionStatistics {
81+
/// The number of conflicts this package was involved in.
82+
///
83+
/// Processing packages with a high conflict count earlier usually speeds up resolution.
84+
///
85+
/// Whenever a package is part of the root cause incompatibility of a conflict, we increase its
86+
/// count by one. Since the structure of the incompatibilities may change, this count too may
87+
/// change in the future.
88+
pub fn conflict_count(&self) -> u32 {
89+
self.unit_propagation_affected
90+
+ self.unit_propagation_culprit
91+
+ self.dependencies_affected
92+
+ self.dependencies_culprit
93+
}
94+
}
95+
7196
/// Main function of the library.
7297
/// Finds a set of packages satisfying dependency bounds for a given package + version pair.
7398
#[cold]
@@ -77,6 +102,7 @@ pub fn resolve<DP: DependencyProvider>(
77102
version: impl Into<DP::V>,
78103
) -> Result<SelectedDependencies<DP>, PubGrubError<DP>> {
79104
let mut state: State<DP> = State::init(package.clone(), version.into());
105+
let mut conflict_tracker: Map<Id<DP::P>, PackageResolutionStatistics> = Map::default();
80106
let mut added_dependencies: Map<Id<DP::P>, Set<DP::V>> = Map::default();
81107
let mut next = state.root_package;
82108
loop {
@@ -88,7 +114,22 @@ pub fn resolve<DP: DependencyProvider>(
88114
"unit_propagation: {:?} = '{}'",
89115
&next, state.package_store[next]
90116
);
91-
state.unit_propagation(next)?;
117+
let root_causes = state.unit_propagation(next)?;
118+
for (affected, incompat) in root_causes {
119+
conflict_tracker
120+
.entry(affected)
121+
.or_default()
122+
.unit_propagation_affected += 1;
123+
for (conflict_package, _) in state.incompatibility_store[incompat].iter() {
124+
if conflict_package == affected {
125+
continue;
126+
}
127+
conflict_tracker
128+
.entry(conflict_package)
129+
.or_default()
130+
.unit_propagation_culprit += 1;
131+
}
132+
}
92133

93134
debug!(
94135
"Partial solution after unit propagation: {}",
@@ -97,7 +138,11 @@ pub fn resolve<DP: DependencyProvider>(
97138

98139
let Some(highest_priority_pkg) =
99140
state.partial_solution.pick_highest_priority_pkg(|p, r| {
100-
dependency_provider.prioritize(&state.package_store[p], r)
141+
dependency_provider.prioritize(
142+
&state.package_store[p],
143+
r,
144+
conflict_tracker.entry(p).or_default(),
145+
)
101146
})
102147
else {
103148
return Ok(state
@@ -171,7 +216,20 @@ pub fn resolve<DP: DependencyProvider>(
171216
};
172217

173218
// Add that package and version if the dependencies are not problematic.
174-
state.add_package_version_dependencies(p, v.clone(), dependencies);
219+
if let Some(conflict) =
220+
state.add_package_version_dependencies(p, v.clone(), dependencies)
221+
{
222+
conflict_tracker.entry(p).or_default().dependencies_affected += 1;
223+
for (incompat_package, _) in state.incompatibility_store[conflict].iter() {
224+
if incompat_package == p {
225+
continue;
226+
}
227+
conflict_tracker
228+
.entry(incompat_package)
229+
.or_default()
230+
.unit_propagation_culprit += 1;
231+
}
232+
}
175233
} else {
176234
// `dep_incompats` are already in `incompatibilities` so we know there are not satisfied
177235
// terms and can add the decision directly.
@@ -249,7 +307,12 @@ pub trait DependencyProvider {
249307
///
250308
/// Note: the resolver may call this even when the range has not changed,
251309
/// if it is more efficient for the resolvers internal data structures.
252-
fn prioritize(&self, package: &Self::P, range: &Self::VS) -> Self::Priority;
310+
fn prioritize(
311+
&self,
312+
package: &Self::P,
313+
range: &Self::VS,
314+
package_conflicts_counts: &PackageResolutionStatistics,
315+
) -> Self::Priority;
253316
/// The type returned from `prioritize`. The resolver does not care what type this is
254317
/// as long as it can pick a largest one and clone it.
255318
///

tests/proptest.rs

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ use proptest::string::string_regex;
1313

1414
use pubgrub::{
1515
resolve, DefaultStringReporter, Dependencies, DependencyProvider, DerivationTree, External,
16-
OfflineDependencyProvider, Package, PubGrubError, Ranges, Reporter, SelectedDependencies,
17-
VersionSet,
16+
OfflineDependencyProvider, Package, PackageResolutionStatistics, PubGrubError, Ranges,
17+
Reporter, SelectedDependencies, VersionSet,
1818
};
1919

2020
use crate::sat_dependency_provider::SatResolve;
@@ -49,8 +49,13 @@ impl<P: Package, VS: VersionSet> DependencyProvider for OldestVersionsDependency
4949

5050
type Priority = <OfflineDependencyProvider<P, VS> as DependencyProvider>::Priority;
5151

52-
fn prioritize(&self, package: &P, range: &VS) -> Self::Priority {
53-
self.0.prioritize(package, range)
52+
fn prioritize(
53+
&self,
54+
package: &Self::P,
55+
range: &Self::VS,
56+
package_statistics: &PackageResolutionStatistics,
57+
) -> Self::Priority {
58+
self.0.prioritize(package, range, package_statistics)
5459
}
5560

5661
type Err = Infallible;
@@ -104,8 +109,13 @@ impl<DP: DependencyProvider> DependencyProvider for TimeoutDependencyProvider<DP
104109

105110
type Priority = DP::Priority;
106111

107-
fn prioritize(&self, package: &DP::P, range: &DP::VS) -> Self::Priority {
108-
self.dp.prioritize(package, range)
112+
fn prioritize(
113+
&self,
114+
package: &Self::P,
115+
range: &Self::VS,
116+
package_statistics: &PackageResolutionStatistics,
117+
) -> Self::Priority {
118+
self.dp.prioritize(package, range, package_statistics)
109119
}
110120

111121
type Err = DP::Err;

0 commit comments

Comments
 (0)