Skip to main content

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