Skip to main content

warp_runtime/
profile.rs

1//! WARP_PROFILE=<file>: where a run spends its time, sampled at each epoch tick with wasmtime's GuestProfiler and
2//! written as Firefox Profiler JSON when the run's store drops (open it at https://profiler.firefox.com). Samples are
3//! taken at function entries and loop headers of the wasm, host calls show as markers. Engines counting fuel don't
4//! tick, so their runs give no samples.
5use wasmtime::{AsContext, CallHook, Engine, Module};
6
7/// Environment variable naming the profile file; runs after the first in a process (stage programs, tasks) write
8/// `<file stem>-<n>.<extension>` beside it
9pub const PROFILE_VARIABLE: &str = "WARP_PROFILE";
10
11#[cfg(feature = "profiling")]
12pub use sampled::Profile;
13
14#[cfg(feature = "profiling")]
15mod sampled {
16	use super::*;
17	use std::path::{Path, PathBuf};
18	use std::sync::atomic::{AtomicUsize, Ordering};
19	use std::sync::{Arc, Mutex};
20	use std::time::Duration;
21	use wasmtime::GuestProfiler;
22
23	/// The profile of one run, shared by its store's epoch callback and call hook
24	#[derive(Clone)]
25	pub struct Profile(Arc<Mutex<RunProfile>>);
26
27	struct RunProfile {
28		profiler: Option<GuestProfiler>,
29		file: PathBuf,
30		/// the thread's CPU time at the last sample
31		sampled_at: Duration,
32	}
33
34	impl Profile {
35		/// A profile of `module`'s run when WARP_PROFILE names a file
36		pub fn of(engine: &Engine, module: &Module) -> Option<Profile> {
37			let file = profile_file(&std::env::var(PROFILE_VARIABLE).ok().filter(|file| !file.is_empty())?);
38			let name = module.name().unwrap_or("main").to_string();
39			let interval = crate::engine::EPOCH_TICK;
40			match GuestProfiler::new(engine, &name, interval, [(name.clone(), module.clone())]) {
41				Ok(profiler) => {
42					let sampled_at = crate::engine::thread_cpu_time();
43					Some(Profile(Arc::new(Mutex::new(RunProfile { profiler: Some(profiler), file, sampled_at }))))
44				}
45				Err(failure) => {
46					eprintln!("{PROFILE_VARIABLE}: no profile of {name}: {failure}");
47					None
48				}
49			}
50		}
51
52		pub fn sample(&self, store: impl AsContext) {
53			let mut run = self.0.lock().unwrap();
54			let now = crate::engine::thread_cpu_time();
55			let delta = now.saturating_sub(run.sampled_at);
56			run.sampled_at = now;
57			if let Some(profiler) = run.profiler.as_mut() {
58				profiler.sample(store, delta);
59			}
60		}
61
62		pub fn call_hook(&self, store: impl AsContext, hook: CallHook) {
63			if let Some(profiler) = self.0.lock().unwrap().profiler.as_mut() {
64				profiler.call_hook(store, hook);
65			}
66		}
67	}
68
69	impl Drop for RunProfile {
70		fn drop(&mut self) {
71			let Some(profiler) = self.profiler.take() else { return };
72			let written = std::fs::File::create(&self.file)
73				.map_err(wasmtime::Error::from)
74				.and_then(|file| profiler.finish(std::io::BufWriter::new(file)));
75			match written {
76				Ok(()) => eprintln!("profile: {} (open it at https://profiler.firefox.com)", self.file.display()),
77				Err(failure) => eprintln!("{PROFILE_VARIABLE}: {} not written: {failure}", self.file.display()),
78			}
79		}
80	}
81
82	/// The file of this process's next profile: `file` first, then `<stem>-2.<extension>`, …
83	fn profile_file(file: &str) -> PathBuf {
84		static PROFILES: AtomicUsize = AtomicUsize::new(0);
85		let number = PROFILES.fetch_add(1, Ordering::Relaxed) + 1;
86		let file = Path::new(file);
87		if number == 1 {
88			return file.to_path_buf();
89		}
90		let stem = file.file_stem().unwrap_or_default().to_string_lossy();
91		let numbered = match file.extension() {
92			Some(extension) => format!("{stem}-{number}.{}", extension.to_string_lossy()),
93			None => format!("{stem}-{number}"),
94		};
95		file.with_file_name(numbered)
96	}
97}
98
99/// Without the profiling feature no run is profiled
100#[cfg(not(feature = "profiling"))]
101#[derive(Clone)]
102pub enum Profile {}
103
104#[cfg(not(feature = "profiling"))]
105impl Profile {
106	pub fn of(_engine: &Engine, _module: &Module) -> Option<Profile> {
107		if std::env::var_os(PROFILE_VARIABLE).is_some() {
108			eprintln!("{PROFILE_VARIABLE}: this build has no profiler (warp-runtime feature profiling)");
109		}
110		None
111	}
112
113	pub fn sample(&self, _store: impl AsContext) {
114		match *self {}
115	}
116
117	pub fn call_hook(&self, _store: impl AsContext, _hook: CallHook) {
118		match *self {}
119	}
120}