Skip to content

Commit

Permalink
feat: define default CPUs for x86-64 using microarchitecture levels
Browse files Browse the repository at this point in the history
To limit build times, we define sane defaults for `x86-64` that relies
on the microarchitecture levels.
Now, unless set with `[package.multivers.x86-64]` or `--cpus`, only a
limited number of build will be used for `x86-64`.

See #6
  • Loading branch information
ronnychevalier committed Jul 26, 2024
1 parent 9bdb9aa commit 3a5ff09
Show file tree
Hide file tree
Showing 7 changed files with 260 additions and 66 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/).

## [Unreleased]

### Added

- Define default CPUs for `x86-64` using [microarchitecture levels](https://en.wikipedia.org/wiki/X86-64#Microarchitecture_levels): `x86-64,x86-64-v2,x86-64-v3,x86-64-v4`. It helps to limit build times ([#6](https://github.com/ronnychevalier/cargo-multivers/issues/6)).

### Changed

- Update dependencies.
Expand Down
65 changes: 42 additions & 23 deletions src/features.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use rayon::prelude::{IntoParallelIterator, ParallelIterator};

use itertools::Itertools;

use crate::metadata::{MultiversMetadata, TargetMetadata};
use crate::rustc::Rustc;

#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
Expand Down Expand Up @@ -40,11 +41,8 @@ pub struct Cpus {
}

impl Cpus {
pub fn builder<'a>(
target: impl Into<String>,
cpus: Option<Vec<String>>,
) -> anyhow::Result<CpusBuilder<'a>> {
CpusBuilder::new(target, cpus)
pub fn builder(target: impl Into<String>) -> anyhow::Result<CpusBuilder> {
CpusBuilder::new(target)
}

/// Returns a sorted and deduplicated iterator of CPU features set for each CPU
Expand All @@ -67,44 +65,65 @@ impl Cpus {
}
}

pub struct CpusBuilder<'a> {
iter: rayon::vec::IntoIter<String>,
excluded_features: Option<&'a [String]>,
#[derive(Clone)]
pub struct CpusBuilder {
excluded_features: Option<Vec<String>>,
target: String,
triple: Triple,
cpus: Option<Vec<String>>,
metadata_cpus: Option<Vec<String>>,
}

impl<'a> CpusBuilder<'a> {
pub fn new(target: impl Into<String>, cpus: Option<Vec<String>>) -> anyhow::Result<Self> {
impl CpusBuilder {
pub fn new(target: impl Into<String>) -> anyhow::Result<Self> {
let target = target.into();
let triple = Triple::from_str(&target).context("Failed to parse the target")?;
let iter = if let Some(cpus) = cpus {
cpus
} else {
Rustc::cpus_from_target(&target)
.context("Failed to get the set of CPUs for the target")?
}
.into_par_iter();

Ok(Self {
iter,
excluded_features: None,
target,
triple,
cpus: None,
metadata_cpus: None,
})
}

pub fn cpus(mut self, cpus: impl Into<Option<Vec<String>>>) -> Self {
self.cpus = cpus.into();
self
}

pub fn metadata(mut self, metadata: &MultiversMetadata) -> anyhow::Result<Self> {
if let Some(cpus) = metadata
.get(&self.triple.architecture)
.and_then(TargetMetadata::cpus)
{
anyhow::ensure!(!cpus.is_empty(), "Empty list of CPUs");

self.metadata_cpus = Some(cpus.to_vec());
}

Ok(self)
}

/// Excludes the given list of CPU features when building the set of CPUs and their features.
///
/// If a CPU no longer has any feature, it is not included in the final list.
pub fn exclude_features(mut self, cpu_features: impl Into<Option<&'a [String]>>) -> Self {
pub fn exclude_features(mut self, cpu_features: impl Into<Option<Vec<String>>>) -> Self {
self.excluded_features = cpu_features.into();
self
}

pub fn build(self) -> anyhow::Result<Cpus> {
let features: BTreeMap<_, _> = self
.iter
pub fn build(&self) -> anyhow::Result<Cpus> {
let iter = if let Some(cpus) = self.cpus.as_ref().or(self.metadata_cpus.as_ref()) {
cpus.to_owned()
} else {
Rustc::cpus_from_target(&self.target)
.context("Failed to get the set of CPUs for the target")?
}
.into_par_iter();

let features: BTreeMap<_, _> = iter
.filter(|cpu| Self::is_cpu_for_target_valid(&self.triple, cpu))
.map(|cpu| {
let features = Rustc::features_from_cpu(&self.target, &cpu)?;
Expand All @@ -116,7 +135,7 @@ impl<'a> CpusBuilder<'a> {
let features = features
.into_iter()
.filter_map(|(cpu, mut features)| {
for exclude in self.excluded_features.into_iter().flatten() {
for exclude in self.excluded_features.iter().flatten() {
features.remove(exclude);
}
if features.is_empty() {
Expand Down
3 changes: 2 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,9 @@ fn main() -> anyhow::Result<()> {
if matches!(args.print, Some(Print::CpuFeatures)) {
let target = args.target()?.into_owned();

let cpus = Cpus::builder(target, args.cpus)
let cpus = Cpus::builder(target)
.context("Failed to get the set of CPU features for the target")?
.cpus(args.cpus)
.build()?;
let mut stdout = std::io::stdout().lock();
for feature in cpus.features() {
Expand Down
201 changes: 200 additions & 1 deletion src/metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ impl<'de> Deserialize<'de> for ArchitectureWrapper {
}

/// The options set for a given target
#[derive(PartialEq, Eq, Hash, Debug)]
#[derive(PartialEq, Eq, Hash, Debug, Clone)]
pub struct TargetMetadata {
cpus: Option<Vec<String>>,
}
Expand Down Expand Up @@ -68,7 +68,42 @@ pub struct MultiversMetadata {
targets: HashMap<Architecture, TargetMetadata>,
}

impl Default for MultiversMetadata {
fn default() -> Self {
let targets = HashMap::from([(
Architecture::X86_64,
TargetMetadata {
cpus: Some(vec![
"x86-64".into(),
"x86-64-v2".into(),
"x86-64-v3".into(),
"x86-64-v4".into(),
]),
},
)]);

Self { targets }
}
}

impl MultiversMetadata {
/// Parses the multivers metadata from a [`Package`] and use default values if not set
pub fn from_package_with_default(package: &Package) -> anyhow::Result<Self> {
Self::from_value_with_default(&package.metadata)
}

/// Parses the multivers metadata from a [`Value`] and use default values if not set
pub fn from_value_with_default(value: &Value) -> anyhow::Result<Self> {
let Some(from_value) = Self::from_value(value)? else {
return Ok(Self::default());
};

let mut default = Self::default();
default.update(&from_value);

Ok(default)
}

/// Parses the multivers metadata from a [`Package`].
pub fn from_package(package: &Package) -> anyhow::Result<Option<Self>> {
Self::from_value(&package.metadata)
Expand Down Expand Up @@ -103,6 +138,12 @@ impl MultiversMetadata {
pub fn get(&self, k: &Architecture) -> Option<&TargetMetadata> {
self.targets.get(k)
}

pub fn update(&mut self, other: &Self) {
for (k, v) in other.targets.clone() {
self.targets.insert(k, v);
}
}
}

#[cfg(test)]
Expand Down Expand Up @@ -224,4 +265,162 @@ mod tests {

assert_eq!(MultiversMetadata::from_value(&value).unwrap(), None);
}

#[test]
fn test_target_default_update() {
let mut default: MultiversMetadata = MultiversMetadata::default();
let target: &TargetMetadata = default.get(&Architecture::X86_64).unwrap();
assert_eq!(
target.cpus(),
Some(
&[
"x86-64".into(),
"x86-64-v2".into(),
"x86-64-v3".into(),
"x86-64-v4".into(),
][..]
)
);

let value = json!({
"multivers": {
"x86_64": {
"cpus": ["alderlake", "skylake", "sandybridge", "ivybridge"]
}
}
});
let metadata = MultiversMetadata::from_value(&value).unwrap().unwrap();
default.update(&metadata);
assert_eq!(
default,
MultiversMetadata {
targets: HashMap::from([(
Architecture::X86_64,
TargetMetadata {
cpus: Some(vec![
"alderlake".into(),
"skylake".into(),
"sandybridge".into(),
"ivybridge".into()
])
}
),])
}
);

let value = json!({
"multivers": {
"powerpc": {
}
}
});
let metadata = MultiversMetadata::from_value(&value).unwrap().unwrap();
default.update(&metadata);
assert_eq!(
default,
MultiversMetadata {
targets: HashMap::from([
(
Architecture::X86_64,
TargetMetadata {
cpus: Some(vec![
"alderlake".into(),
"skylake".into(),
"sandybridge".into(),
"ivybridge".into()
])
}
),
(Architecture::Powerpc, TargetMetadata { cpus: None })
])
}
);

let value = json!({
"multivers": {
"x86_64": {
}
}
});
let metadata = MultiversMetadata::from_value(&value).unwrap().unwrap();
default.update(&metadata);
assert_eq!(
default,
MultiversMetadata {
targets: HashMap::from([
(Architecture::X86_64, TargetMetadata { cpus: None }),
(Architecture::Powerpc, TargetMetadata { cpus: None })
])
}
);
}

#[test]
fn test_from_value_with_default() {
let value = json!({
"multivers": {
"x86_64": {
"cpus": ["alderlake", "skylake", "sandybridge", "ivybridge"]
}
}
});
let metadata = MultiversMetadata::from_value_with_default(&value).unwrap();
assert_eq!(
metadata,
MultiversMetadata {
targets: HashMap::from([(
Architecture::X86_64,
TargetMetadata {
cpus: Some(vec![
"alderlake".into(),
"skylake".into(),
"sandybridge".into(),
"ivybridge".into()
])
}
),])
}
);

let value = json!({
"multivers": {
"powerpc": {
}
}
});
let metadata = MultiversMetadata::from_value_with_default(&value).unwrap();
assert_eq!(
metadata,
MultiversMetadata {
targets: HashMap::from([
(
Architecture::X86_64,
TargetMetadata {
cpus: Some(vec![
"x86-64".into(),
"x86-64-v2".into(),
"x86-64-v3".into(),
"x86-64-v4".into(),
])
}
),
(Architecture::Powerpc, TargetMetadata { cpus: None })
])
}
);

let value = json!({
"multivers": {
"x86_64": {
}
}
});
let metadata = MultiversMetadata::from_value_with_default(&value).unwrap();
assert_eq!(
metadata,
MultiversMetadata {
targets: HashMap::from([(Architecture::X86_64, TargetMetadata { cpus: None }),])
}
);
}
}
Loading

0 comments on commit 3a5ff09

Please sign in to comment.