wasmer_wasix/runtime/module_cache/
filesystem.rs

1use std::path::{Path, PathBuf};
2use std::sync::Arc;
3
4use tempfile::NamedTempFile;
5use tokio::io::AsyncWriteExt;
6use wasmer::{Engine, Module};
7
8use crate::runtime::module_cache::{CacheError, ModuleCache, ModuleHash};
9use crate::runtime::task_manager::tokio::TokioTaskManager;
10
11/// A cache that saves modules to a folder on the host filesystem using
12/// [`Module::serialize()`].
13#[derive(Debug, Clone)]
14pub struct FileSystemCache {
15    cache_dir: PathBuf,
16    task_manager: Arc<TokioTaskManager>,
17}
18
19impl FileSystemCache {
20    pub fn new(cache_dir: impl Into<PathBuf>, task_manager: Arc<TokioTaskManager>) -> Self {
21        FileSystemCache {
22            cache_dir: cache_dir.into(),
23            task_manager,
24        }
25    }
26
27    pub fn cache_dir(&self) -> &Path {
28        &self.cache_dir
29    }
30
31    fn path(&self, key: ModuleHash, deterministic_id: &str) -> PathBuf {
32        let artifact_version = wasmer_types::MetadataHeader::CURRENT_VERSION;
33        self.cache_dir
34            .join(format!("{deterministic_id}-v{artifact_version}"))
35            .join(key.to_string())
36            .with_extension("bin")
37    }
38}
39
40/// Loads a module from the filesystem cache.
41///
42/// A tokio reactor must be available
43#[tracing::instrument(level = "debug", skip_all, fields(? path))]
44async fn tokio_load(path: PathBuf, engine: Engine) -> Result<Module, CacheError> {
45    let artifact_path = path.clone();
46    let deserialized =
47        tokio::task::spawn_blocking(move || deserialize_file(&artifact_path, &engine))
48            .await
49            .unwrap();
50    match deserialized {
51        Ok(m) => {
52            tracing::debug!("Cache hit!");
53            Ok(m)
54        }
55        Err(e) => {
56            tracing::debug!(
57                path=%path.display(),
58                error=&e as &dyn std::error::Error,
59                "Deleting the cache file because the artifact couldn't be deserialized",
60            );
61
62            if let Err(e) = std::fs::remove_file(&path) {
63                tracing::warn!(
64                    path=%path.display(),
65                    error=&e as &dyn std::error::Error,
66                    "Unable to remove the corrupted cache file",
67                );
68            }
69            Err(e)
70        }
71    }
72}
73
74/// Checks if the path exists in the filesystem cache.
75///
76/// A tokio reactor must be available
77async fn tokio_contains(path: PathBuf) -> Result<bool, CacheError> {
78    tokio::fs::try_exists(&path)
79        .await
80        .map_err(|e| CacheError::FileRead {
81            path: path.clone(),
82            error: e,
83        })
84}
85
86/// Saves the module to the filesystem cache.
87///
88/// A tokio reactor must be available
89#[tracing::instrument(level = "debug", skip_all, fields(? path))]
90async fn tokio_save(path: PathBuf, module: Module) -> Result<(), CacheError> {
91    let parent = path
92        .parent()
93        .expect("Unreachable - always created by joining onto cache_dir");
94
95    if let Err(e) = tokio::fs::create_dir_all(parent).await {
96        tracing::warn!(
97            dir=%parent.display(),
98            error=&e as &dyn std::error::Error,
99            "Unable to create the cache directory",
100        );
101    }
102
103    // TODO: NamedTempFile is blocking and we should use the appropriate tokio function instead.
104    // Note: We save to a temporary file and persist() it at the end so
105    // concurrent readers won't see a partially written module.
106    let (file, temp) = NamedTempFile::new_in(parent)
107        .map_err(CacheError::other)?
108        .into_parts();
109
110    let mut file = tokio::fs::File::from_std(file);
111
112    let serialized = tokio::task::spawn_blocking(move || module.serialize())
113        .await
114        .unwrap()?;
115
116    let mut writer = tokio::io::BufWriter::new(&mut file);
117    if let Err(error) = writer.write_all(&serialized).await {
118        return Err(CacheError::FileWrite { path, error });
119    }
120    if let Err(error) = writer.flush().await {
121        return Err(CacheError::FileWrite { path, error });
122    }
123
124    temp.persist(&path).map_err(CacheError::other)?;
125    tracing::debug!(path=%path.display(), "Saved to disk");
126
127    Ok(())
128}
129
130#[async_trait::async_trait]
131impl ModuleCache for FileSystemCache {
132    #[tracing::instrument(level = "debug", skip_all, fields(% key))]
133    async fn load(&self, key: ModuleHash, engine: &Engine) -> Result<Module, CacheError> {
134        let path = self.path(key, &engine.deterministic_id());
135        let engine = engine.clone();
136
137        // Use the bundled tokio runtime instead of the given async runtime
138        // This is necessary because this function can also be called with a futures_executor
139        self.task_manager
140            .runtime_handle()
141            .spawn(tokio_load(path, engine))
142            .await
143            .unwrap()
144    }
145
146    async fn contains(&self, key: ModuleHash, engine: &Engine) -> Result<bool, CacheError> {
147        let path = self.path(key, &engine.deterministic_id());
148
149        // Use the bundled tokio runtime instead of the given async runtime
150        // This is necessary because this function can also be called with a futures_executor
151        self.task_manager
152            .runtime_handle()
153            .spawn(tokio_contains(path))
154            .await
155            .unwrap()
156    }
157
158    #[tracing::instrument(level = "debug", skip_all, fields(% key))]
159    async fn save(
160        &self,
161        key: ModuleHash,
162        engine: &Engine,
163        module: &Module,
164    ) -> Result<(), CacheError> {
165        let path = self.path(key, &engine.deterministic_id());
166        let module = module.clone();
167
168        // Use the bundled tokio runtime instead of the given async runtime
169        // This is necessary because this function can also be called with a futures_executor
170        self.task_manager
171            .runtime_handle()
172            .spawn(tokio_save(path, module))
173            .await
174            .unwrap()
175    }
176}
177
178fn deserialize_file(path: &Path, engine: &Engine) -> Result<Module, CacheError> {
179    // We used to compress our compiled modules using LZW encoding in the past.
180    // This was removed because it has a negative impact on startup times for
181    // "wasmer run", so all new compiled modules should be saved directly to
182    // disk.
183    //
184    // For perspective, compiling php.wasm with cranelift took about 4.75
185    // seconds on a M1 Mac.
186    //
187    // Without LZW compression:
188    // - ModuleCache::save(): 408ms, 142MB binary
189    // - ModuleCache::load(): 155ms
190    // With LZW compression:
191    // - ModuleCache::save(): 2.4s, 72MB binary
192    // - ModuleCache::load(): 822ms
193
194    match unsafe { Module::deserialize_from_file(engine, path) } {
195        // The happy case. ELF artifacts are memory mapped directly from the cache file.
196        Ok(m) => Ok(m),
197        Err(wasmer::DeserializeError::Io(error))
198            if error.kind() == std::io::ErrorKind::NotFound =>
199        {
200            Err(CacheError::NotFound)
201        }
202        Err(wasmer::DeserializeError::Io(error)) => Err(CacheError::FileRead {
203            path: path.to_path_buf(),
204            error,
205        }),
206        Err(e) => Err(CacheError::Deserialize(e)),
207    }
208}
209
210#[cfg(test)]
211mod tests {
212    use crate::runtime::task_manager::tokio::TokioTaskManager;
213    use tempfile::TempDir;
214
215    use super::*;
216
217    const ADD_WAT: &[u8] = br#"(
218        module
219            (func
220                (export "add")
221                (param $x i64)
222                (param $y i64)
223                (result i64)
224                (i64.add (local.get $x) (local.get $y)))
225        )"#;
226
227    fn create_tokio_task_manager() -> Arc<TokioTaskManager> {
228        Arc::new(TokioTaskManager::new(tokio::runtime::Handle::current()))
229    }
230
231    #[tokio::test]
232    async fn save_to_disk() {
233        let temp = TempDir::new().unwrap();
234        let engine = Engine::default();
235        let module = Module::new(&engine, ADD_WAT).unwrap();
236        let cache = FileSystemCache::new(temp.path(), create_tokio_task_manager());
237        let key = ModuleHash::from_bytes([0; _]);
238        let expected_path = cache.path(key, &engine.deterministic_id());
239
240        cache.save(key, &engine, &module).await.unwrap();
241
242        assert!(expected_path.exists());
243    }
244
245    #[tokio::test]
246    async fn create_cache_dir_automatically() {
247        let temp = TempDir::new().unwrap();
248        let engine = Engine::default();
249        let module = Module::new(&engine, ADD_WAT).unwrap();
250        let cache_dir = temp.path().join("this").join("doesn't").join("exist");
251        assert!(!cache_dir.exists());
252        let cache = FileSystemCache::new(&cache_dir, create_tokio_task_manager());
253        let key = ModuleHash::from_bytes([0; _]);
254
255        cache.save(key, &engine, &module).await.unwrap();
256
257        assert!(cache_dir.is_dir());
258    }
259
260    #[tokio::test]
261    async fn missing_file() {
262        let temp = TempDir::new().unwrap();
263        let engine = Engine::default();
264        let key = ModuleHash::from_bytes([0; _]);
265        let cache = FileSystemCache::new(temp.path(), create_tokio_task_manager());
266
267        let err = cache.load(key, &engine).await.unwrap_err();
268
269        assert!(matches!(err, CacheError::NotFound));
270    }
271
272    #[tokio::test]
273    async fn load_from_disk() {
274        let temp = TempDir::new().unwrap();
275        let engine = Engine::default();
276        let module = Module::new(&engine, ADD_WAT).unwrap();
277        let key = ModuleHash::from_bytes([0; _]);
278        let cache = FileSystemCache::new(temp.path(), create_tokio_task_manager());
279        let expected_path = cache.path(key, &engine.deterministic_id());
280        std::fs::create_dir_all(expected_path.parent().unwrap()).unwrap();
281        let serialized = module.serialize().unwrap();
282        std::fs::write(&expected_path, &serialized).unwrap();
283
284        let module = cache.load(key, &engine).await.unwrap();
285
286        let exports: Vec<_> = module
287            .exports()
288            .map(|export| export.name().to_string())
289            .collect();
290        assert_eq!(exports, ["add"]);
291    }
292}