Skip to content

Commit d49bf5f

Browse files
authored
Return and track affected and culprit on conflicts (#298)
* 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 #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 #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 ``` * Use smallvec for root causes * Add more docs * Review
1 parent ffef172 commit d49bf5f

File tree

7 files changed

+181
-51
lines changed

7 files changed

+181
-51
lines changed

examples/caching_dependency_provider.rs

+12-3
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

+8-2
Original file line numberDiff line numberDiff line change
@@ -107,11 +107,16 @@ impl<DP: DependencyProvider> State<DP> {
107107

108108
/// Unit propagation is the core mechanism of the solving algorithm.
109109
/// CF <https://github.com/dart-lang/pub/blob/master/doc/solver.md#unit-propagation>
110+
///
111+
/// For each package with a satisfied incompatibility, returns the package and the root cause
112+
/// incompatibility.
110113
#[cold]
114+
#[allow(clippy::type_complexity)] // Type definitions don't support impl trait.
111115
pub(crate) fn unit_propagation(
112116
&mut self,
113117
package: Id<DP::P>,
114-
) -> Result<(), NoSolutionError<DP>> {
118+
) -> Result<SmallVec<(Id<DP::P>, IncompDpId<DP>)>, NoSolutionError<DP>> {
119+
let mut root_causes = SmallVec::default();
115120
self.unit_propagation_buffer.clear();
116121
self.unit_propagation_buffer.push(package);
117122
while let Some(current_package) = self.unit_propagation_buffer.pop() {
@@ -169,6 +174,7 @@ impl<DP: DependencyProvider> State<DP> {
169174
.map_err(|terminal_incompat_id| {
170175
self.build_derivation_tree(terminal_incompat_id)
171176
})?;
177+
root_causes.push((package, root_cause));
172178
self.unit_propagation_buffer.clear();
173179
self.unit_propagation_buffer.push(package_almost);
174180
// Add to the partial solution with incompat as cause.
@@ -184,7 +190,7 @@ impl<DP: DependencyProvider> State<DP> {
184190
}
185191
}
186192
// If there are no more changed packages, unit propagation is done.
187-
Ok(())
193+
Ok(root_causes)
188194
}
189195

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

src/internal/partial_solution.rs

+44-24
Original file line numberDiff line numberDiff line change
@@ -310,7 +310,7 @@ impl<DP: DependencyProvider> PartialSolution<DP> {
310310
#[cold]
311311
pub(crate) fn pick_highest_priority_pkg(
312312
&mut self,
313-
prioritizer: impl Fn(Id<DP::P>, &DP::VS) -> DP::Priority,
313+
mut prioritizer: impl FnMut(Id<DP::P>, &DP::VS) -> DP::Priority,
314314
) -> Option<Id<DP::P>> {
315315
let check_all = self.prioritize_decision_level
316316
== self.current_decision_level.0.saturating_sub(1) as usize;
@@ -350,7 +350,22 @@ impl<DP: DependencyProvider> PartialSolution<DP> {
350350
term: _,
351351
} => (p, v.clone()),
352352
AssignmentsIntersection::Derivations(_) => {
353-
panic!("Derivations in the Decision part")
353+
// The invariant on the order in `self.package_assignments` was broken.
354+
let mut context = String::new();
355+
for (id, assignment) in self
356+
.package_assignments
357+
.iter()
358+
.take(self.current_decision_level.0 as usize)
359+
{
360+
context.push_str(&format!(
361+
" * {:?} {:?}\n",
362+
id, assignment.assignments_intersection
363+
));
364+
}
365+
panic!(
366+
"Derivations in the Decision part. Decision level {}\n{}",
367+
self.current_decision_level.0, context
368+
)
354369
}
355370
})
356371
}
@@ -408,34 +423,39 @@ impl<DP: DependencyProvider> PartialSolution<DP> {
408423
version: DP::V,
409424
new_incompatibilities: std::ops::Range<IncompId<DP::P, DP::VS, DP::M>>,
410425
store: &Arena<Incompatibility<DP::P, DP::VS, DP::M>>,
411-
) {
426+
) -> Option<IncompId<DP::P, DP::VS, DP::M>> {
412427
if !self.has_ever_backtracked {
413-
// Nothing has yet gone wrong during this resolution. This call is unlikely to be the first problem.
428+
// Fast path: Nothing has yet gone wrong during this resolution. This call is unlikely to be the first problem.
414429
// So let's live with a little bit of risk and add the decision without checking the dependencies.
415430
// The worst that can happen is we will have to do a full backtrack which only removes this one decision.
416431
log::info!("add_decision: {package:?} @ {version} without checking dependencies");
417432
self.add_decision(package, version);
433+
return None;
434+
}
435+
436+
// Check if any of the dependencies preclude deciding on this crate version.
437+
let package_term = Term::exact(version.clone());
438+
let relation = |incompat: IncompId<DP::P, DP::VS, DP::M>| {
439+
store[incompat].relation(|p| {
440+
// The current package isn't part of the package assignments yet.
441+
if p == package {
442+
Some(&package_term)
443+
} else {
444+
self.term_intersection_for_package(p)
445+
}
446+
})
447+
};
448+
if let Some(satisfied) = Id::range_to_iter(new_incompatibilities)
449+
.find(|incompat| relation(*incompat) == Relation::Satisfied)
450+
{
451+
log::info!(
452+
"rejecting decision {package:?} @ {version} because its dependencies conflict"
453+
);
454+
Some(satisfied)
418455
} else {
419-
// Check if any of the new dependencies preclude deciding on this crate version.
420-
let exact = Term::exact(version.clone());
421-
let not_satisfied = |incompat: &Incompatibility<DP::P, DP::VS, DP::M>| {
422-
incompat.relation(|p| {
423-
if p == package {
424-
Some(&exact)
425-
} else {
426-
self.term_intersection_for_package(p)
427-
}
428-
}) != Relation::Satisfied
429-
};
430-
431-
// Check none of the dependencies (new_incompatibilities)
432-
// would create a conflict (be satisfied).
433-
if store[new_incompatibilities].iter().all(not_satisfied) {
434-
log::info!("add_decision: {package:?} @ {version}");
435-
self.add_decision(package, version);
436-
} else {
437-
log::info!("not adding {package:?} @ {version} because of its dependencies",);
438-
}
456+
log::info!("adding decision: {package:?} @ {version}");
457+
self.add_decision(package, version);
458+
None
439459
}
440460
}
441461

src/lib.rs

+3-3
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

+16-7
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

+82-6
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,43 @@ 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+
// We track these fields separately but currently don't expose them separately to keep the
75+
// stable API slim. Please be encouraged to try different combinations of them and report if
76+
// you find better metrics that should be exposed.
77+
//
78+
// Say we have packages A and B, A having higher priority than B. We first decide A and then B,
79+
// and then find B to conflict with A. We call be B "affected" and A "culprit" since the
80+
// decisions for B is being rejected due to the decision we made for A earlier.
81+
//
82+
// If B is rejected due to its dependencies conflicting with A, we increase
83+
// `dependencies_affected` for B and for `dependencies_culprit` A. If B is rejected in unit
84+
// through an incompatibility with B, we increase `unit_propagation_affected` for B and for
85+
// `unit_propagation_culprit` A.
86+
unit_propagation_affected: u32,
87+
unit_propagation_culprit: u32,
88+
dependencies_affected: u32,
89+
dependencies_culprit: u32,
90+
}
91+
92+
impl PackageResolutionStatistics {
93+
/// The number of conflicts this package was involved in.
94+
///
95+
/// Processing packages with a high conflict count earlier usually speeds up resolution.
96+
///
97+
/// Whenever a package is part of the root cause incompatibility of a conflict, we increase its
98+
/// count by one. Since the structure of the incompatibilities may change, this count too may
99+
/// change in the future.
100+
pub fn conflict_count(&self) -> u32 {
101+
self.unit_propagation_affected
102+
+ self.unit_propagation_culprit
103+
+ self.dependencies_affected
104+
+ self.dependencies_culprit
105+
}
106+
}
107+
71108
/// Main function of the library.
72109
/// Finds a set of packages satisfying dependency bounds for a given package + version pair.
73110
#[cold]
@@ -77,6 +114,7 @@ pub fn resolve<DP: DependencyProvider>(
77114
version: impl Into<DP::V>,
78115
) -> Result<SelectedDependencies<DP>, PubGrubError<DP>> {
79116
let mut state: State<DP> = State::init(package.clone(), version.into());
117+
let mut conflict_tracker: Map<Id<DP::P>, PackageResolutionStatistics> = Map::default();
80118
let mut added_dependencies: Map<Id<DP::P>, Set<DP::V>> = Map::default();
81119
let mut next = state.root_package;
82120
loop {
@@ -88,7 +126,22 @@ pub fn resolve<DP: DependencyProvider>(
88126
"unit_propagation: {:?} = '{}'",
89127
&next, state.package_store[next]
90128
);
91-
state.unit_propagation(next)?;
129+
let root_causes = state.unit_propagation(next)?;
130+
for (affected, incompat) in root_causes {
131+
conflict_tracker
132+
.entry(affected)
133+
.or_default()
134+
.unit_propagation_affected += 1;
135+
for (conflict_package, _) in state.incompatibility_store[incompat].iter() {
136+
if conflict_package == affected {
137+
continue;
138+
}
139+
conflict_tracker
140+
.entry(conflict_package)
141+
.or_default()
142+
.unit_propagation_culprit += 1;
143+
}
144+
}
92145

93146
debug!(
94147
"Partial solution after unit propagation: {}",
@@ -97,7 +150,11 @@ pub fn resolve<DP: DependencyProvider>(
97150

98151
let Some(highest_priority_pkg) =
99152
state.partial_solution.pick_highest_priority_pkg(|p, r| {
100-
dependency_provider.prioritize(&state.package_store[p], r)
153+
dependency_provider.prioritize(
154+
&state.package_store[p],
155+
r,
156+
conflict_tracker.entry(p).or_default(),
157+
)
101158
})
102159
else {
103160
return Ok(state
@@ -174,9 +231,23 @@ pub fn resolve<DP: DependencyProvider>(
174231
let dep_incompats =
175232
state.add_incompatibility_from_dependencies(p, v.clone(), dependencies);
176233

177-
state
178-
.partial_solution
179-
.add_version(p, v, dep_incompats, &state.incompatibility_store);
234+
if let Some(conflict) = state.partial_solution.add_version(
235+
p,
236+
v,
237+
dep_incompats,
238+
&state.incompatibility_store,
239+
) {
240+
conflict_tracker.entry(p).or_default().dependencies_affected += 1;
241+
for (incompat_package, _) in state.incompatibility_store[conflict].iter() {
242+
if incompat_package == p {
243+
continue;
244+
}
245+
conflict_tracker
246+
.entry(incompat_package)
247+
.or_default()
248+
.dependencies_culprit += 1;
249+
}
250+
}
180251
} else {
181252
// `dep_incompats` are already in `incompatibilities` so we know there are not satisfied
182253
// terms and can add the decision directly.
@@ -254,7 +325,12 @@ pub trait DependencyProvider {
254325
///
255326
/// Note: the resolver may call this even when the range has not changed,
256327
/// if it is more efficient for the resolvers internal data structures.
257-
fn prioritize(&self, package: &Self::P, range: &Self::VS) -> Self::Priority;
328+
fn prioritize(
329+
&self,
330+
package: &Self::P,
331+
range: &Self::VS,
332+
package_conflicts_counts: &PackageResolutionStatistics,
333+
) -> Self::Priority;
258334
/// The type returned from `prioritize`. The resolver does not care what type this is
259335
/// as long as it can pick a largest one and clone it.
260336
///

tests/proptest.rs

+16-6
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)