1use std::num::NonZeroUsize;
19use std::path::Path;
20use std::sync::atomic::{AtomicUsize, Ordering};
21
22use murmur_artifact::RuntimeType;
23use wasmtime::Engine;
24
25use crate::compiled_forms::CompiledForms;
26
27pub struct Precompiler {
29 engine: Engine,
30 forms: CompiledForms,
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
36#[serde(rename_all = "snake_case")]
37pub enum Precompiled {
38 Compiled,
41 AlreadyStored,
43 NotWasm,
46 Failed,
50}
51
52impl Precompiler {
53 pub fn new(capsule_writable: Option<&Path>) -> Option<Self> {
58 let engine = crate::runtime::build_engine().ok()?;
59 let forms = match capsule_writable {
60 Some(dir) => CompiledForms::new(&engine, dir),
61 None => CompiledForms::without_capsule(&engine),
62 };
63 Some(Self { engine, forms })
64 }
65
66 pub fn precompile(&self, name: &str, version: &str, payload: &[u8]) -> Precompiled {
76 ineligible(payload).unwrap_or_else(|| self.compile_eligible(name, version, payload))
77 }
78
79 pub fn precompile_all(&self, items: &[PrecompileItem<'_>]) -> Vec<Precompiled> {
88 let settled: Vec<Option<Precompiled>> =
89 items.iter().map(|item| ineligible(item.payload)).collect();
90 let eligible: Vec<&PrecompileItem<'_>> = items
91 .iter()
92 .zip(&settled)
93 .filter(|(_, settled)| settled.is_none())
94 .map(|(item, _)| item)
95 .collect();
96 let mut compiled = run_bounded(&eligible, compile_bound(), |item| {
97 self.compile_eligible(item.name, item.version, item.payload)
98 })
99 .into_iter();
100 settled
101 .into_iter()
102 .map(|settled| {
103 settled.unwrap_or_else(|| {
104 compiled
105 .next()
106 .expect("one compile outcome per eligible item")
107 })
108 })
109 .collect()
110 }
111
112 fn compile_eligible(&self, name: &str, version: &str, payload: &[u8]) -> Precompiled {
114 let key = murmur_artifact::sha256_hex(payload);
115 if self.forms.load(&self.engine, &key).is_some() {
116 return Precompiled::AlreadyStored;
117 }
118 match crate::runtime::stage_root_component(
119 &self.engine,
120 &self.forms,
121 name,
122 version,
123 payload,
124 &key,
125 ) {
126 Ok(_) => Precompiled::Compiled,
127 Err(_) => Precompiled::Failed,
128 }
129 }
130}
131
132#[derive(Debug, Clone, Copy)]
135pub struct PrecompileItem<'a> {
136 pub name: &'a str,
137 pub version: &'a str,
138 pub payload: &'a [u8],
139}
140
141fn ineligible(payload: &[u8]) -> Option<Precompiled> {
145 let Ok(manifest) = murmur_artifact::load_manifest_from_artifact_bytes(payload) else {
146 return Some(Precompiled::Failed);
147 };
148 if manifest.registry_runtime() != RuntimeType::Wasm || manifest.runtime == "capsule" {
149 return Some(Precompiled::NotWasm);
150 }
151 None
152}
153
154fn compile_bound() -> usize {
157 std::thread::available_parallelism().map_or(1, NonZeroUsize::get)
158}
159
160fn run_bounded<T: Sync, R: Send>(items: &[T], bound: usize, f: impl Fn(&T) -> R + Sync) -> Vec<R> {
165 let workers = bound.min(items.len());
166 if workers <= 1 {
167 return items.iter().map(f).collect();
168 }
169 let next = AtomicUsize::new(0);
170 let mut results: Vec<Option<R>> = std::iter::repeat_with(|| None).take(items.len()).collect();
171 std::thread::scope(|scope| {
172 let (f, next) = (&f, &next);
173 let handles: Vec<_> = (0..workers)
174 .map(|_| {
175 scope.spawn(move || {
176 let mut done = Vec::new();
177 loop {
178 let index = next.fetch_add(1, Ordering::Relaxed);
179 let Some(item) = items.get(index) else {
180 break done;
181 };
182 done.push((index, f(item)));
183 }
184 })
185 })
186 .collect();
187 for handle in handles {
188 let done: Vec<(usize, R)> = handle
189 .join()
190 .unwrap_or_else(|panic| std::panic::resume_unwind(panic));
191 for (index, result) in done {
192 results[index] = Some(result);
193 }
194 }
195 });
196 results
197 .into_iter()
198 .map(|result| result.expect("every index is taken by exactly one worker"))
199 .collect()
200}
201
202#[cfg(test)]
203mod tests {
204 use super::*;
205 use crate::artifact::test_support::archive_with_files as payload;
206 use std::path::PathBuf;
207
208 fn fixture(relative: &str) -> Vec<u8> {
209 let path = Path::new(env!("CARGO_MANIFEST_DIR"))
210 .join("../murmur-cli/tests/fixtures")
211 .join(relative);
212 std::fs::read(&path).unwrap_or_else(|err| panic!("{}: {err}", path.display()))
213 }
214
215 fn tool() -> Vec<u8> {
216 payload(&[
217 (
218 "murmur.yaml",
219 b"name: echo-tool\nversion: 0.1.0\nruntime: tool\n",
220 ),
221 ("tool.wasm", &fixture("run/components/echo-tool.wasm")),
222 ])
223 }
224
225 fn driver() -> Vec<u8> {
226 payload(&[
227 (
228 "murmur.yaml",
229 b"name: demo-driver\nversion: 0.1.0\nruntime: driver\n",
230 ),
231 (
232 "tool.wasm",
233 &fixture("drivers/anthropic/driver/murmur-driver-anthropic.wasm"),
234 ),
235 ])
236 }
237
238 fn hook() -> Vec<u8> {
239 payload(&[
240 (
241 "murmur.yaml",
242 b"name: demo-hook\nversion: 0.1.0\nruntime: hook\n",
243 ),
244 (
245 "hook.wasm",
246 &fixture("gateway-probe-hook/hook/gateway-probe-hook.wasm"),
247 ),
248 ])
249 }
250
251 fn compiled_dir() -> PathBuf {
252 crate::state_store::murmur_home_dir()
253 .unwrap()
254 .join("compiled")
255 }
256
257 fn forms_of(payload: &[u8]) -> Vec<PathBuf> {
259 let prefix = format!("{}-", murmur_artifact::sha256_hex(payload));
260 std::fs::read_dir(compiled_dir())
261 .into_iter()
262 .flatten()
263 .filter_map(Result::ok)
264 .map(|entry| entry.path())
265 .filter(|path| {
266 path.extension().is_some_and(|ext| ext == "cwasm")
267 && path
268 .file_name()
269 .and_then(|name| name.to_str())
270 .is_some_and(|name| name.starts_with(&prefix))
271 })
272 .collect()
273 }
274
275 fn inode_and_mtime(path: &Path) -> (u64, std::time::SystemTime) {
276 use std::os::unix::fs::MetadataExt;
277 let metadata = std::fs::symlink_metadata(path).unwrap();
278 (metadata.ino(), metadata.modified().unwrap())
279 }
280
281 #[test]
282 fn a_precompiler_is_shareable_across_threads() {
283 fn shareable<T: Send + Sync>() {}
284 shareable::<Precompiler>();
285 }
286
287 #[test]
288 fn a_wasm_tool_driver_and_hook_are_each_compiled_then_already_stored() {
289 let home = tempfile::tempdir().unwrap();
290 crate::murmur_home::run_with_home(
291 "precompile::tests::inner_a_wasm_tool_driver_and_hook_are_each_compiled_then_already_stored",
292 home.path(),
293 );
294 }
295
296 #[test]
297 #[ignore = "run by a_wasm_tool_driver_and_hook_are_each_compiled_then_already_stored"]
298 fn inner_a_wasm_tool_driver_and_hook_are_each_compiled_then_already_stored() {
299 if !crate::murmur_home::in_scratch_home() {
300 return;
301 }
302 let workdir = tempfile::tempdir().unwrap();
303 let precompiler = Precompiler::new(Some(workdir.path())).unwrap();
304
305 for (name, payload) in [
306 ("echo-tool", tool()),
307 ("demo-driver", driver()),
308 ("demo-hook", hook()),
309 ] {
310 assert_eq!(
311 precompiler.precompile(name, "0.1.0", &payload),
312 Precompiled::Compiled,
313 "{name}"
314 );
315 let forms = forms_of(&payload);
316 let [form] = &forms[..] else {
317 panic!("{name}: expected one form, found {forms:?}");
318 };
319 let before = inode_and_mtime(form);
320 assert_eq!(
321 precompiler.precompile(name, "0.1.0", &payload),
322 Precompiled::AlreadyStored,
323 "{name}"
324 );
325 assert_eq!(inode_and_mtime(form), before, "{name}: form was rewritten");
326 }
327 }
328
329 #[test]
330 fn a_skill_and_a_capsule_are_not_wasm_and_a_native_tool_fails() {
331 let home = tempfile::tempdir().unwrap();
332 crate::murmur_home::run_with_home(
333 "precompile::tests::inner_a_skill_and_a_capsule_are_not_wasm_and_a_native_tool_fails",
334 home.path(),
335 );
336 }
337
338 #[test]
339 #[ignore = "run by a_skill_and_a_capsule_are_not_wasm_and_a_native_tool_fails"]
340 fn inner_a_skill_and_a_capsule_are_not_wasm_and_a_native_tool_fails() {
341 if !crate::murmur_home::in_scratch_home() {
342 return;
343 }
344 let precompiler = Precompiler::new(None).unwrap();
345 let skill = payload(&[
346 (
347 "murmur.yaml",
348 b"name: demo-skill\nversion: 0.1.0\nruntime: skill\n",
349 ),
350 ("skill.md", b"# Demo\n"),
351 ]);
352 let capsule = payload(&[
353 (
354 "murmur.yaml",
355 b"name: demo-capsule\nversion: 0.1.0\nruntime: capsule\n",
356 ),
357 ("capsule.wasm", &fixture("run/components/echo-tool.wasm")),
358 ]);
359
360 for (name, payload) in [("demo-skill", skill), ("demo-capsule", capsule)] {
361 assert_eq!(
362 precompiler.precompile(name, "0.1.0", &payload),
363 Precompiled::NotWasm,
364 "{name}"
365 );
366 }
367 assert_eq!(
368 precompiler.precompile("native-tool", "0.1.0", &native_tool()),
369 Precompiled::Failed
370 );
371 assert!(
372 !compiled_dir().exists(),
373 "{} was created",
374 compiled_dir().display()
375 );
376 }
377
378 #[test]
379 fn a_form_install_wrote_is_the_form_staging_loads() {
380 let home = tempfile::tempdir().unwrap();
381 crate::murmur_home::run_with_home(
382 "precompile::tests::inner_a_form_install_wrote_is_the_form_staging_loads",
383 home.path(),
384 );
385 }
386
387 #[test]
388 #[ignore = "run by a_form_install_wrote_is_the_form_staging_loads"]
389 fn inner_a_form_install_wrote_is_the_form_staging_loads() {
390 if !crate::murmur_home::in_scratch_home() {
391 return;
392 }
393 let payload = tool();
394 let key = murmur_artifact::sha256_hex(&payload);
395 assert_eq!(
396 Precompiler::new(None)
397 .unwrap()
398 .precompile("echo-tool", "0.1.0", &payload),
399 Precompiled::Compiled
400 );
401
402 let engine = crate::runtime::build_engine().unwrap();
403 let workdir = tempfile::tempdir().unwrap();
404 let staging = CompiledForms::new(&engine, workdir.path());
405 assert!(staging.load(&engine, &key).is_some());
406 }
407
408 #[test]
409 fn a_payload_whose_root_wasm_does_not_compile_is_failed_and_stores_nothing() {
410 let home = tempfile::tempdir().unwrap();
411 crate::murmur_home::run_with_home(
412 "precompile::tests::inner_a_payload_whose_root_wasm_does_not_compile_is_failed_and_stores_nothing",
413 home.path(),
414 );
415 }
416
417 #[test]
418 #[ignore = "run by a_payload_whose_root_wasm_does_not_compile_is_failed_and_stores_nothing"]
419 fn inner_a_payload_whose_root_wasm_does_not_compile_is_failed_and_stores_nothing() {
420 if !crate::murmur_home::in_scratch_home() {
421 return;
422 }
423 let precompiler = Precompiler::new(None).unwrap();
424 let not_wasm = payload(&[
425 (
426 "murmur.yaml",
427 b"name: broken-tool\nversion: 0.1.0\nruntime: tool\n",
428 ),
429 ("tool.wasm", b"not wasm"),
430 ]);
431 let no_manifest = payload(&[("tool.wasm", &fixture("run/components/echo-tool.wasm"))]);
432
433 for payload in [not_wasm, no_manifest, b"not a zip".to_vec()] {
434 assert_eq!(
435 precompiler.precompile("broken-tool", "0.1.0", &payload),
436 Precompiled::Failed
437 );
438 assert!(forms_of(&payload).is_empty());
439 }
440 }
441
442 fn native_tool() -> Vec<u8> {
444 payload(&[
445 (
446 "murmur.yaml",
447 b"name: native-tool\nversion: 0.1.0\nruntime: tool\nimplementation: native\n",
448 ),
449 ("bin/native-tool", b"#!/bin/sh\necho hi\n"),
450 ])
451 }
452
453 #[derive(Default)]
455 struct InFlight {
456 now: AtomicUsize,
457 most: AtomicUsize,
458 }
459
460 impl InFlight {
461 fn enter(&self) {
462 let now = self.now.fetch_add(1, Ordering::SeqCst) + 1;
463 self.most.fetch_max(now, Ordering::SeqCst);
464 }
465
466 fn leave(&self) {
467 self.now.fetch_sub(1, Ordering::SeqCst);
468 }
469
470 fn most(&self) -> usize {
471 self.most.load(Ordering::SeqCst)
472 }
473 }
474
475 #[test]
476 fn bounded_results_come_back_in_input_order_when_the_first_item_is_slowest() {
477 let items: Vec<u64> = (0..6).collect();
478 let results = run_bounded(&items, 4, |&item| {
479 if item == 0 {
480 std::thread::sleep(std::time::Duration::from_millis(200));
481 }
482 item * 10
483 });
484 assert_eq!(results, vec![0, 10, 20, 30, 40, 50]);
485 }
486
487 #[test]
488 fn bounded_never_runs_more_items_at_once_than_its_bound() {
489 let items: Vec<usize> = (0..24).collect();
490 for bound in [2, 3, 5] {
491 let in_flight = InFlight::default();
492 let results = run_bounded(&items, bound, |&item| {
493 in_flight.enter();
494 std::thread::sleep(std::time::Duration::from_millis(10));
495 in_flight.leave();
496 item
497 });
498 assert_eq!(results, items);
499 assert!(
500 (1..=bound).contains(&in_flight.most()),
501 "bound {bound}: {} items ran at once",
502 in_flight.most()
503 );
504 }
505 }
506
507 #[test]
508 fn bounded_runs_two_items_at_once_when_its_bound_allows() {
509 let arrived = std::sync::Mutex::new(0usize);
512 let both_in = std::sync::Condvar::new();
513 let results = run_bounded(&[(), ()], 2, |_| {
514 let mut count = arrived.lock().unwrap();
515 *count += 1;
516 both_in.notify_all();
517 let (count, _) = both_in
518 .wait_timeout_while(count, std::time::Duration::from_secs(30), |count| {
519 *count < 2
520 })
521 .unwrap();
522 *count >= 2
523 });
524 assert_eq!(results, vec![true, true], "the two items never overlapped");
525 }
526
527 #[test]
528 fn a_bound_of_one_runs_every_item_in_turn_on_the_calling_thread() {
529 let caller = std::thread::current().id();
530 let in_flight = InFlight::default();
531 let started = std::sync::Mutex::new(Vec::new());
532 let items: Vec<usize> = (0..5).collect();
533 let results = run_bounded(&items, 1, |&item| {
534 in_flight.enter();
535 started.lock().unwrap().push(item);
536 let on_caller = std::thread::current().id() == caller;
537 in_flight.leave();
538 on_caller
539 });
540 assert_eq!(results, vec![true; 5]);
541 assert_eq!(in_flight.most(), 1);
542 assert_eq!(*started.lock().unwrap(), items);
543 }
544
545 #[test]
546 fn bounded_returns_nothing_for_no_items() {
547 let results = run_bounded(&[] as &[u8], 4, |_| -> u8 { panic!("called for no items") });
548 assert!(results.is_empty());
549 }
550
551 #[test]
552 fn a_batch_reports_each_payload_in_order_and_stores_each_wasm_form_once() {
553 let home = tempfile::tempdir().unwrap();
554 crate::murmur_home::run_with_home(
555 "precompile::tests::inner_a_batch_reports_each_payload_in_order_and_stores_each_wasm_form_once",
556 home.path(),
557 );
558 }
559
560 #[test]
561 #[ignore = "run by a_batch_reports_each_payload_in_order_and_stores_each_wasm_form_once"]
562 fn inner_a_batch_reports_each_payload_in_order_and_stores_each_wasm_form_once() {
563 if !crate::murmur_home::in_scratch_home() {
564 return;
565 }
566 let workdir = tempfile::tempdir().unwrap();
567 let precompiler = Precompiler::new(Some(workdir.path())).unwrap();
568 let payloads = [
569 ("demo-driver", driver()),
570 ("not-a-zip", b"not a zip".to_vec()),
571 ("echo-tool", tool()),
572 ("native-tool", native_tool()),
573 ("demo-hook", hook()),
574 ];
575 let items: Vec<PrecompileItem<'_>> = payloads
576 .iter()
577 .map(|(name, payload)| PrecompileItem {
578 name,
579 version: "0.1.0",
580 payload,
581 })
582 .collect();
583
584 assert_eq!(
585 precompiler.precompile_all(&items),
586 vec![
587 Precompiled::Compiled,
588 Precompiled::Failed,
589 Precompiled::Compiled,
590 Precompiled::Failed,
591 Precompiled::Compiled,
592 ]
593 );
594 let engine = crate::runtime::build_engine().unwrap();
595 let staging = CompiledForms::new(&engine, workdir.path());
596 let mut stamps = Vec::new();
597 for (name, payload) in &payloads {
598 let forms = forms_of(payload);
599 match *name {
600 "demo-driver" | "echo-tool" | "demo-hook" => {
601 let [form] = &forms[..] else {
602 panic!("{name}: expected one form, found {forms:?}");
603 };
604 let key = murmur_artifact::sha256_hex(payload);
605 assert!(staging.load(&engine, &key).is_some(), "{name}");
606 stamps.push((form.clone(), inode_and_mtime(form)));
607 }
608 _ => assert!(forms.is_empty(), "{name}: {forms:?}"),
609 }
610 }
611
612 assert_eq!(
613 precompiler.precompile_all(&items),
614 vec![
615 Precompiled::AlreadyStored,
616 Precompiled::Failed,
617 Precompiled::AlreadyStored,
618 Precompiled::Failed,
619 Precompiled::AlreadyStored,
620 ]
621 );
622 for (form, stamp) in stamps {
623 assert_eq!(
624 inode_and_mtime(&form),
625 stamp,
626 "{} was rewritten",
627 form.display()
628 );
629 }
630 }
631}