Skip to content

Commit 09cb178

Browse files
authored
Optimize runtime of test-artifacts build script (#13089)
* Convert core wasms to components in parallel * Skip the core wasm to component part if the artifacts are up-to-date (based on mtime) * Build the C/C++ programs in parallel. The goal here is to pick some low-hanging fruit to prevent this from being such a bottleneck in local development, but there's more that can be done if necessary (e.g. reading the dep files and calculating that all manually). Right now for example the longer steps are invoking Cargo which does nothing and invoking the C/C++ compilers unconditionally, but solving that makes this more of a "build a build system" script and I feel like we haven't quite crossed that threshold yet.
1 parent 4426555 commit 09cb178

File tree

3 files changed

+103
-46
lines changed

3 files changed

+103
-46
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/test-programs/artifacts/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,4 @@ cargo_metadata = "0.19.2"
2121
wasmtime-test-util = { workspace = true, features = ['wast'] }
2222
serde_derive = { workspace = true }
2323
serde = { workspace = true }
24+
rayon = { workspace = true }

crates/test-programs/artifacts/build.rs

Lines changed: 101 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
use heck::*;
2+
use rayon::prelude::*;
23
use std::collections::{BTreeMap, HashSet};
34
use std::env;
45
use std::fs;
56
use std::path::{Path, PathBuf};
67
use std::process::Command;
8+
use std::time::SystemTime;
79
use wit_component::ComponentEncoder;
810

911
fn main() {
@@ -62,6 +64,7 @@ impl Artifacts {
6264
let mut kinds = BTreeMap::new();
6365
let missing_sdk_path =
6466
PathBuf::from("Asset not compiled, WASI_SDK_PATH missing at compile time");
67+
let mut components = Vec::new();
6568
for test in tests.iter() {
6669
let shouty_snake = test.name.to_shouty_snake_case();
6770
let snake = test.name.to_snake_case();
@@ -122,14 +125,20 @@ impl Artifacts {
122125
{
123126
continue;
124127
}
125-
let adapter = match test.name.as_str() {
128+
let (adapter, mtime) = match test.name.as_str() {
126129
"reactor" => &reactor_adapter,
127130
s if s.starts_with("p3_") => &reactor_adapter,
128131
s if s.starts_with("p2_api_proxy") => &proxy_adapter,
129132
_ => &command_adapter,
130133
};
131134
let path = match &test.core_wasm {
132-
Some(path) => self.compile_component(path, adapter),
135+
Some(path) => {
136+
let out_dir = path.parent().unwrap();
137+
let stem = path.file_stem().unwrap().to_str().unwrap();
138+
let component_path = out_dir.join(format!("{stem}.component.wasm"));
139+
components.push((path, adapter, mtime.as_ref(), component_path.clone()));
140+
component_path
141+
}
133142
None => missing_sdk_path.clone(),
134143
};
135144
generated_code +=
@@ -141,6 +150,12 @@ impl Artifacts {
141150
);
142151
}
143152

153+
components
154+
.par_iter()
155+
.for_each(|(wasm, adapter, mtime, component_path)| {
156+
self.compile_component(wasm, adapter, *mtime, component_path);
157+
});
158+
144159
for (kind, targets) in kinds {
145160
generated_code += &format!("#[macro_export]");
146161
generated_code += &format!("macro_rules! foreach_{kind} {{\n");
@@ -206,13 +221,15 @@ impl Artifacts {
206221
generated_code: &mut String,
207222
name: &str,
208223
features: &[&str],
209-
) -> Vec<u8> {
224+
) -> (Vec<u8>, Option<SystemTime>) {
210225
let mut cmd = cargo();
211226
cmd.arg("build")
212227
.arg("--release")
228+
.arg("-vv")
213229
.arg("--package=wasi-preview1-component-adapter")
214230
.arg("--target=wasm32-unknown-unknown")
215-
.env("CARGO_TARGET_DIR", &self.out_dir)
231+
.env("CARGO_BUILD_BUILD_DIR", &self.out_dir)
232+
.env("CARGO_TARGET_DIR", &self.out_dir.join(name))
216233
.env("RUSTFLAGS", rustflags())
217234
.env_remove("CARGO_ENCODED_RUSTFLAGS");
218235
for f in features {
@@ -224,24 +241,55 @@ impl Artifacts {
224241

225242
let artifact = self
226243
.out_dir
244+
.join(name)
227245
.join("wasm32-unknown-unknown")
228246
.join("release")
229247
.join("wasi_snapshot_preview1.wasm");
230248
let adapter = self
231249
.out_dir
232250
.join(format!("wasi_snapshot_preview1.{name}.wasm"));
233-
std::fs::copy(&artifact, &adapter).unwrap();
251+
252+
if let Ok(prev) = std::fs::read(&adapter)
253+
&& let Ok(cur) = std::fs::read(&artifact)
254+
&& prev == cur
255+
{
256+
// nothing to do ...
257+
} else {
258+
if adapter.exists() {
259+
fs::remove_file(&adapter).unwrap();
260+
}
261+
std::fs::hard_link(&artifact, &adapter)
262+
.or_else(|_| std::fs::copy(&artifact, &adapter).map(|_| ()))
263+
.unwrap();
264+
}
234265
self.read_deps_of(&artifact);
235266
println!("wasi {name} adapter: {:?}", &adapter);
236267
generated_code.push_str(&format!(
237268
"pub const ADAPTER_{}: &'static str = {adapter:?};\n",
238269
name.to_shouty_snake_case(),
239270
));
240-
fs::read(&adapter).unwrap()
271+
(fs::read(&adapter).unwrap(), mtime(&adapter))
241272
}
242273

243274
// Compile a component, return the path of the binary:
244-
fn compile_component(&self, wasm: &Path, adapter: &[u8]) -> PathBuf {
275+
fn compile_component(
276+
&self,
277+
wasm: &Path,
278+
adapter: &[u8],
279+
adapter_mtime: Option<&SystemTime>,
280+
component_path: &Path,
281+
) {
282+
// If the component exists and was last updated after the inputs that
283+
// make it up then there's no need to recreate it.
284+
if let Some(wasm_mtime) = mtime(wasm)
285+
&& let Some(adapter_mtime) = adapter_mtime
286+
&& let Some(component_mtime) = mtime(&component_path)
287+
&& wasm_mtime < component_mtime
288+
&& *adapter_mtime < component_mtime
289+
{
290+
println!("reusing cached component for {wasm:?}");
291+
return;
292+
}
245293
println!("creating a component from {wasm:?}");
246294
let module = fs::read(wasm).expect("read wasm module");
247295
let component = ComponentEncoder::default()
@@ -252,59 +300,63 @@ impl Artifacts {
252300
.unwrap()
253301
.encode()
254302
.expect("module can be translated to a component");
255-
let out_dir = wasm.parent().unwrap();
256-
let stem = wasm.file_stem().unwrap().to_str().unwrap();
257-
let component_path = out_dir.join(format!("{stem}.component.wasm"));
258303
fs::write(&component_path, component).expect("write component to disk");
259-
component_path
260304
}
261305

262306
fn build_non_rust_tests(&mut self, tests: &mut Vec<Test>) {
263307
const ASSETS_REL_SRC_DIR: &'static str = "../src/bin";
264308
println!("cargo:rerun-if-changed={ASSETS_REL_SRC_DIR}");
265309

266-
for entry in fs::read_dir(ASSETS_REL_SRC_DIR).unwrap() {
267-
let entry = entry.unwrap();
268-
let path = entry.path();
269-
let name = path.file_stem().unwrap().to_str().unwrap().to_owned();
270-
match path.extension().and_then(|s| s.to_str()) {
271-
// Compile C/C++ tests with clang
272-
Some("c") | Some("cc") => self.build_c_or_cpp_test(path, name, tests),
273-
274-
// just a header, part of another test.
275-
Some("h") => {}
276-
277-
// Convert the text format to binary and use it as a test.
278-
Some("wat") => {
279-
let wasm = wat::parse_file(&path).unwrap();
280-
let core_wasm = self.out_dir.join(&name).with_extension("wasm");
281-
fs::write(&core_wasm, &wasm).unwrap();
282-
tests.push(Test {
283-
name,
284-
core_wasm: Some(core_wasm),
285-
});
310+
let entries = fs::read_dir(ASSETS_REL_SRC_DIR)
311+
.unwrap()
312+
.map(|e| e.unwrap())
313+
.collect::<Vec<_>>();
314+
let mut c_tests = entries
315+
.par_iter()
316+
.flat_map(|entry| {
317+
let path = entry.path();
318+
let name = path.file_stem().unwrap().to_str().unwrap().to_owned();
319+
match path.extension().and_then(|s| s.to_str()) {
320+
// Compile C/C++ tests with clang
321+
Some("c") | Some("cc") => self.build_c_or_cpp_test(path, name),
322+
323+
// just a header, part of another test.
324+
Some("h") => None,
325+
326+
// Convert the text format to binary and use it as a test.
327+
Some("wat") => {
328+
let wasm = wat::parse_file(&path).unwrap();
329+
let core_wasm = self.out_dir.join(&name).with_extension("wasm");
330+
fs::write(&core_wasm, &wasm).unwrap();
331+
Some(Test {
332+
name,
333+
core_wasm: Some(core_wasm),
334+
})
335+
}
336+
337+
// these are built above in `build_rust_tests`
338+
Some("rs") => None,
339+
340+
// Prevent stray files for now that we don't understand.
341+
Some(_) => panic!("unknown file extension on {path:?}"),
342+
343+
None => unreachable!("no extension in path {path:?}"),
286344
}
287-
288-
// these are built above in `build_rust_tests`
289-
Some("rs") => {}
290-
291-
// Prevent stray files for now that we don't understand.
292-
Some(_) => panic!("unknown file extension on {path:?}"),
293-
294-
None => unreachable!("no extension in path {path:?}"),
295-
}
296-
}
345+
})
346+
.collect::<Vec<_>>();
347+
c_tests.sort_by_key(|t| t.name.clone());
348+
tests.extend(c_tests);
297349
}
298350

299-
fn build_c_or_cpp_test(&mut self, path: PathBuf, name: String, tests: &mut Vec<Test>) {
351+
fn build_c_or_cpp_test(&self, path: PathBuf, name: String) -> Option<Test> {
300352
println!("compiling {path:?}");
301353
println!("cargo:rerun-if-changed={}", path.display());
302354
let contents = std::fs::read_to_string(&path).unwrap();
303355
let config =
304356
wasmtime_test_util::wast::parse_test_config::<CTestConfig>(&contents, "//!").unwrap();
305357

306358
if config.skip {
307-
return;
359+
return None;
308360
}
309361

310362
// The debug tests relying on these assets are ignored by default,
@@ -317,11 +369,10 @@ impl Artifacts {
317369
let wasi_sdk_path = match env::var_os("WASI_SDK_PATH") {
318370
Some(path) => PathBuf::from(path),
319371
None => {
320-
tests.push(Test {
372+
return Some(Test {
321373
name,
322374
core_wasm: None,
323375
});
324-
return;
325376
}
326377
};
327378

@@ -352,7 +403,7 @@ impl Artifacts {
352403
assert!(dwp.status().expect("failed to spawn llvm-dwp").success());
353404
}
354405

355-
tests.push(Test {
406+
return Some(Test {
356407
name,
357408
core_wasm: Some(wasm_path),
358409
});
@@ -425,3 +476,7 @@ fn rustflags() -> &'static str {
425476
_ => "",
426477
}
427478
}
479+
480+
fn mtime(path: &Path) -> Option<SystemTime> {
481+
path.metadata().ok()?.modified().ok()
482+
}

0 commit comments

Comments
 (0)