fix(plugin): Fix caching of wasm modulee (#3616)

This commit is contained in:
OJ Kwon 2022-02-17 21:50:51 -08:00 committed by GitHub
parent b69e4b2a93
commit 05aecf507e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 199 additions and 160 deletions

View File

@ -439,6 +439,9 @@ impl Options {
filename: transform_filename,
};
#[cfg(feature = "plugin")]
swc_plugin_runner::cache::init_plugin_module_cache_once(&experimental.cache_root);
let pass = chain!(
lint_to_fold(swc_ecma_lints::rules::all(LintParams {
program: &program,

View File

@ -45,13 +45,9 @@ pub fn plugins(
) -> impl Fold {
#[cfg(feature = "plugin")]
{
let cache_root =
swc_plugin_runner::resolve::resolve_plugin_cache_root(config.cache_root).ok();
RustPlugins {
resolver,
plugins: config.plugins,
plugin_cache: cache_root,
plugin_context,
}
}
@ -65,10 +61,6 @@ pub fn plugins(
struct RustPlugins {
resolver: CachingResolver<NodeModulesResolver>,
plugins: Option<Vec<PluginConfig>>,
/// TODO: it is unclear how we'll support plugin itself in wasm target of
/// swc, as well as cache.
#[cfg(feature = "plugin")]
plugin_cache: Option<swc_plugin_runner::resolve::PluginCache>,
plugin_context: PluginContext,
}
@ -112,7 +104,7 @@ impl RustPlugins {
serialized = swc_plugin_runner::apply_js_plugin(
&p.0,
&path,
&mut self.plugin_cache,
&swc_plugin_runner::cache::PLUGIN_MODULE_CACHE,
serialized,
config_json,
context_json,

View File

@ -16,7 +16,7 @@ parking_lot = "0.12.0"
serde = {version = "1.0.126", features = ["derive"]}
serde_json = "1.0.64"
swc_atoms = {version = "0.2.7", path = '../swc_atoms'}
swc_common = {version = "0.17.0", path = "../swc_common", features = ["plugin-rt"]}
swc_common = {version = "0.17.0", path = "../swc_common", features = ["plugin-rt", "concurrent"]}
swc_ecma_ast = {version = "0.65.0", path = "../swc_ecma_ast", features = ["rkyv-impl"]}
swc_ecma_loader = { version = "0.28.0", path = "../swc_ecma_loader" }
swc_ecma_parser = {version = "0.88.0", path = "../swc_ecma_parser"}

View File

@ -0,0 +1,153 @@
use std::{
env::current_dir,
path::{Path, PathBuf},
};
use anyhow::{Context, Error};
use parking_lot::Mutex;
use swc_common::{
collections::AHashMap,
sync::{Lazy, OnceCell},
};
use wasmer::{Module, Store};
use wasmer_cache::{Cache as WasmerCache, FileSystemCache, Hash};
/// Version for bytecode cache stored in local filesystem.
///
/// This MUST be updated when bump up wasmer.
///
/// Bytecode cache generated via wasmer is generally portable,
/// however it is not gauranteed to be compatible across wasmer's
/// internal changes.
/// https://github.com/wasmerio/wasmer/issues/2781
const MODULE_SERIALIZATION_VERSION: &str = "v1";
/// A shared instance to plugin's module bytecode cache.
/// TODO: it is unclear how we'll support plugin itself in wasm target of
/// swc, as well as cache.
pub static PLUGIN_MODULE_CACHE: Lazy<PluginModuleCache> = Lazy::new(Default::default);
#[derive(Default)]
pub struct CacheInner {
fs_cache: Option<FileSystemCache>,
memory_cache: InMemoryCache,
}
/// Lightweight in-memory cache to hold plugin module instances.
/// Current it doesn't have any invalidation or expiration logics like lru,
/// having a lot of plugins may create some memory pressure.
#[derive(Default)]
pub struct InMemoryCache {
modules: AHashMap<PathBuf, Module>,
}
#[derive(Default)]
pub struct PluginModuleCache {
inner: OnceCell<Mutex<CacheInner>>,
/// To prevent concurrent access to `WasmerInstance::new`
instantiation_lock: Mutex<()>,
}
fn create_filesystem_cache(filesystem_cache_root: &Option<String>) -> Option<FileSystemCache> {
let mut root_path = if let Some(root) = filesystem_cache_root {
Some(PathBuf::from(root))
} else if let Ok(cwd) = current_dir() {
Some(cwd.join(".swc"))
} else {
None
};
if let Some(root_path) = &mut root_path {
root_path.push("plugins");
root_path.push(MODULE_SERIALIZATION_VERSION);
return FileSystemCache::new(&root_path).ok();
}
None
}
/// Create a new cache instance if not intialized. This can be called multiple
/// time, but any subsequent call will be ignored.
///
/// This fn have a side effect to create path to cache if given path is not
/// resolvable. If root is not specified, it'll generate default root for
/// cache location.
///
/// If cache failed to initialize filesystem cache for given location
/// it'll be serve in-memory cache only.
pub fn init_plugin_module_cache_once(filesystem_cache_root: &Option<String>) {
PLUGIN_MODULE_CACHE.inner.get_or_init(|| {
Mutex::new(CacheInner {
fs_cache: create_filesystem_cache(filesystem_cache_root),
memory_cache: Default::default(),
})
});
}
impl PluginModuleCache {
/// DO NOT USE unless absolutely necessary. This is mainly for testing
/// purpose.
pub fn new() -> Self {
PluginModuleCache {
inner: OnceCell::from(Mutex::new(Default::default())),
instantiation_lock: Mutex::new(()),
}
}
/// Load a compiled plugin Module from speficied path.
/// Since plugin will be initialized per-file transform, this function tries
/// to avoid reading filesystem per each initialization via naive
/// in-memory map which stores raw bytecodes from file. Unlike compiled
/// bytecode cache for the wasm, this is volatile.
///
/// ### Notes
/// [This code](https://github.com/swc-project/swc/blob/fc4c6708f24cda39640fbbfe56123f2f6eeb2474/crates/swc/src/plugin.rs#L19-L44)
/// includes previous incorrect attempt to workaround file read issues.
/// In actual transform, `plugins` is also being called per each transform.
pub fn load_module(&self, binary_path: &Path) -> Result<Module, Error> {
let binary_path = binary_path.to_path_buf();
let mut inner_cache = self.inner.get().expect("Cache should be available").lock();
// if constructed Module is available in-memory, directly return it.
// Note we do not invalidate in-memory cache currently: if wasm binary is
// replaced in-process lifecycle (i.e devserver) it won't be reflected.
let in_memory_module = inner_cache.memory_cache.modules.get(&binary_path);
if let Some(module) = in_memory_module {
return Ok(module.clone());
}
let module_bytes =
std::fs::read(&binary_path).context("Cannot read plugin from specified path")?;
let module_bytes_hash = Hash::generate(&module_bytes);
let wasmer_store = Store::default();
let load_cold_wasm_bytes = || {
let _lock = self.instantiation_lock.lock();
Module::new(&wasmer_store, module_bytes).context("Cannot compile plugin binary")
};
// Try to load compiled bytes from filesystem cache if available.
// Otherwise, cold compile instead.
let module = if let Some(fs_cache) = &mut inner_cache.fs_cache {
let load_result = unsafe { fs_cache.load(&wasmer_store, module_bytes_hash) };
if let Ok(module) = load_result {
module
} else {
let cold_bytes = load_cold_wasm_bytes()?;
fs_cache.store(module_bytes_hash, &cold_bytes)?;
cold_bytes
}
} else {
load_cold_wasm_bytes()?
};
inner_cache
.memory_cache
.modules
.insert(binary_path, module.clone());
Ok(module)
}
}

View File

@ -1,26 +1,19 @@
use std::{
path::{Path, PathBuf},
sync::Arc,
};
use std::{path::Path, sync::Arc};
use anyhow::{anyhow, Context, Error};
use cache::PluginModuleCache;
use once_cell::sync::Lazy;
use parking_lot::Mutex;
use resolve::PluginCache;
use swc_common::{
collections::AHashMap,
errors::{Diagnostic, HANDLER},
hygiene::MutableMarkContext,
plugin::{PluginError, Serialized},
Mark, SyntaxContext,
};
use wasmer::{
imports, Array, Exports, Function, Instance, LazyInit, Memory, Module, Store, WasmPtr,
};
use wasmer_cache::{Cache, Hash};
use wasmer::{imports, Array, Exports, Function, Instance, LazyInit, Memory, WasmPtr};
use wasmer_wasi::{is_wasi_module, WasiState};
pub mod resolve;
pub mod cache;
fn copy_bytes_into_host(memory: &Memory, bytes_ptr: i32, bytes_ptr_len: i32) -> Vec<u8> {
let ptr: WasmPtr<u8, Array> = WasmPtr::new(bytes_ptr as _);
@ -202,94 +195,18 @@ struct HostEnvironment {
transform_result: Arc<Mutex<Vec<u8>>>,
}
/// Load plugin from specified path.
/// If cache is provided, it'll try to load from cache first to avoid
/// compilation.
///
/// Since plugin will be initialized per-file transform, this function tries to
/// avoid reading filesystem per each initialization via naive in-memory map
/// which stores raw bytecodes from file. Unlike compiled bytecode cache for the
/// wasm, this is volatile.
///
/// ### Notes
/// [This code](https://github.com/swc-project/swc/blob/fc4c6708f24cda39640fbbfe56123f2f6eeb2474/crates/swc/src/plugin.rs#L19-L44)
/// includes previous incorrect attempt to workaround file read issues.
/// In actual transform, `plugins` is also being called per each transform.
fn load_plugin(
plugin_path: &Path,
cache: &mut Option<PluginCache>,
cache: &Lazy<PluginModuleCache>,
) -> Result<(Instance, Arc<Mutex<Vec<u8>>>), Error> {
static BYTE_CACHE: Lazy<Mutex<AHashMap<PathBuf, Arc<Vec<u8>>>>> = Lazy::new(Default::default);
// TODO: This caching streategy does not consider few edge cases.
// 1. If process is long-running (devServer) binary change in the middle of
// process won't be reflected.
// 2. If reading binary fails somehow it won't bail out but keep retry.
let module_bytes_key = plugin_path.to_path_buf();
let cached_bytes = BYTE_CACHE.lock().get(&module_bytes_key).cloned();
let module_bytes = if let Some(cached_bytes) = cached_bytes {
cached_bytes
} else {
let fresh_module_bytes = std::fs::read(plugin_path)
.map(Arc::new)
.context("Cannot read plugin from specified path")?;
BYTE_CACHE
.lock()
.insert(module_bytes_key, fresh_module_bytes.clone());
fresh_module_bytes
};
// TODO: can we share store instances across each plugin binaries?
let wasmer_store = Store::default();
let load_from_cache = |c: &mut PluginCache, hash: Hash| match c {
PluginCache::File(filesystem_cache) => unsafe {
filesystem_cache.load(&wasmer_store, hash)
},
};
let store_into_cache = |c: &mut PluginCache, hash: Hash, module: &Module| match c {
PluginCache::File(filesystem_cache) => filesystem_cache.store(hash, module),
};
let hash = Hash::generate(&module_bytes);
let load_cold_wasm_bytes =
|| Module::new(&wasmer_store, module_bytes.as_ref()).context("Cannot compile plugin");
let module = if let Some(cache) = cache {
let cached_module =
load_from_cache(cache, hash).context("Failed to load plugin from cache");
match cached_module {
Ok(module) => Ok(module),
Err(err) => {
let loaded_module = load_cold_wasm_bytes().map_err(|_| err);
match &loaded_module {
Ok(module) => {
if let Err(err) = store_into_cache(cache, hash, module) {
loaded_module
.map_err(|_| err)
.context("Failed to store compiled plugin into cache")
} else {
loaded_module
}
}
Err(..) => loaded_module,
}
}
}
} else {
load_cold_wasm_bytes()
};
let module = cache.load_module(plugin_path);
return match module {
Ok(module) => {
let wasmer_store = module.store();
let transform_result: Arc<Mutex<Vec<u8>>> = Arc::new(Mutex::new(vec![]));
let set_transform_result_fn_decl = Function::new_native_with_env(
&wasmer_store,
wasmer_store,
HostEnvironment {
memory: LazyInit::default(),
transform_result: transform_result.clone(),
@ -298,7 +215,7 @@ fn load_plugin(
);
let emit_diagnostics_fn_decl = Function::new_native_with_env(
&wasmer_store,
wasmer_store,
HostEnvironment {
memory: LazyInit::default(),
transform_result: transform_result.clone(),
@ -306,14 +223,13 @@ fn load_plugin(
emit_diagnostics,
);
let mark_fresh_fn_decl = Function::new_native(&wasmer_store, mark_fresh_proxy);
let mark_parent_fn_decl = Function::new_native(&wasmer_store, mark_parent_proxy);
let mark_is_builtin_fn_decl =
Function::new_native(&wasmer_store, mark_is_builtin_proxy);
let mark_fresh_fn_decl = Function::new_native(wasmer_store, mark_fresh_proxy);
let mark_parent_fn_decl = Function::new_native(wasmer_store, mark_parent_proxy);
let mark_is_builtin_fn_decl = Function::new_native(wasmer_store, mark_is_builtin_proxy);
let mark_set_builtin_fn_decl =
Function::new_native(&wasmer_store, mark_set_builtin_proxy);
Function::new_native(wasmer_store, mark_set_builtin_proxy);
let mark_is_descendant_of_fn_decl = Function::new_native_with_env(
&wasmer_store,
wasmer_store,
HostEnvironment {
memory: LazyInit::default(),
transform_result: transform_result.clone(),
@ -322,7 +238,7 @@ fn load_plugin(
);
let mark_least_ancestor_fn_decl = Function::new_native_with_env(
&wasmer_store,
wasmer_store,
HostEnvironment {
memory: LazyInit::default(),
transform_result: transform_result.clone(),
@ -331,9 +247,9 @@ fn load_plugin(
);
let syntax_context_apply_mark_fn_decl =
Function::new_native(&wasmer_store, syntax_context_apply_mark_proxy);
Function::new_native(wasmer_store, syntax_context_apply_mark_proxy);
let syntax_context_remove_mark_fn_decl = Function::new_native_with_env(
&wasmer_store,
wasmer_store,
HostEnvironment {
memory: LazyInit::default(),
transform_result: transform_result.clone(),
@ -341,7 +257,7 @@ fn load_plugin(
syntax_context_remove_mark_proxy,
);
let syntax_context_outer_fn_decl =
Function::new_native(&wasmer_store, syntax_context_outer_proxy);
Function::new_native(wasmer_store, syntax_context_outer_proxy);
// Plugin binary can be either wasm32-wasi or wasm32-unknown-unknown
let import_object = if is_wasi_module(&module) {
@ -431,7 +347,7 @@ struct PluginTransformTracker {
}
impl PluginTransformTracker {
fn new(path: &Path, cache: &mut Option<PluginCache>) -> Result<PluginTransformTracker, Error> {
fn new(path: &Path, cache: &Lazy<PluginModuleCache>) -> Result<PluginTransformTracker, Error> {
let (instance, transform_result) = load_plugin(path, cache)?;
let tracker = PluginTransformTracker {
@ -534,7 +450,7 @@ impl Drop for PluginTransformTracker {
pub fn apply_js_plugin(
plugin_name: &str,
path: &Path,
cache: &mut Option<PluginCache>,
cache: &Lazy<PluginModuleCache>,
program: Serialized,
config_json: Serialized,
context_json: Serialized,

View File

@ -1,38 +0,0 @@
use std::{env::current_dir, path::PathBuf};
use anyhow::{Context, Error};
use wasmer_cache::FileSystemCache;
/// Type of cache to store compiled bytecodes of plugins.
/// Currently only supports filesystem, but _may_ supports
/// other type (in-memory, etcs) for the long-running processes like devserver.
pub enum PluginCache {
File(FileSystemCache),
}
/// Build a path to cache location where plugin's bytecode cache will be stored.
/// This fn does a side effect to create path to cache if given path is not
/// resolvable. If root is not specified, it'll generate default root for cache
/// location.
///
/// Note SWC's plugin should not fail to load when cache location is not
/// available. It'll make each invocation to cold start.
pub fn resolve_plugin_cache_root(root: Option<String>) -> Result<PluginCache, Error> {
let root_path = match root {
Some(root) => {
let mut root = PathBuf::from(root);
root.push("plugins");
root
}
None => {
let mut cwd = current_dir().context("failed to get current directory")?;
cwd.push(".swc");
cwd.push("plugins");
cwd
}
};
FileSystemCache::new(root_path)
.map(PluginCache::File)
.context("Failed to create cache location for the plugins")
}

View File

@ -5,10 +5,11 @@ use std::{
};
use anyhow::{anyhow, Error};
use swc_common::{errors::HANDLER, plugin::Serialized, FileName};
use swc_common::{errors::HANDLER, plugin::Serialized, sync::Lazy, FileName};
use swc_ecma_ast::{CallExpr, Callee, EsVersion, Expr, Lit, MemberExpr, Program, Str};
use swc_ecma_parser::{lexer::Lexer, EsConfig, Parser, StringInput, Syntax};
use swc_ecma_visit::{Visit, VisitWith};
use swc_plugin_runner::cache::PluginModuleCache;
/// Returns the path to the built plugin
fn build_plugin(dir: &Path) -> Result<PathBuf, Error> {
@ -85,10 +86,12 @@ fn internal() -> Result<(), Error> {
let context = Serialized::serialize(&"{sourceFileName: 'single_plugin_test'}".to_string())
.expect("Should serializable");
let cache: Lazy<PluginModuleCache> = Lazy::new(PluginModuleCache::new);
let program_bytes = swc_plugin_runner::apply_js_plugin(
"internal-test",
&path,
&mut None,
&cache,
program,
config,
context,
@ -131,11 +134,13 @@ fn internal() -> Result<(), Error> {
Serialized::serialize(&"{sourceFileName: 'single_plugin_handler_test'}".to_string())
.expect("Should serializable");
let cache: Lazy<PluginModuleCache> = Lazy::new(PluginModuleCache::new);
let _res = HANDLER.set(&handler, || {
swc_plugin_runner::apply_js_plugin(
"internal-test",
&path,
&mut None,
&cache,
program,
config,
context,
@ -164,11 +169,12 @@ fn internal() -> Result<(), Error> {
let program = parser.parse_program().unwrap();
let mut serialized_program = Serialized::serialize(&program).expect("Should serializable");
let cache: Lazy<PluginModuleCache> = Lazy::new(PluginModuleCache::new);
serialized_program = swc_plugin_runner::apply_js_plugin(
"internal-test",
&path,
&mut None,
&cache,
serialized_program,
Serialized::serialize(&"{}".to_string()).expect("Should serializable"),
Serialized::serialize(&"{sourceFileName: 'multiple_plugin_test'}".to_string())
@ -180,7 +186,7 @@ fn internal() -> Result<(), Error> {
serialized_program = swc_plugin_runner::apply_js_plugin(
"internal-test",
&path,
&mut None,
&cache,
serialized_program,
Serialized::serialize(&"{}".to_string()).expect("Should serializable"),
Serialized::serialize(&"{sourceFileName: 'multiple_plugin_test2'}".to_string())

View File

@ -56,6 +56,13 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
[[package]]
name = "better_scoped_tls"
version = "0.1.0"
dependencies = [
"scoped-tls",
]
[[package]]
name = "bitflags"
version = "1.3.2"
@ -685,11 +692,12 @@ dependencies = [
[[package]]
name = "swc_common"
version = "0.17.5"
version = "0.17.7"
dependencies = [
"ahash",
"anyhow",
"ast_node",
"better_scoped_tls",
"cfg-if 0.1.10",
"debug_unreachable",
"either",
@ -699,7 +707,6 @@ dependencies = [
"owning_ref",
"rkyv",
"rustc-hash",
"scoped-tls",
"serde",
"siphasher",
"string_cache",
@ -766,7 +773,7 @@ dependencies = [
[[package]]
name = "swc_plugin"
version = "0.27.0"
version = "0.27.1"
dependencies = [
"swc_atoms",
"swc_common",