swc_plugin_runner/
cache.rs

1#![allow(unused)]
2
3use std::{
4    env::current_dir,
5    path::{Path, PathBuf},
6    str::FromStr,
7};
8
9use anyhow::{Context, Error};
10use parking_lot::Mutex;
11use rustc_hash::FxHashMap;
12use swc_common::sync::{Lazy, OnceCell};
13
14use crate::{
15    plugin_module_bytes::{CompiledPluginModuleBytes, PluginModuleBytes, RawPluginModuleBytes},
16    runtime,
17};
18
19#[derive(Default)]
20pub struct PluginModuleCacheInner {
21    current_runtime: Option<&'static str>,
22
23    #[cfg(all(not(target_arch = "wasm32"), feature = "filesystem_cache"))]
24    fs_cache_root: Option<String>,
25    #[cfg(all(not(target_arch = "wasm32"), feature = "filesystem_cache"))]
26    fs_cache_store: Option<FileSystemCache>,
27    // Stores the string representation of the hash of the plugin module to store into
28    // FileSystemCache. This works since SWC does not revalidates plugin in single process
29    // lifecycle.
30    #[cfg(all(not(target_arch = "wasm32"), feature = "filesystem_cache"))]
31    fs_cache_hash_store: FxHashMap<String, String>,
32    // Generic in-memory cache to the raw bytes, either read by fs or supplied by bindgen.
33    memory_cache_store: FxHashMap<String, Vec<u8>>,
34    // A naive hashmap to the compiled plugin modules.
35    // Current it doesn't have any invalidation or expiration logics like lru,
36    // having a lot of plugins may create some memory pressure.
37    compiled_module_bytes: FxHashMap<String, runtime::ModuleCache>,
38}
39
40impl PluginModuleCacheInner {
41    pub fn get_fs_cache_root(&self) -> Option<String> {
42        #[cfg(all(not(target_arch = "wasm32"), feature = "filesystem_cache"))]
43        return self.fs_cache_root.clone();
44
45        None
46    }
47
48    /// Check if the cache contains bytes for the corresponding key.
49    pub fn contains(&self, rt: &dyn runtime::Runtime, key: &str) -> bool {
50        if Some(rt.identifier()) != self.current_runtime {
51            return false;
52        }
53
54        let is_in_cache = self.memory_cache_store.contains_key(key)
55            || self.compiled_module_bytes.contains_key(key);
56
57        #[cfg(all(not(target_arch = "wasm32"), feature = "filesystem_cache"))]
58        {
59            // Instead of accessing FileSystemCache, check if the key have corresponding
60            // hash since FileSystemCache does not have a way to check if the key
61            // exists.
62            return is_in_cache || self.fs_cache_hash_store.contains_key(key);
63        }
64
65        is_in_cache
66    }
67
68    /// Insert raw plugin module bytes into cache does not have compiled
69    /// wasmer::Module. The bytes stored in this type of cache will return
70    /// RawPluginModuleBytes. It is strongly recommend to avoid using this
71    /// type of cache as much as possible, since module compilation time for
72    /// the wasm is noticeably expensive and caching raw bytes only cuts off
73    /// the reading time for the plugin module.
74    pub fn insert_raw_bytes(&mut self, key: String, value: Vec<u8>) {
75        self.memory_cache_store.insert(key, value);
76    }
77
78    /// Insert already compiled wasmer::Module into cache.
79    /// The module stored in this cache will return CompiledPluginModuleBytes,
80    /// which costs near-zero time when calling its `compile_module` method as
81    /// it clones precompiled module directly.
82    ///
83    /// In genearl it is recommended to use either using filesystemcache
84    /// `store_bytes_from_path` which internally calls this or directly call
85    /// this to store compiled module bytes. CompiledModuleBytes provides way to
86    /// create it via RawModuleBytes, so there's no practical reason to
87    /// store raw bytes most cases.
88    fn insert_compiled_module_bytes(&mut self, key: String, value: runtime::ModuleCache) {
89        self.compiled_module_bytes.insert(key, value);
90    }
91
92    /// Store plugin module bytes into the cache, from actual filesystem.
93    pub fn store_bytes_from_path(
94        &mut self,
95        rt: &dyn runtime::Runtime,
96        binary_path: &Path,
97        key: &str,
98    ) -> Result<(), Error> {
99        let runtime_name = rt.identifier();
100
101        // The module caches of different runtimes are not compatible, so we cleanup the
102        // old caches.
103        if *self.current_runtime.get_or_insert(runtime_name) != runtime_name {
104            self.current_runtime = Some(runtime_name);
105            self.compiled_module_bytes.clear();
106        }
107
108        #[cfg(all(not(target_arch = "wasm32"), feature = "filesystem_cache"))]
109        {
110            let raw_module_bytes =
111                std::fs::read(binary_path).context("Cannot read plugin from specified path")?;
112
113            // If FilesystemCache is available, store serialized bytes into fs.
114            if let Some(fs_cache_store) = &mut self.fs_cache_store {
115                let module_bytes_hash = blake3::hash(&raw_module_bytes).to_string();
116
117                let module =
118                    if let Some(cache) = unsafe { fs_cache_store.load(rt, &module_bytes_hash) } {
119                        tracing::debug!("Build WASM from cache: {key}");
120                        cache
121                    } else {
122                        let cache = rt
123                            .prepare_module(&raw_module_bytes)
124                            .context("Cannot compile plugin binary")?;
125                        fs_cache_store.store(rt, &module_bytes_hash, &cache)?;
126                        cache
127                    };
128
129                // Store hash to load from fs_cache_store later.
130                self.fs_cache_hash_store
131                    .insert(key.to_string(), module_bytes_hash);
132
133                // Also store in memory for the in-process cache.
134                self.insert_compiled_module_bytes(key.to_string(), module);
135            }
136
137            // Store raw bytes into memory cache.
138            self.insert_raw_bytes(key.to_string(), raw_module_bytes);
139
140            return Ok(());
141        }
142
143        anyhow::bail!("Filesystem cache is not enabled, cannot read plugin from phsyical path");
144    }
145
146    /// Returns a PluingModuleBytes can be compiled into a wasmer::Module.
147    /// Depends on the cache availability, it may return a raw bytes or a
148    /// serialized bytes.
149    pub fn get(&self, rt: &dyn runtime::Runtime, key: &str) -> Option<Box<dyn PluginModuleBytes>> {
150        // Look for compiled module bytes first, it is the cheapest way to get compile
151        // wasmer::Module.
152        if let Some(compiled_module) = self.compiled_module_bytes.get(key) {
153            let cache = rt.clone_cache(compiled_module)?;
154            return Some(Box::new(CompiledPluginModuleBytes::new(
155                key.to_string(),
156                cache,
157            )));
158        }
159
160        // Next, read serialzied bytes from filesystem cache.
161        #[cfg(all(not(target_arch = "wasm32"), feature = "filesystem_cache"))]
162        if let Some(fs_cache_store) = &self.fs_cache_store {
163            let hash = self.fs_cache_hash_store.get(key)?;
164            let module = unsafe { fs_cache_store.load(rt, hash) };
165            if let Some(module) = module {
166                return Some(Box::new(CompiledPluginModuleBytes::new(
167                    key.to_string(),
168                    module,
169                )));
170            }
171        }
172
173        // Lastly, look for if there's a raw bytes in memory. This requires compilation
174        // still, but doesn't go through filesystem access.
175        if let Some(memory_cache_bytes) = self.memory_cache_store.get(key) {
176            return Some(Box::new(RawPluginModuleBytes::new(
177                key.to_string(),
178                memory_cache_bytes.clone(),
179            )));
180        }
181        None
182    }
183}
184
185#[derive(Default)]
186pub struct PluginModuleCache {
187    pub inner: OnceCell<Mutex<PluginModuleCacheInner>>,
188    /// To prevent concurrent access to `WasmerInstance::new`.
189    /// This is a precaution only yet, for the preparation of wasm thread
190    /// support in the future.
191    instantiation_lock: Mutex<()>,
192}
193
194impl PluginModuleCache {
195    pub fn create_inner(
196        enable_fs_cache_store: bool,
197        fs_cache_store_root: Option<&str>,
198    ) -> PluginModuleCacheInner {
199        PluginModuleCacheInner {
200            current_runtime: None,
201
202            #[cfg(all(not(target_arch = "wasm32"), feature = "filesystem_cache"))]
203            fs_cache_root: fs_cache_store_root.map(|s| s.to_string()),
204            #[cfg(all(not(target_arch = "wasm32"), feature = "filesystem_cache"))]
205            fs_cache_store: if enable_fs_cache_store {
206                FileSystemCache::create(fs_cache_store_root)
207            } else {
208                None
209            },
210            #[cfg(all(not(target_arch = "wasm32"), feature = "filesystem_cache"))]
211            fs_cache_hash_store: Default::default(),
212            memory_cache_store: Default::default(),
213            compiled_module_bytes: Default::default(),
214        }
215    }
216}
217
218#[cfg(all(not(target_arch = "wasm32"), feature = "filesystem_cache"))]
219struct FileSystemCache {
220    path: PathBuf,
221}
222
223#[cfg(all(not(target_arch = "wasm32"), feature = "filesystem_cache"))]
224impl FileSystemCache {
225    #[tracing::instrument(level = "info", skip_all)]
226    fn create(root: Option<&str>) -> Option<Self> {
227        let mut root_path = if let Some(root) = root {
228            Some(PathBuf::from(root))
229        } else if let Ok(cwd) = current_dir() {
230            Some(cwd.join(".swc"))
231        } else {
232            None
233        };
234
235        let mut path = root_path?;
236        path.push("plugins");
237        path.push(format!(
238            "{}_{}_{}",
239            std::env::consts::OS,
240            std::env::consts::ARCH,
241            option_env!("CARGO_PKG_VERSION").unwrap_or("plugin_runner_unknown")
242        ));
243
244        std::fs::create_dir_all(&path).ok()?;
245        Some(FileSystemCache { path })
246    }
247
248    unsafe fn load(&self, rt: &dyn runtime::Runtime, key: &str) -> Option<runtime::ModuleCache> {
249        let path = self.path.join(format!("{}.{}", key, rt.identifier()));
250        rt.load_cache(&path)
251    }
252
253    fn store(
254        &self,
255        rt: &dyn runtime::Runtime,
256        key: &str,
257        cache: &runtime::ModuleCache,
258    ) -> anyhow::Result<()> {
259        let path = self.path.join(format!("{}.{}", key, rt.identifier()));
260        rt.store_cache(&path, cache)
261    }
262}