Skip to content

Commit daa4be4

Browse files
committed
Rework build sorting in the new solver
Reuse the same build sorting logic as the original solver in order to get a deterministic result out of the solve. The solve can change drastically based on which build is selected first. Signed-off-by: J Robert Ray <jrray@jrray.org>
1 parent f3ed70c commit daa4be4

File tree

3 files changed

+212
-32
lines changed

3 files changed

+212
-32
lines changed

crates/spk-solve/crates/package-iterator/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@ mod error;
77
mod package_iterator;
88
mod promotion_patterns;
99

10+
pub use build_key::BuildKey;
1011
pub use error::{Error, Result};
1112
pub use package_iterator::{
1213
BuildIterator,
14+
BuildToSortedOptName,
1315
EmptyBuildIterator,
1416
PackageIterator,
1517
RepositoryPackageIterator,

crates/spk-solve/crates/package-iterator/src/package_iterator.rs

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -472,13 +472,13 @@ pub struct BuildToSortedOptName {}
472472

473473
impl BuildToSortedOptName {
474474
pub fn sort_builds<'a>(
475-
builds: impl Iterator<Item = &'a (Arc<Spec>, PackageSource)>,
475+
builds: impl Iterator<Item = &'a Arc<Spec>>,
476476
) -> (Vec<OptNameBuf>, HashMap<BuildIdent, OptionMap>) {
477477
let mut number_non_src_builds: u64 = 0;
478478
let mut build_name_values: HashMap<BuildIdent, OptionMap> = HashMap::default();
479479
let mut changes: HashMap<OptNameBuf, ChangeCounter> = HashMap::new();
480480

481-
for (build, _) in builds {
481+
for build in builds {
482482
// Skip this if it's a '/src' build because '/src' builds
483483
// won't use the build option values in their key, they
484484
// don't need to be looked at. They have a type of key
@@ -591,20 +591,9 @@ impl SortedBuildIterator {
591591
Ok(sbi)
592592
}
593593

594-
pub async fn new_from_builds(
595-
builds: VecDeque<BuildWithRepos>,
596-
builds_with_impossible_requests: HashMap<BuildIdent, Compatibility>,
597-
) -> Result<Self> {
598-
let mut sbi = SortedBuildIterator { builds };
599-
600-
sbi.sort_by_build_option_values(builds_with_impossible_requests)
601-
.await;
602-
Ok(sbi)
603-
}
604-
605594
/// Helper for making BuildKey structures used in the sorting in
606595
/// sort_by_build_option_values() below
607-
fn make_option_values_build_key(
596+
pub fn make_option_values_build_key(
608597
spec: &Spec,
609598
ordered_names: &Vec<OptNameBuf>,
610599
build_name_values: &HashMap<BuildIdent, OptionMap>,
@@ -632,8 +621,11 @@ impl SortedBuildIterator {
632621
) {
633622
let start = Instant::now();
634623

635-
let (key_entry_names, build_name_values) =
636-
BuildToSortedOptName::sort_builds(self.builds.iter().flat_map(|hm| hm.values()));
624+
let (key_entry_names, build_name_values) = BuildToSortedOptName::sort_builds(
625+
self.builds
626+
.iter()
627+
.flat_map(|hm| hm.values().map(|(spec, _src)| spec)),
628+
);
637629

638630
// Sort the builds by their generated keys generated from the
639631
// ordered names and values worth including.

crates/spk-solve/src/cdcl_solver/spk_provider.rs

Lines changed: 202 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@
55
use std::borrow::Cow;
66
use std::cell::RefCell;
77
use std::collections::{BTreeSet, HashMap, HashSet};
8+
use std::ops::Not;
89
use std::str::FromStr;
910
use std::sync::Arc;
1011

12+
use itertools::Itertools;
1113
use resolvo::utils::Pool;
1214
use resolvo::{
1315
Candidates,
@@ -38,7 +40,18 @@ use spk_schema::name::{OptNameBuf, PkgNameBuf};
3840
use spk_schema::prelude::{HasVersion, Named};
3941
use spk_schema::version::Version;
4042
use spk_schema::version_range::{parse_version_range, DoubleEqualsVersion, Ranged, VersionFilter};
41-
use spk_schema::{BuildIdent, Deprecate, Opt, OptionMap, Package, Recipe, Request, VersionIdent};
43+
use spk_schema::{
44+
BuildIdent,
45+
Deprecate,
46+
Opt,
47+
OptionMap,
48+
Package,
49+
Recipe,
50+
Request,
51+
Spec,
52+
VersionIdent,
53+
};
54+
use spk_solve_package_iterator::{BuildKey, BuildToSortedOptName, SortedBuildIterator};
4255
use spk_storage::RepositoryHandle;
4356
use tracing::{debug_span, Instrument};
4457

@@ -337,6 +350,51 @@ enum CanBuildFromSource {
337350
No(StringId),
338351
}
339352

353+
/// An iterator that yields slices of items that fall into the same partition.
354+
///
355+
/// The partition is determined by the key function.
356+
/// The items must be already sorted in ascending order by the key function.
357+
struct PartitionIter<'a, I, F, K>
358+
where
359+
F: for<'i> Fn(&'i I) -> K,
360+
K: PartialOrd,
361+
{
362+
slice: &'a [I],
363+
key_fn: F,
364+
}
365+
366+
impl<'a, I, F, K> PartitionIter<'a, I, F, K>
367+
where
368+
F: for<'i> Fn(&'i I) -> K,
369+
K: PartialOrd,
370+
{
371+
fn new(slice: &'a [I], key_fn: F) -> Self {
372+
Self { slice, key_fn }
373+
}
374+
}
375+
376+
impl<'a, I, F, K> Iterator for PartitionIter<'a, I, F, K>
377+
where
378+
F: for<'i> Fn(&'i I) -> K,
379+
K: PartialOrd,
380+
{
381+
type Item = &'a [I];
382+
383+
fn next(&mut self) -> Option<Self::Item> {
384+
let element = self.slice.first()?;
385+
386+
// Is a binary search overkill?
387+
let partition_key = (self.key_fn)(element);
388+
// No need to check the first element again.
389+
let p =
390+
1 + self.slice[1..].partition_point(|element| (self.key_fn)(element) <= partition_key);
391+
392+
let part = &self.slice[..p];
393+
self.slice = &self.slice[p..];
394+
Some(part)
395+
}
396+
}
397+
340398
pub(crate) struct SpkProvider {
341399
pub(crate) pool: Pool<RequestVS, ResolvoPackageName>,
342400
repos: Vec<Arc<RepositoryHandle>>,
@@ -564,6 +622,21 @@ impl SpkProvider {
564622
self.cancel_solving.borrow().is_some()
565623
}
566624

625+
/// Return an iterator that yields slices of builds that are from the same
626+
/// package version.
627+
///
628+
/// The provided builds must already be sorted otherwise the behavior is
629+
/// undefined.
630+
fn find_version_runs<'a>(
631+
builds: &'a [(SolvableId, &'a LocatedBuildIdentWithComponent, Arc<Spec>)],
632+
) -> impl Iterator<Item = &'a [(SolvableId, &'a LocatedBuildIdentWithComponent, Arc<Spec>)]>
633+
{
634+
PartitionIter::new(builds, |(_, ident, _)| {
635+
// partition by (name, version) ignoring repository
636+
(ident.ident.name(), ident.ident.version())
637+
})
638+
}
639+
567640
fn request_to_known_dependencies(&self, requirement: &Request) -> KnownDependencies {
568641
let mut known_deps = KnownDependencies::default();
569642
match requirement {
@@ -657,6 +730,44 @@ impl SpkProvider {
657730
}
658731
}
659732

733+
/// Order two builds based on which should be preferred to include in a
734+
/// solve as a candidate.
735+
///
736+
/// Generally this means a build with newer dependencies is ordered first.
737+
fn sort_builds(
738+
&self,
739+
build_key_index: &HashMap<SolvableId, BuildKey>,
740+
a: (SolvableId, &LocatedBuildIdentWithComponent),
741+
b: (SolvableId, &LocatedBuildIdentWithComponent),
742+
) -> std::cmp::Ordering {
743+
// This function should _not_ return `std::cmp::Ordering::Equal` unless
744+
// `a` and `b` are the same build (in practice this function will never
745+
// be called when that is true).
746+
747+
// Embedded stubs are always ordered last.
748+
match (a.1.ident.is_embedded(), b.1.ident.is_embedded()) {
749+
(true, false) => return std::cmp::Ordering::Greater,
750+
(false, true) => return std::cmp::Ordering::Less,
751+
_ => {}
752+
};
753+
754+
match (build_key_index.get(&a.0), build_key_index.get(&b.0)) {
755+
(Some(a_key), Some(b_key)) => {
756+
// BuildKey orders in reverse order from what is needed here.
757+
return b_key.cmp(a_key);
758+
}
759+
(Some(_), None) => return std::cmp::Ordering::Less,
760+
(None, Some(_)) => return std::cmp::Ordering::Greater,
761+
_ => {}
762+
};
763+
764+
// If neither build has a key, both packages failed to load?
765+
// Add debug assert to see if this ever happens.
766+
debug_assert!(false, "builds without keys");
767+
768+
a.1.ident.cmp(&b.1.ident)
769+
}
770+
660771
pub fn var_requirements(&mut self, requests: &[Request]) -> Vec<VersionSetId> {
661772
self.global_var_requests.reserve(requests.len());
662773
requests
@@ -967,13 +1078,94 @@ impl DependencyProvider for SpkProvider {
9671078
}
9681079

9691080
async fn sort_candidates(&self, _solver: &SolverCache<Self>, solvables: &mut [SolvableId]) {
970-
// This implementation just picks the highest version.
1081+
// Goal: Create a `BuildKey` for each build in `solvables`.
1082+
// The `BuildKey` factory needs as input the output from
1083+
// `BuildToSortedOptName::sort_builds`.
1084+
// `BuildToSortedOptName::sort_builds` needs to be fed builds from the
1085+
// same version.
1086+
// `solvables` can be builds from various versions so they need to be
1087+
// grouped by version.
1088+
let build_solvables = solvables
1089+
.iter()
1090+
.filter_map(|solvable_id| {
1091+
let solvable = self.pool.resolve_solvable(*solvable_id);
1092+
match &solvable.record {
1093+
SpkSolvable::LocatedBuildIdentWithComponent(
1094+
located_build_ident_with_component,
1095+
) =>
1096+
// sorting the source build (if any) is handled
1097+
// elsewhere; skip source builds.
1098+
{
1099+
located_build_ident_with_component
1100+
.ident
1101+
.is_source()
1102+
.not()
1103+
.then_some((*solvable_id, located_build_ident_with_component))
1104+
}
1105+
_ => None,
1106+
}
1107+
})
1108+
.sorted_by(
1109+
|(_, LocatedBuildIdentWithComponent { ident: a, .. }),
1110+
(_, LocatedBuildIdentWithComponent { ident: b, .. })| {
1111+
// build_solvables will be ordered by (pkg, version, build).
1112+
a.target().cmp(b.target())
1113+
},
1114+
)
1115+
.collect::<Vec<_>>();
1116+
1117+
// `BuildToSortedOptName::sort_builds` will need the package specs.
1118+
let mut build_solvables_and_specs = Vec::with_capacity(build_solvables.len());
1119+
for build_solvable in build_solvables {
1120+
let (solvable_id, located_build_ident_with_component) = build_solvable;
1121+
let repo = self
1122+
.repos
1123+
.iter()
1124+
.find(|repo| {
1125+
repo.name() == located_build_ident_with_component.ident.repository_name()
1126+
})
1127+
.expect("Expected solved package's repository to be in the list of repositories");
1128+
let Ok(package) = repo
1129+
.read_package(located_build_ident_with_component.ident.target())
1130+
.await
1131+
else {
1132+
// Any builds that can't load the spec will be sorted to the
1133+
// end. In most cases the package spec would already be loaded
1134+
// in cache at this point.
1135+
continue;
1136+
};
1137+
build_solvables_and_specs.push((
1138+
solvable_id,
1139+
located_build_ident_with_component,
1140+
package,
1141+
));
1142+
}
1143+
1144+
let mut build_key_index = HashMap::new();
1145+
build_key_index.reserve(build_solvables_and_specs.len());
1146+
1147+
// Find runs of the same package version.
1148+
for version_run in SpkProvider::find_version_runs(&build_solvables_and_specs) {
1149+
let (ordered_names, build_name_values) =
1150+
BuildToSortedOptName::sort_builds(version_run.iter().map(|(_, _, spec)| spec));
1151+
1152+
for (solvable_id, _, spec) in version_run {
1153+
let build_key = SortedBuildIterator::make_option_values_build_key(
1154+
spec,
1155+
&ordered_names,
1156+
&build_name_values,
1157+
false,
1158+
);
1159+
build_key_index.insert(*solvable_id, build_key);
1160+
}
1161+
}
1162+
9711163
// TODO: The ordering should take component names into account, so
9721164
// the run component or the build component is tried first in the
9731165
// appropriate situations.
974-
solvables.sort_by(|a, b| {
975-
let a = self.pool.resolve_solvable(*a);
976-
let b = self.pool.resolve_solvable(*b);
1166+
solvables.sort_by(|solvable_id_a, solvable_id_b| {
1167+
let a = self.pool.resolve_solvable(*solvable_id_a);
1168+
let b = self.pool.resolve_solvable(*solvable_id_b);
9771169
match (&a.record, &b.record) {
9781170
(
9791171
SpkSolvable::LocatedBuildIdentWithComponent(a),
@@ -1005,17 +1197,11 @@ impl DependencyProvider for SpkProvider {
10051197
(_, Build::Source) => return std::cmp::Ordering::Less,
10061198
_ => {}
10071199
};
1008-
// Sort embedded packges second last
1009-
match (a.ident.build(), b.ident.build()) {
1010-
(Build::Embedded(_), Build::Embedded(_)) => {
1011-
// TODO: Could perhaps sort on the parent
1012-
// package to prefer a newer parent.
1013-
std::cmp::Ordering::Equal
1014-
}
1015-
(Build::Embedded(_), _) => std::cmp::Ordering::Greater,
1016-
(_, Build::Embedded(_)) => std::cmp::Ordering::Less,
1017-
_ => std::cmp::Ordering::Equal,
1018-
}
1200+
self.sort_builds(
1201+
&build_key_index,
1202+
(*solvable_id_a, a),
1203+
(*solvable_id_b, b),
1204+
)
10191205
}
10201206
ord => ord,
10211207
}

0 commit comments

Comments
 (0)