76418f5bdf
Pubgrub stores incompatibilities as (package name, version range) tuples, meaning it needs to clone the package name for each incompatibility, and each non-borrowed operation on incompatibilities. https://github.com/astral-sh/uv/pull/3673 made me realize that `PubGrubPackage` has gotten large (expensive to copy), so like `Version` and other structs, i've added an `Arc` wrapper around it. It's a pity clippy forbids `.deref()`, it's less opaque than `&**` and has IDE support (clicking on `.deref()` jumps to the right impl). ## Benchmarks It looks like this matters most for complex resolutions which, i assume because they carry larger `PubGrubPackageInner::Package` and `PubGrubPackageInner::Extra` types. ```bash hyperfine --warmup 5 "./uv-main pip compile -q ./scripts/requirements/jupyter.in" "./uv-branch pip compile -q ./scripts/requirements/jupyter.in" hyperfine --warmup 5 "./uv-main pip compile -q ./scripts/requirements/airflow.in" "./uv-branch pip compile -q ./scripts/requirements/airflow.in" hyperfine --warmup 5 "./uv-main pip compile -q ./scripts/requirements/boto3.in" "./uv-branch pip compile -q ./scripts/requirements/boto3.in" ``` ``` Benchmark 1: ./uv-main pip compile -q ./scripts/requirements/jupyter.in Time (mean ± σ): 18.2 ms ± 1.6 ms [User: 14.4 ms, System: 26.0 ms] Range (min … max): 15.8 ms … 22.5 ms 181 runs Benchmark 2: ./uv-branch pip compile -q ./scripts/requirements/jupyter.in Time (mean ± σ): 17.8 ms ± 1.4 ms [User: 14.4 ms, System: 25.3 ms] Range (min … max): 15.4 ms … 23.1 ms 159 runs Summary ./uv-branch pip compile -q ./scripts/requirements/jupyter.in ran 1.02 ± 0.12 times faster than ./uv-main pip compile -q ./scripts/requirements/jupyter.in ``` ``` Benchmark 1: ./uv-main pip compile -q ./scripts/requirements/airflow.in Time (mean ± σ): 153.7 ms ± 3.5 ms [User: 165.2 ms, System: 157.6 ms] Range (min … max): 150.4 ms … 163.0 ms 19 runs Benchmark 2: ./uv-branch pip compile -q ./scripts/requirements/airflow.in Time (mean ± σ): 123.9 ms ± 4.6 ms [User: 152.4 ms, System: 133.8 ms] Range (min … max): 118.4 ms … 138.1 ms 24 runs Summary ./uv-branch pip compile -q ./scripts/requirements/airflow.in ran 1.24 ± 0.05 times faster than ./uv-main pip compile -q ./scripts/requirements/airflow.in ``` ``` Benchmark 1: ./uv-main pip compile -q ./scripts/requirements/boto3.in Time (mean ± σ): 327.0 ms ± 3.8 ms [User: 344.5 ms, System: 71.6 ms] Range (min … max): 322.7 ms … 334.6 ms 10 runs Benchmark 2: ./uv-branch pip compile -q ./scripts/requirements/boto3.in Time (mean ± σ): 311.2 ms ± 3.1 ms [User: 339.3 ms, System: 63.1 ms] Range (min … max): 307.8 ms … 317.0 ms 10 runs Summary ./uv-branch pip compile -q ./scripts/requirements/boto3.in ran 1.05 ± 0.02 times faster than ./uv-main pip compile -q ./scripts/requirements/boto3.in ``` <!-- Thank you for contributing to uv! To help us out with reviewing, please consider the following: - Does this pull request include a summary of the change? (See below.) - Does this pull request include a descriptive title? - Does this pull request include references to any relevant issues? -->
134 lines
5.6 KiB
Rust
134 lines
5.6 KiB
Rust
use std::cmp::Reverse;
|
|
|
|
use pubgrub::range::Range;
|
|
use rustc_hash::FxHashMap;
|
|
|
|
use pep440_rs::Version;
|
|
use uv_normalize::PackageName;
|
|
|
|
use crate::pubgrub::package::PubGrubPackage;
|
|
use crate::pubgrub::PubGrubPackageInner;
|
|
|
|
/// A prioritization map to guide the PubGrub resolution process.
|
|
///
|
|
/// During resolution, PubGrub needs to decide which package to consider next. The priorities
|
|
/// encoded here are used to guide that decision.
|
|
///
|
|
/// Like `pip`, we prefer packages that are pinned to direct URLs over packages pinned to a single
|
|
/// version over packages that are constrained in some way over packages that are unconstrained.
|
|
///
|
|
/// See: <https://github.com/pypa/pip/blob/ef78c129b1a966dbbbdb8ebfffc43723e89110d1/src/pip/_internal/resolution/resolvelib/provider.py#L120>
|
|
#[derive(Clone, Debug, Default)]
|
|
pub(crate) struct PubGrubPriorities(FxHashMap<PackageName, PubGrubPriority>);
|
|
|
|
impl PubGrubPriorities {
|
|
/// Add a [`PubGrubPackage`] to the priority map.
|
|
pub(crate) fn insert(&mut self, package: &PubGrubPackage, version: &Range<Version>) {
|
|
let next = self.0.len();
|
|
match &**package {
|
|
PubGrubPackageInner::Root(_) => {}
|
|
PubGrubPackageInner::Python(_) => {}
|
|
|
|
PubGrubPackageInner::Extra {
|
|
name, url: None, ..
|
|
}
|
|
| PubGrubPackageInner::Package {
|
|
name, url: None, ..
|
|
} => {
|
|
match self.0.entry(name.clone()) {
|
|
std::collections::hash_map::Entry::Occupied(mut entry) => {
|
|
// Preserve the original index.
|
|
let index = match entry.get() {
|
|
PubGrubPriority::Unspecified(Reverse(index)) => *index,
|
|
PubGrubPriority::Singleton(Reverse(index)) => *index,
|
|
PubGrubPriority::DirectUrl(Reverse(index)) => *index,
|
|
PubGrubPriority::Root => next,
|
|
};
|
|
|
|
// Compute the priority.
|
|
let priority = if version.as_singleton().is_some() {
|
|
PubGrubPriority::Singleton(Reverse(index))
|
|
} else {
|
|
PubGrubPriority::Unspecified(Reverse(index))
|
|
};
|
|
|
|
// Take the maximum of the new and existing priorities.
|
|
if priority > *entry.get() {
|
|
entry.insert(priority);
|
|
}
|
|
}
|
|
std::collections::hash_map::Entry::Vacant(entry) => {
|
|
// Insert the priority.
|
|
entry.insert(if version.as_singleton().is_some() {
|
|
PubGrubPriority::Singleton(Reverse(next))
|
|
} else {
|
|
PubGrubPriority::Unspecified(Reverse(next))
|
|
});
|
|
}
|
|
}
|
|
}
|
|
PubGrubPackageInner::Extra {
|
|
name, url: Some(_), ..
|
|
}
|
|
| PubGrubPackageInner::Package {
|
|
name, url: Some(_), ..
|
|
} => {
|
|
match self.0.entry(name.clone()) {
|
|
std::collections::hash_map::Entry::Occupied(mut entry) => {
|
|
// Preserve the original index.
|
|
let index = match entry.get() {
|
|
PubGrubPriority::Unspecified(Reverse(index)) => *index,
|
|
PubGrubPriority::Singleton(Reverse(index)) => *index,
|
|
PubGrubPriority::DirectUrl(Reverse(index)) => *index,
|
|
PubGrubPriority::Root => next,
|
|
};
|
|
|
|
// Compute the priority.
|
|
let priority = PubGrubPriority::DirectUrl(Reverse(index));
|
|
|
|
// Take the maximum of the new and existing priorities.
|
|
if priority > *entry.get() {
|
|
entry.insert(priority);
|
|
}
|
|
}
|
|
std::collections::hash_map::Entry::Vacant(entry) => {
|
|
// Insert the priority.
|
|
entry.insert(PubGrubPriority::DirectUrl(Reverse(next)));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Return the [`PubGrubPriority`] of the given package, if it exists.
|
|
pub(crate) fn get(&self, package: &PubGrubPackage) -> Option<PubGrubPriority> {
|
|
match &**package {
|
|
PubGrubPackageInner::Root(_) => Some(PubGrubPriority::Root),
|
|
PubGrubPackageInner::Python(_) => Some(PubGrubPriority::Root),
|
|
PubGrubPackageInner::Extra { name, .. } => self.0.get(name).copied(),
|
|
PubGrubPackageInner::Package { name, .. } => self.0.get(name).copied(),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
|
pub(crate) enum PubGrubPriority {
|
|
/// The package has no specific priority.
|
|
///
|
|
/// As such, its priority is based on the order in which the packages were added (FIFO), such
|
|
/// that the first package we visit is prioritized over subsequent packages.
|
|
///
|
|
/// TODO(charlie): Prefer constrained over unconstrained packages, if they're at the same depth
|
|
/// in the dependency graph.
|
|
Unspecified(Reverse<usize>),
|
|
|
|
/// The version range is constrained to a single version (e.g., with the `==` operator).
|
|
Singleton(Reverse<usize>),
|
|
|
|
/// The package was specified via a direct URL.
|
|
DirectUrl(Reverse<usize>),
|
|
|
|
/// The package is the root package.
|
|
Root,
|
|
}
|