Skip to content

Commit 7883d9b

Browse files
Eh2406zanieb
authored andcommitted
perf!: use a priority queue (pubgrub-rs#104)
BREAKING CHANGE: Changes the API of DependencyProvider
1 parent 8bb2020 commit 7883d9b

File tree

9 files changed

+264
-198
lines changed

9 files changed

+264
-198
lines changed

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ include = ["Cargo.toml", "LICENSE", "README.md", "src/**", "tests/**", "examples
2020
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
2121

2222
[dependencies]
23+
indexmap = "2.0.2"
24+
priority-queue = "1.1.1"
2325
thiserror = "1.0"
2426
rustc-hash = "1.1.0"
2527
serde = { version = "1.0", features = ["derive"], optional = true }

examples/caching_dependency_provider.rs

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,6 @@ impl<P: Package, VS: VersionSet, DP: DependencyProvider<P, VS>>
3232
impl<P: Package, VS: VersionSet, DP: DependencyProvider<P, VS>> DependencyProvider<P, VS>
3333
for CachingDependencyProvider<P, VS, DP>
3434
{
35-
fn choose_package_version<T: std::borrow::Borrow<P>, U: std::borrow::Borrow<VS>>(
36-
&self,
37-
packages: impl Iterator<Item = (T, U)>,
38-
) -> Result<(T, Option<VS::V>), Box<dyn Error + Send + Sync>> {
39-
self.remote_dependencies.choose_package_version(packages)
40-
}
41-
4235
// Caches dependencies if they were already queried
4336
fn get_dependencies(
4437
&self,
@@ -66,6 +59,20 @@ impl<P: Package, VS: VersionSet, DP: DependencyProvider<P, VS>> DependencyProvid
6659
error @ Err(_) => error,
6760
}
6861
}
62+
63+
fn choose_version(
64+
&self,
65+
package: &P,
66+
range: &VS,
67+
) -> Result<Option<VS::V>, Box<dyn Error + Send + Sync>> {
68+
self.remote_dependencies.choose_version(package, range)
69+
}
70+
71+
type Priority = DP::Priority;
72+
73+
fn prioritize(&self, package: &P, range: &VS) -> Self::Priority {
74+
self.remote_dependencies.prioritize(package, range)
75+
}
6976
}
7077

7178
fn main() {

src/error.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ pub enum PubGrubError<P: Package, VS: VersionSet> {
4646
/// Error arising when the implementer of
4747
/// [DependencyProvider](crate::solver::DependencyProvider)
4848
/// returned an error in the method
49-
/// [choose_package_version](crate::solver::DependencyProvider::choose_package_version).
49+
/// [choose_version](crate::solver::DependencyProvider::choose_version).
5050
#[error("Decision making failed")]
5151
ErrorChoosingPackageVersion(Box<dyn std::error::Error + Send + Sync>),
5252

src/internal/core.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ use crate::version_set::VersionSet;
2020

2121
/// Current state of the PubGrub algorithm.
2222
#[derive(Clone)]
23-
pub struct State<P: Package, VS: VersionSet> {
23+
pub struct State<P: Package, VS: VersionSet, Priority: Ord + Clone> {
2424
root_package: P,
2525
root_version: VS::V,
2626

@@ -32,7 +32,7 @@ pub struct State<P: Package, VS: VersionSet> {
3232

3333
/// Partial solution.
3434
/// TODO: remove pub.
35-
pub partial_solution: PartialSolution<P, VS>,
35+
pub partial_solution: PartialSolution<P, VS, Priority>,
3636

3737
/// The store is the reference storage for all incompatibilities.
3838
pub incompatibility_store: Arena<Incompatibility<P, VS>>,
@@ -43,7 +43,7 @@ pub struct State<P: Package, VS: VersionSet> {
4343
unit_propagation_buffer: SmallVec<P>,
4444
}
4545

46-
impl<P: Package, VS: VersionSet> State<P, VS> {
46+
impl<P: Package, VS: VersionSet, Priority: Ord + Clone> State<P, VS, Priority> {
4747
/// Initialization of PubGrub state.
4848
pub fn init(root_package: P, root_version: VS::V) -> Self {
4949
let mut incompatibility_store = Arena::new();

src/internal/partial_solution.rs

Lines changed: 94 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,26 @@
11
// SPDX-License-Identifier: MPL-2.0
22

33
//! A Memory acts like a structured partial solution
4-
//! where terms are regrouped by package in a [Map].
4+
//! where terms are regrouped by package in a [Map](crate::type_aliases::Map).
55
66
use std::fmt::Display;
7+
use std::hash::BuildHasherDefault;
8+
9+
use priority_queue::PriorityQueue;
10+
use rustc_hash::FxHasher;
711

812
use crate::internal::arena::Arena;
913
use crate::internal::incompatibility::{IncompId, Incompatibility, Relation};
1014
use crate::internal::small_map::SmallMap;
1115
use crate::package::Package;
1216
use crate::term::Term;
13-
use crate::type_aliases::{Map, SelectedDependencies};
17+
use crate::type_aliases::SelectedDependencies;
1418
use crate::version_set::VersionSet;
1519

1620
use super::small_vec::SmallVec;
1721

22+
type FnvIndexMap<K, V> = indexmap::IndexMap<K, V, BuildHasherDefault<rustc_hash::FxHasher>>;
23+
1824
#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)]
1925
pub struct DecisionLevel(pub u32);
2026

@@ -27,13 +33,29 @@ impl DecisionLevel {
2733
/// The partial solution contains all package assignments,
2834
/// organized by package and historically ordered.
2935
#[derive(Clone, Debug)]
30-
pub struct PartialSolution<P: Package, VS: VersionSet> {
36+
pub struct PartialSolution<P: Package, VS: VersionSet, Priority: Ord + Clone> {
3137
next_global_index: u32,
3238
current_decision_level: DecisionLevel,
33-
package_assignments: Map<P, PackageAssignments<P, VS>>,
39+
/// `package_assignments` is primarily a HashMap from a package to its
40+
/// `PackageAssignments`. But it can also keep the items in an order.
41+
/// We maintain three sections in this order:
42+
/// 1. `[..current_decision_level]` Are packages that have had a decision made sorted by the `decision_level`.
43+
/// This makes it very efficient to extract the solution, And to backtrack to a particular decision level.
44+
/// 2. `[current_decision_level..changed_this_decision_level]` Are packages that have **not** had there assignments
45+
/// changed since the last time `prioritize` has bean called. Within this range there is no sorting.
46+
/// 3. `[changed_this_decision_level..]` Containes all packages that **have** had there assignments changed since
47+
/// the last time `prioritize` has bean called. The inverse is not necessarily true, some packages in the range
48+
/// did not have a change. Within this range there is no sorting.
49+
package_assignments: FnvIndexMap<P, PackageAssignments<P, VS>>,
50+
/// `prioritized_potential_packages` is primarily a HashMap from a package with no desition and a positive assignment
51+
/// to its `Priority`. But, it also maintains a max heap of packages by `Priority` order.
52+
prioritized_potential_packages: PriorityQueue<P, Priority, BuildHasherDefault<FxHasher>>,
53+
changed_this_decision_level: usize,
3454
}
3555

36-
impl<P: Package, VS: VersionSet> Display for PartialSolution<P, VS> {
56+
impl<P: Package, VS: VersionSet, Priority: Ord + Clone> Display
57+
for PartialSolution<P, VS, Priority>
58+
{
3759
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3860
let mut assignments: Vec<_> = self
3961
.package_assignments
@@ -120,13 +142,15 @@ pub enum SatisfierSearch<P: Package, VS: VersionSet> {
120142
},
121143
}
122144

123-
impl<P: Package, VS: VersionSet> PartialSolution<P, VS> {
145+
impl<P: Package, VS: VersionSet, Priority: Ord + Clone> PartialSolution<P, VS, Priority> {
124146
/// Initialize an empty PartialSolution.
125147
pub fn empty() -> Self {
126148
Self {
127149
next_global_index: 0,
128150
current_decision_level: DecisionLevel(0),
129-
package_assignments: Map::default(),
151+
package_assignments: FnvIndexMap::default(),
152+
prioritized_potential_packages: PriorityQueue::default(),
153+
changed_this_decision_level: 0,
130154
}
131155
}
132156

@@ -151,18 +175,27 @@ impl<P: Package, VS: VersionSet> PartialSolution<P, VS> {
151175
}
152176
},
153177
}
178+
assert_eq!(
179+
self.changed_this_decision_level,
180+
self.package_assignments.len()
181+
);
154182
}
183+
let new_idx = self.current_decision_level.0 as usize;
155184
self.current_decision_level = self.current_decision_level.increment();
156-
let pa = self
185+
let (old_idx, _, pa) = self
157186
.package_assignments
158-
.get_mut(&package)
187+
.get_full_mut(&package)
159188
.expect("Derivations must already exist");
160189
pa.highest_decision_level = self.current_decision_level;
161190
pa.assignments_intersection = AssignmentsIntersection::Decision((
162191
self.next_global_index,
163192
version.clone(),
164193
Term::exact(version),
165194
));
195+
// Maintain that the beginning of the `package_assignments` Have all decisions in sorted order.
196+
if new_idx != old_idx {
197+
self.package_assignments.swap_indices(new_idx, old_idx);
198+
}
166199
self.next_global_index += 1;
167200
}
168201

@@ -173,16 +206,18 @@ impl<P: Package, VS: VersionSet> PartialSolution<P, VS> {
173206
cause: IncompId<P, VS>,
174207
store: &Arena<Incompatibility<P, VS>>,
175208
) {
176-
use std::collections::hash_map::Entry;
209+
use indexmap::map::Entry;
177210
let term = store[cause].get(&package).unwrap().negate();
178211
let dated_derivation = DatedDerivation {
179212
global_index: self.next_global_index,
180213
decision_level: self.current_decision_level,
181214
cause,
182215
};
183216
self.next_global_index += 1;
217+
let pa_last_index = self.package_assignments.len().saturating_sub(1);
184218
match self.package_assignments.entry(package) {
185219
Entry::Occupied(mut occupied) => {
220+
let idx = occupied.index();
186221
let pa = occupied.get_mut();
187222
pa.highest_decision_level = self.current_decision_level;
188223
match &mut pa.assignments_intersection {
@@ -192,11 +227,21 @@ impl<P: Package, VS: VersionSet> PartialSolution<P, VS> {
192227
}
193228
AssignmentsIntersection::Derivations(t) => {
194229
*t = t.intersection(&term);
230+
if t.is_positive() {
231+
// we can use `swap_indices` to make `changed_this_decision_level` only go down by 1
232+
// but the copying is slower then the larger search
233+
self.changed_this_decision_level =
234+
std::cmp::min(self.changed_this_decision_level, idx);
235+
}
195236
}
196237
}
197238
pa.dated_derivations.push(dated_derivation);
198239
}
199240
Entry::Vacant(v) => {
241+
if term.is_positive() {
242+
self.changed_this_decision_level =
243+
std::cmp::min(self.changed_this_decision_level, pa_last_index);
244+
}
200245
v.insert(PackageAssignments {
201246
smallest_decision_level: self.current_decision_level,
202247
highest_decision_level: self.current_decision_level,
@@ -207,43 +252,48 @@ impl<P: Package, VS: VersionSet> PartialSolution<P, VS> {
207252
}
208253
}
209254

210-
/// Extract potential packages for the next iteration of unit propagation.
211-
/// Return `None` if there is no suitable package anymore, which stops the algorithm.
212-
/// A package is a potential pick if there isn't an already
213-
/// selected version (no "decision")
214-
/// and if it contains at least one positive derivation term
215-
/// in the partial solution.
216-
pub fn potential_packages(&self) -> Option<impl Iterator<Item = (&P, &VS)>> {
217-
let mut iter = self
218-
.package_assignments
255+
pub fn pick_highest_priority_pkg(
256+
&mut self,
257+
prioritizer: impl Fn(&P, &VS) -> Priority,
258+
) -> Option<P> {
259+
let check_all = self.changed_this_decision_level
260+
== self.current_decision_level.0.saturating_sub(1) as usize;
261+
let current_decision_level = self.current_decision_level;
262+
let prioritized_potential_packages = &mut self.prioritized_potential_packages;
263+
self.package_assignments
264+
.get_range(self.changed_this_decision_level..)
265+
.unwrap()
219266
.iter()
267+
.filter(|(_, pa)| {
268+
// We only actually need to update the package if its Been changed
269+
// since the last time we called prioritize.
270+
// Which means it's highest decision level is the current decision level,
271+
// or if we backtracked in the mean time.
272+
check_all || pa.highest_decision_level == current_decision_level
273+
})
220274
.filter_map(|(p, pa)| pa.assignments_intersection.potential_package_filter(p))
221-
.peekable();
222-
if iter.peek().is_some() {
223-
Some(iter)
224-
} else {
225-
None
226-
}
275+
.for_each(|(p, r)| {
276+
let priority = prioritizer(p, r);
277+
prioritized_potential_packages.push(p.clone(), priority);
278+
});
279+
self.changed_this_decision_level = self.package_assignments.len();
280+
prioritized_potential_packages.pop().map(|(p, _)| p)
227281
}
228282

229283
/// If a partial solution has, for every positive derivation,
230284
/// a corresponding decision that satisfies that assignment,
231285
/// it's a total solution and version solving has succeeded.
232-
pub fn extract_solution(&self) -> Option<SelectedDependencies<P, VS::V>> {
233-
let mut solution = Map::default();
234-
for (p, pa) in &self.package_assignments {
235-
match &pa.assignments_intersection {
236-
AssignmentsIntersection::Decision((_, v, _)) => {
237-
solution.insert(p.clone(), v.clone());
238-
}
239-
AssignmentsIntersection::Derivations(term) => {
240-
if term.is_positive() {
241-
return None;
242-
}
286+
pub fn extract_solution(&self) -> SelectedDependencies<P, VS::V> {
287+
self.package_assignments
288+
.iter()
289+
.take(self.current_decision_level.0 as usize)
290+
.map(|(p, pa)| match &pa.assignments_intersection {
291+
AssignmentsIntersection::Decision((_, v, _)) => (p.clone(), v.clone()),
292+
AssignmentsIntersection::Derivations(_) => {
293+
panic!("Derivations in the Decision part")
243294
}
244-
}
245-
}
246-
Some(solution)
295+
})
296+
.collect()
247297
}
248298

249299
/// Backtrack the partial solution to a given decision level.
@@ -290,6 +340,9 @@ impl<P: Package, VS: VersionSet> PartialSolution<P, VS> {
290340
true
291341
}
292342
});
343+
// Throw away all stored priority levels, And mark that they all need to be recomputed.
344+
self.prioritized_potential_packages.clear();
345+
self.changed_this_decision_level = self.current_decision_level.0.saturating_sub(1) as usize;
293346
}
294347

295348
/// We can add the version to the partial solution as a decision
@@ -386,7 +439,7 @@ impl<P: Package, VS: VersionSet> PartialSolution<P, VS> {
386439
/// to return a coherent previous_satisfier_level.
387440
fn find_satisfier(
388441
incompat: &Incompatibility<P, VS>,
389-
package_assignments: &Map<P, PackageAssignments<P, VS>>,
442+
package_assignments: &FnvIndexMap<P, PackageAssignments<P, VS>>,
390443
store: &Arena<Incompatibility<P, VS>>,
391444
) -> SmallMap<P, (usize, u32, DecisionLevel)> {
392445
let mut satisfied = SmallMap::Empty;
@@ -407,7 +460,7 @@ impl<P: Package, VS: VersionSet> PartialSolution<P, VS> {
407460
incompat: &Incompatibility<P, VS>,
408461
satisfier_package: &P,
409462
mut satisfied_map: SmallMap<P, (usize, u32, DecisionLevel)>,
410-
package_assignments: &Map<P, PackageAssignments<P, VS>>,
463+
package_assignments: &FnvIndexMap<P, PackageAssignments<P, VS>>,
411464
store: &Arena<Incompatibility<P, VS>>,
412465
) -> DecisionLevel {
413466
// First, let's retrieve the previous derivations and the initial accum_term.

src/lib.rs

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@
7575
//! trait for our own type.
7676
//! Let's say that we will use [String] for packages,
7777
//! and [SemanticVersion](version::SemanticVersion) for versions.
78-
//! This may be done quite easily by implementing the two following functions.
78+
//! This may be done quite easily by implementing the three following functions.
7979
//! ```
8080
//! # use pubgrub::solver::{DependencyProvider, Dependencies};
8181
//! # use pubgrub::version::SemanticVersion;
@@ -89,7 +89,12 @@
8989
//! type SemVS = Range<SemanticVersion>;
9090
//!
9191
//! impl DependencyProvider<String, SemVS> for MyDependencyProvider {
92-
//! fn choose_package_version<T: Borrow<String>, U: Borrow<SemVS>>(&self,packages: impl Iterator<Item=(T, U)>) -> Result<(T, Option<SemanticVersion>), Box<dyn Error + Send + Sync>> {
92+
//! fn choose_version(&self, package: &String, range: &SemVS) -> Result<Option<SemanticVersion>, Box<dyn Error + Send + Sync>> {
93+
//! unimplemented!()
94+
//! }
95+
//!
96+
//! type Priority = usize;
97+
//! fn prioritize(&self, package: &String, range: &SemVS) -> Self::Priority {
9398
//! unimplemented!()
9499
//! }
95100
//!
@@ -104,18 +109,17 @@
104109
//! ```
105110
//!
106111
//! The first method
107-
//! [choose_package_version](crate::solver::DependencyProvider::choose_package_version)
108-
//! chooses a package and available version compatible with the provided options.
109-
//! A helper function
110-
//! [choose_package_with_fewest_versions](crate::solver::choose_package_with_fewest_versions)
111-
//! is provided for convenience
112-
//! in cases when lists of available versions for packages are easily obtained.
113-
//! The strategy of that helper function consists in choosing the package
114-
//! with the fewest number of compatible versions to speed up resolution.
112+
//! [choose_version](crate::solver::DependencyProvider::choose_version)
113+
//! chooses a version compatible with the provided range for a package.
114+
//! The second method
115+
//! [prioritize](crate::solver::DependencyProvider::prioritize)
116+
//! in which order different packages should be chosen.
117+
//! Usually prioritizing packages
118+
//! with the fewest number of compatible versions speeds up resolution.
115119
//! But in general you are free to employ whatever strategy suits you best
116120
//! to pick a package and a version.
117121
//!
118-
//! The second method [get_dependencies](crate::solver::DependencyProvider::get_dependencies)
122+
//! The third method [get_dependencies](crate::solver::DependencyProvider::get_dependencies)
119123
//! aims at retrieving the dependencies of a given package at a given version.
120124
//! Returns [None] if dependencies are unknown.
121125
//!

0 commit comments

Comments
 (0)