1use 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#[derive(clap::Args)]
56pub struct VmmPerfCli {
57 #[clap(long, value_enum)]
59 target: Option<VmmPerfTargetCli>,
60
61 #[clap(long, value_enum)]
63 profile: Vec<VmmPerfProfileCli>,
64
65 #[clap(long)]
67 dir: Option<PathBuf>,
68
69 #[clap(long = "vmm-perf-vmsizes", value_name = "KEY=VALUE,...")]
71 vmm_perf_vm_sizes: Vec<String>,
72
73 #[clap(long, value_name = "KEY=VALUE")]
75 vmm_perf_parameter: Vec<String>,
76
77 #[clap(long = "vmm-perf-runtime-archive", value_name = "PATH")]
79 vmm_perf_runtime_archive: Option<PathBuf>,
80
81 #[clap(long)]
83 release: bool,
84
85 #[clap(long)]
87 build_only: bool,
88
89 #[clap(long)]
91 install_missing_deps: bool,
92
93 #[clap(long)]
95 custom_uefi_firmware: Option<PathBuf>,
96
97 #[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(¶meters)?))
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}