Skip to main content

flowey_hvlite/pipelines/
vmm_perf.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Build and run the standalone VMM.Perf harness locally.
5
6use anyhow::Context as _;
7use flowey::node::prelude::ReadVar;
8use flowey::pipeline::prelude::*;
9use flowey_lib_hvlite::common::CommonArch;
10use flowey_lib_hvlite::common::CommonPlatform;
11use flowey_lib_hvlite::common::CommonProfile;
12use flowey_lib_hvlite::common::CommonTriple;
13use flowey_lib_hvlite::run_vmm_perf::VmmPerfProfile;
14use std::collections::BTreeMap;
15use std::path::PathBuf;
16
17#[derive(clap::ValueEnum, Clone, Copy)]
18enum VmmPerfTargetCli {
19    LinuxX64Gnu,
20    LinuxX64Musl,
21    WindowsX64,
22}
23
24impl VmmPerfTargetCli {
25    fn triple(self) -> CommonTriple {
26        CommonTriple::Common {
27            arch: CommonArch::X86_64,
28            platform: match self {
29                Self::LinuxX64Gnu => CommonPlatform::LinuxGnu,
30                Self::LinuxX64Musl => CommonPlatform::LinuxMusl,
31                Self::WindowsX64 => CommonPlatform::WindowsMsvc,
32            },
33        }
34    }
35}
36
37#[derive(clap::ValueEnum, Clone, Copy)]
38enum VmmPerfProfileCli {
39    BootTime,
40    Fio,
41    Iperf3,
42}
43
44impl From<VmmPerfProfileCli> for VmmPerfProfile {
45    fn from(value: VmmPerfProfileCli) -> Self {
46        match value {
47            VmmPerfProfileCli::BootTime => Self::BootTime,
48            VmmPerfProfileCli::Fio => Self::Fio,
49            VmmPerfProfileCli::Iperf3 => Self::Iperf3,
50        }
51    }
52}
53
54/// Build and run VMM.Perf without the Petri/nextest test harness.
55#[derive(clap::Args)]
56pub struct VmmPerfCli {
57    /// Target for the OpenVMM and VMM.Perf runner binaries.
58    #[clap(long, value_enum)]
59    target: Option<VmmPerfTargetCli>,
60
61    /// Run only the selected profile. May be repeated; defaults to all profiles.
62    #[clap(long, value_enum)]
63    profile: Vec<VmmPerfProfileCli>,
64
65    /// Root directory for benchmark scratch files and results.
66    #[clap(long)]
67    dir: Option<PathBuf>,
68
69    /// Replace the default VM matrix with one parameter set.
70    #[clap(long = "vmm-perf-vmsizes", value_name = "KEY=VALUE,...")]
71    vmm_perf_vm_sizes: Vec<String>,
72
73    /// Override a VMM.Perf parameter on every selected configuration.
74    #[clap(long, value_name = "KEY=VALUE")]
75    vmm_perf_parameter: Vec<String>,
76
77    /// Use a local VMM.Perf runtime archive instead of downloading the pinned version.
78    #[clap(long = "vmm-perf-runtime-archive", value_name = "PATH")]
79    vmm_perf_runtime_archive: Option<PathBuf>,
80
81    /// Release build instead of debug build.
82    #[clap(long)]
83    release: bool,
84
85    /// Build OpenVMM and the runner without executing benchmarks.
86    #[clap(long)]
87    build_only: bool,
88
89    /// Automatically install missing build dependencies.
90    #[clap(long)]
91    install_missing_deps: bool,
92
93    /// Use a local MSVM.fd instead of the configured mu_msvm release.
94    #[clap(long)]
95    custom_uefi_firmware: Option<PathBuf>,
96
97    /// Enable verbose Flowey output.
98    #[clap(long)]
99    verbose: bool,
100}
101
102impl IntoPipeline for VmmPerfCli {
103    fn into_pipeline(self, backend_hint: PipelineBackendHint) -> anyhow::Result<Pipeline> {
104        if !matches!(backend_hint, PipelineBackendHint::Local) {
105            anyhow::bail!("vmm-perf is for local use only")
106        }
107
108        let Self {
109            target,
110            profile,
111            dir,
112            vmm_perf_vm_sizes,
113            vmm_perf_parameter,
114            vmm_perf_runtime_archive,
115            release,
116            build_only,
117            install_missing_deps,
118            custom_uefi_firmware,
119            verbose,
120        } = self;
121
122        let host_platform = FlowPlatform::host(backend_hint);
123        anyhow::ensure!(
124            matches!(FlowArch::host(backend_hint), FlowArch::X86_64),
125            "vmm-perf currently requires an x64 host"
126        );
127        let target = target.unwrap_or(match host_platform {
128            FlowPlatform::Linux(_) => VmmPerfTargetCli::LinuxX64Gnu,
129            FlowPlatform::Windows => VmmPerfTargetCli::WindowsX64,
130            platform => anyhow::bail!("vmm-perf does not support a {platform:?} host"),
131        });
132        anyhow::ensure!(
133            matches!(
134                (host_platform, target),
135                (
136                    FlowPlatform::Linux(_),
137                    VmmPerfTargetCli::LinuxX64Gnu | VmmPerfTargetCli::LinuxX64Musl
138                ) | (FlowPlatform::Windows, VmmPerfTargetCli::WindowsX64)
139            ),
140            "the selected VMM.Perf target does not match the host platform"
141        );
142
143        let vm_sizes_json = serialize_vm_sizes(&vmm_perf_vm_sizes)?;
144        let parameters_json = serialize_parameters(&vmm_perf_parameter)?;
145        let profiles = if profile.is_empty() {
146            VmmPerfProfile::all()
147        } else {
148            profile.into_iter().map(Into::into).collect()
149        };
150        let root_dir = std::path::absolute(
151            dir.unwrap_or_else(|| crate::repo_root().join("target").join("vmm_perf")),
152        )
153        .context("failed to resolve VMM.Perf root directory")?;
154        std::fs::create_dir_all(&root_dir)?;
155        let runtime_archive = vmm_perf_runtime_archive
156            .map(|path| {
157                let path = std::path::absolute(path)
158                    .context("failed to resolve local VMM.Perf runtime archive")?;
159                anyhow::ensure!(
160                    path.is_file(),
161                    "local VMM.Perf runtime archive does not exist or is not a file: {}",
162                    path.display()
163                );
164                Ok(path)
165            })
166            .transpose()?;
167
168        let openvmm_repo = flowey_lib_common::git_checkout::RepoSource::ExistingClone(
169            ReadVar::from_static(crate::repo_root()),
170        );
171        let mut pipeline = Pipeline::new();
172        let mut job = pipeline
173            .new_job(
174                FlowPlatform::host(backend_hint),
175                FlowArch::host(backend_hint),
176                "build and run VMM.Perf",
177            )
178            .dep_on(|_| flowey_lib_hvlite::_jobs::cfg_versions::Request::Init);
179
180        if let Some(firmware) = custom_uefi_firmware {
181            job = job.dep_on(move |_| {
182                flowey_lib_hvlite::_jobs::cfg_versions::Request::LocalUefi(
183                    CommonArch::X86_64,
184                    ReadVar::from_static(firmware),
185                )
186            });
187        }
188
189        job.dep_on(
190            |_| flowey_lib_hvlite::_jobs::cfg_hvlite_reposource::Params {
191                hvlite_repo_source: openvmm_repo,
192            },
193        )
194        .dep_on(|_| flowey_lib_hvlite::_jobs::cfg_common::Params {
195            local_only: Some(flowey_lib_hvlite::_jobs::cfg_common::LocalOnlyParams {
196                interactive: true,
197                auto_install: install_missing_deps,
198                ignore_rust_version: true,
199            }),
200            verbose: ReadVar::from_static(verbose),
201            locked: false,
202            deny_warnings: false,
203            no_incremental: false,
204        })
205        .dep_on(
206            |ctx| flowey_lib_hvlite::_jobs::local_build_and_run_vmm_perf::Params {
207                target: target.triple(),
208                profile: CommonProfile::from_release(release),
209                root_dir,
210                profiles,
211                vm_sizes_json,
212                parameters_json,
213                runtime_archive,
214                build_only,
215                done: ctx.new_done_handle(),
216            },
217        )
218        .finish();
219
220        Ok(pipeline)
221    }
222}
223
224fn serialize_vm_sizes(vm_sizes: &[String]) -> anyhow::Result<Option<String>> {
225    if vm_sizes.is_empty() {
226        return Ok(None);
227    }
228
229    let mut parsed = Vec::with_capacity(vm_sizes.len());
230    for (index, vm_size) in vm_sizes.iter().enumerate() {
231        let parameters = parse_parameter_set(
232            vm_size.split(','),
233            &format!("--vmm-perf-vmsizes at position {}", index + 1),
234        )?;
235        parsed.push(serde_json::json!({ "parameters": parameters }));
236    }
237    Ok(Some(serde_json::to_string(&parsed)?))
238}
239
240fn serialize_parameters(parameters: &[String]) -> anyhow::Result<Option<String>> {
241    if parameters.is_empty() {
242        return Ok(None);
243    }
244    let parameters = parse_parameter_set(
245        parameters.iter().map(String::as_str),
246        "--vmm-perf-parameter",
247    )?;
248    Ok(Some(serde_json::to_string(&parameters)?))
249}
250
251fn parse_parameter_set<'a>(
252    values: impl IntoIterator<Item = &'a str>,
253    context: &str,
254) -> anyhow::Result<BTreeMap<String, String>> {
255    let mut parameters = BTreeMap::new();
256    for value in values {
257        let (name, value) = value
258            .split_once('=')
259            .ok_or_else(|| anyhow::anyhow!("{context} value {value:?} must use KEY=VALUE"))?;
260        let name = name.trim();
261        let value = value.trim();
262        anyhow::ensure!(
263            !name.is_empty(),
264            "{context} contains an empty parameter name"
265        );
266        anyhow::ensure!(
267            parameters
268                .insert(name.to_owned(), value.to_owned())
269                .is_none(),
270            "{context} contains duplicate parameter {name:?}"
271        );
272    }
273    anyhow::ensure!(!parameters.is_empty(), "{context} cannot be empty");
274    Ok(parameters)
275}