feat: enable wasmtime caching (#605)

Alternate to: #596 without support for manually compiling/loading native
code

- Enables wasmtime caching: https://docs.wasmtime.dev/cli-cache.html
- Adds `EXTISM_CACHE_CONFIG` and `PluginBuilder::with_cache_config` to
determine where to load a custom cache configuration from
- Adds `PluginBuilder::with_cache_disabled` to disable the cache when
initializing a plugin
  - Setting `EXTISM_CACHE_CONFIG=""` will also disable caching 

## Performance

With caching:
```
create/create_plugin    time:   [2.9079 ms 2.9139 ms 2.9200 ms]                                  
                        change: [+3.2677% +3.6399% +3.9766%] (p = 0.00 < 0.20)
                        Change within noise threshold.
```

Compared to `main`:
```
create/create_plugin    time:   [26.089 ms 26.498 ms 26.923 ms]                                 
                        change: [+0.1729% +2.0868% +4.1337%] (p = 0.04 < 0.20)
                        Change within noise threshold.
```
This commit is contained in:
zach
2023-11-28 11:50:21 -08:00
committed by GitHub
parent 938e3af7f0
commit a517cd23be
8 changed files with 261 additions and 126 deletions

4
.gitignore vendored
View File

@@ -46,4 +46,6 @@ java/.DS_Store
extism-maturin/src/extism.h
runtime/*.log
libextism/example
libextism/extism*.pc
libextism/extism*.pc
*.cwasm
test-cache

View File

@@ -181,6 +181,18 @@ impl Wasm {
Wasm::Url { req: _, meta } => meta,
}
}
/// Update Wasm module name
pub fn with_name(mut self, name: impl Into<String>) -> Self {
self.meta_mut().name = Some(name.into());
self
}
/// Update Wasm module hash
pub fn with_hash(mut self, hash: impl Into<String>) -> Self {
self.meta_mut().hash = Some(hash.into());
self
}
}
#[cfg(feature = "json_schema")]
@@ -237,6 +249,11 @@ impl Manifest {
}
}
pub fn with_wasm(mut self, wasm: impl Into<Wasm>) -> Self {
self.wasm.push(wasm.into());
self
}
/// Disallow HTTP requests to all hosts
pub fn disallow_all_hosts(mut self) -> Self {
self.allowed_hosts = Some(vec![]);
@@ -329,7 +346,7 @@ mod base64 {
use serde::{Deserializer, Serializer};
pub fn serialize<S: Serializer>(v: &Vec<u8>, s: S) -> Result<S::Ok, S::Error> {
let base64 = general_purpose::STANDARD.encode(v);
let base64 = general_purpose::STANDARD.encode(v.as_slice());
String::serialize(&base64, s)
}

View File

@@ -24,6 +24,7 @@ There are a few environment variables that can be used for debugging purposes:
- `EXTISM_COREDUMP=extism.core`: write [coredump](https://github.com/WebAssembly/tool-conventions/blob/main/Coredump.md) to a file when a WebAssembly function traps
- `EXTISM_DEBUG=1`: generate debug information
- `EXTISM_PROFILE=perf|jitdump|vtune`: enable Wasmtime profiling
- `EXTISM_CACHE_CONFIG=path/to/config.toml`: enable Wasmtime cache, see [the docs](https://docs.wasmtime.dev/cli-cache.html) for details about configuration. Setting this to an empty string will disable caching.
> *Note*: The debug and coredump info will only be written if the plug-in has an error.

View File

@@ -23,11 +23,10 @@ pub use current_plugin::CurrentPlugin;
pub use extism_convert::{FromBytes, FromBytesOwned, ToBytes};
pub use extism_manifest::{Manifest, Wasm, WasmMetadata};
pub use function::{Function, UserData, Val, ValType, PTR};
pub use plugin::{CancelHandle, Plugin, EXTISM_ENV_MODULE, EXTISM_USER_MODULE};
pub use plugin_builder::PluginBuilder;
pub use plugin::{CancelHandle, Plugin, WasmInput, EXTISM_ENV_MODULE, EXTISM_USER_MODULE};
pub use plugin_builder::{DebugOptions, PluginBuilder};
pub(crate) use internal::{Internal, Wasi};
pub(crate) use plugin_builder::DebugOptions;
pub(crate) use timer::{Timer, TimerAction};
pub(crate) use tracing::{debug, error, trace, warn};

View File

@@ -4,6 +4,7 @@ use std::io::Read;
use sha2::Digest;
use crate::plugin::WasmInput;
use crate::*;
fn hex(data: &[u8]) -> String {
@@ -14,32 +15,9 @@ fn hex(data: &[u8]) -> String {
s
}
#[allow(unused)]
fn cache_add_file(hash: &str, data: &[u8]) -> Result<(), Error> {
let cache_dir = std::env::temp_dir().join("extism-cache");
let _ = std::fs::create_dir(&cache_dir);
let file = cache_dir.join(hash);
if file.exists() {
return Ok(());
}
std::fs::write(file, data)?;
Ok(())
}
fn cache_get_file(hash: &str) -> Result<Option<Vec<u8>>, Error> {
let cache_dir = std::env::temp_dir().join("extism-cache");
let file = cache_dir.join(hash);
if file.exists() {
let r = std::fs::read(file)?;
return Ok(Some(r));
}
Ok(None)
}
fn check_hash(hash: &Option<String>, data: &[u8]) -> Result<(), Error> {
fn check_hash(hash: &Option<String>, data: &[u8]) -> Result<Option<String>, Error> {
match hash {
None => Ok(()),
None => Ok(None),
Some(hash) => {
let digest = sha2::Sha256::digest(data);
let hex = hex(&digest);
@@ -50,7 +28,7 @@ fn check_hash(hash: &Option<String>, data: &[u8]) -> Result<(), Error> {
hash
));
}
Ok(())
Ok(Some(hex))
}
}
}
@@ -80,7 +58,6 @@ fn to_module(engine: &Engine, wasm: &extism_manifest::Wasm) -> Result<(String, M
file.read_to_end(&mut buf)?;
check_hash(&meta.hash, &buf)?;
Ok((name, Module::new(engine, buf)?))
}
extism_manifest::Wasm::Data { meta, data } => {
@@ -117,17 +94,9 @@ fn to_module(engine: &Engine, wasm: &extism_manifest::Wasm) -> Result<(String, M
}
};
if let Some(h) = &meta.hash {
if let Ok(Some(data)) = cache_get_file(h) {
check_hash(&meta.hash, &data)?;
let module = Module::new(engine, data)?;
return Ok((name.to_string(), module));
}
}
#[cfg(not(feature = "register-http"))]
{
return Err(anyhow::format_err!("HTTP registration is disabled"));
return anyhow::bail!("HTTP registration is disabled");
}
#[cfg(feature = "register-http")]
@@ -144,15 +113,12 @@ fn to_module(engine: &Engine, wasm: &extism_manifest::Wasm) -> Result<(String, M
let mut data = Vec::new();
r.read_to_end(&mut data)?;
// Try to cache file
if let Some(hash) = &meta.hash {
cache_add_file(hash, &data);
}
// Check hash against manifest
check_hash(&meta.hash, &data)?;
// Convert fetched data to module
let module = Module::new(engine, data)?;
Ok((name.to_string(), module))
}
}
@@ -163,52 +129,64 @@ const WASM_MAGIC: [u8; 4] = [0x00, 0x61, 0x73, 0x6d];
pub(crate) fn load(
engine: &Engine,
data: &[u8],
input: WasmInput<'_>,
) -> Result<(extism_manifest::Manifest, BTreeMap<String, Module>), Error> {
let extism_module = Module::new(engine, WASM)?;
let has_magic = data.len() >= 4 && data[0..4] == WASM_MAGIC;
let is_wast = data.starts_with(b"(module") || data.starts_with(b";;");
if !has_magic && !is_wast {
trace!("Loading manifest");
if let Ok(s) = std::str::from_utf8(data) {
if let Ok(t) = toml::from_str::<extism_manifest::Manifest>(s) {
trace!("Manifest is TOML");
let mut m = modules(&t, engine)?;
m.insert(EXTISM_ENV_MODULE.to_string(), extism_module);
return Ok((t, m));
let mut mods = BTreeMap::new();
mods.insert(EXTISM_ENV_MODULE.to_string(), Module::new(engine, WASM)?);
match input {
WasmInput::Data(data) => {
let has_magic = data.len() >= 4 && data[0..4] == WASM_MAGIC;
let is_wat = data.starts_with(b"(module") || data.starts_with(b";;");
if !has_magic && !is_wat {
trace!("Loading manifest");
if let Ok(s) = std::str::from_utf8(&data) {
let t = if let Ok(t) = toml::from_str::<extism_manifest::Manifest>(s) {
trace!("Manifest is TOML");
modules(engine, &t, &mut mods)?;
t
} else if let Ok(t) = serde_json::from_str::<extism_manifest::Manifest>(s) {
trace!("Manifest is JSON");
modules(engine, &t, &mut mods)?;
t
} else {
anyhow::bail!("Unknown manifest format");
};
return Ok((t, mods));
}
}
let m = Module::new(engine, data)?;
mods.insert("main".to_string(), m);
Ok((Default::default(), mods))
}
WasmInput::Manifest(m) => {
trace!("Loading from existing manifest");
modules(engine, &m, &mut mods)?;
Ok((m, mods))
}
WasmInput::ManifestRef(m) => {
trace!("Loading from existing manifest");
modules(engine, m, &mut mods)?;
Ok((m.clone(), mods))
}
let t = serde_json::from_slice::<extism_manifest::Manifest>(data)?;
trace!("Manifest is JSON");
let mut m = modules(&t, engine)?;
m.insert(EXTISM_ENV_MODULE.to_string(), extism_module);
return Ok((t, m));
}
trace!("Loading WASM module bytes");
let m = Module::new(engine, data)?;
let mut modules = BTreeMap::new();
modules.insert(EXTISM_ENV_MODULE.to_string(), extism_module);
modules.insert("main".to_string(), m);
Ok((Default::default(), modules))
}
pub(crate) fn modules(
manifest: &extism_manifest::Manifest,
engine: &Engine,
) -> Result<BTreeMap<String, Module>, Error> {
manifest: &extism_manifest::Manifest,
modules: &mut BTreeMap<String, Module>,
) -> Result<(), Error> {
if manifest.wasm.is_empty() {
return Err(anyhow::format_err!("No wasm files specified"));
}
let mut modules = BTreeMap::new();
// If there's only one module, it should be called `main`
if manifest.wasm.len() == 1 {
let (_, m) = to_module(engine, &manifest.wasm[0])?;
modules.insert("main".to_string(), m);
return Ok(modules);
return Ok(());
}
for f in &manifest.wasm {
@@ -217,5 +195,5 @@ pub(crate) fn modules(
modules.insert(name, m);
}
Ok(modules)
Ok(())
}

View File

@@ -1,4 +1,4 @@
use std::collections::BTreeMap;
use std::{collections::BTreeMap, path::PathBuf};
use crate::*;
@@ -109,7 +109,7 @@ impl Internal for Plugin {
}
}
fn profiling_strategy() -> ProfilingStrategy {
pub(crate) fn profiling_strategy() -> ProfilingStrategy {
match std::env::var("EXTISM_PROFILE").as_deref() {
Ok("perf") => ProfilingStrategy::PerfMap,
Ok("jitdump") => ProfilingStrategy::JitDump,
@@ -122,55 +122,106 @@ fn profiling_strategy() -> ProfilingStrategy {
}
}
pub trait WasmInput<'a>: Into<std::borrow::Cow<'a, [u8]>> {}
/// Defines an input type for Wasm data.
///
/// Types that implement `Into<WasmInput>` can be passed directly into `Plugin::new`
pub enum WasmInput<'a> {
/// Raw Wasm module
Data(std::borrow::Cow<'a, [u8]>),
/// Owned manifest
Manifest(Manifest),
/// Borrowed manifest
ManifestRef(&'a Manifest),
}
impl<'a> WasmInput<'a> for Manifest {}
impl<'a> WasmInput<'a> for &Manifest {}
impl<'a> WasmInput<'a> for &'a [u8] {}
impl<'a> WasmInput<'a> for Vec<u8> {}
impl<'a> From<Manifest> for WasmInput<'a> {
fn from(value: Manifest) -> Self {
WasmInput::Manifest(value)
}
}
impl<'a> From<&'a Manifest> for WasmInput<'a> {
fn from(value: &'a Manifest) -> Self {
WasmInput::ManifestRef(value)
}
}
impl<'a> From<&'a mut Manifest> for WasmInput<'a> {
fn from(value: &'a mut Manifest) -> Self {
WasmInput::ManifestRef(value)
}
}
impl<'a> From<&'a [u8]> for WasmInput<'a> {
fn from(value: &'a [u8]) -> Self {
WasmInput::Data(value.into())
}
}
impl<'a> From<&'a str> for WasmInput<'a> {
fn from(value: &'a str) -> Self {
WasmInput::Data(value.as_bytes().into())
}
}
impl<'a> From<Vec<u8>> for WasmInput<'a> {
fn from(value: Vec<u8>) -> Self {
WasmInput::Data(value.into())
}
}
impl<'a> From<&'a Vec<u8>> for WasmInput<'a> {
fn from(value: &'a Vec<u8>) -> Self {
WasmInput::Data(value.into())
}
}
impl Plugin {
/// Create a new plugin from a Manifest or WebAssembly module, and host functions. The `with_wasi`
/// parameter determines whether or not the module should be executed with WASI enabled.
pub fn new<'a>(
wasm: impl WasmInput<'a>,
wasm: impl Into<WasmInput<'a>>,
imports: impl IntoIterator<Item = Function>,
with_wasi: bool,
) -> Result<Plugin, Error> {
Self::build_new(wasm.into(), imports, with_wasi, Default::default())
Self::build_new(wasm.into(), imports, with_wasi, Default::default(), None)
}
pub(crate) fn build_new(
wasm: impl AsRef<[u8]>,
wasm: WasmInput<'_>,
imports: impl IntoIterator<Item = Function>,
with_wasi: bool,
mut debug_options: DebugOptions,
debug_options: DebugOptions,
cache_dir: Option<Option<PathBuf>>,
) -> Result<Plugin, Error> {
// Configure debug options
debug_options.debug_info =
debug_options.debug_info || std::env::var("EXTISM_DEBUG").is_ok();
if let Ok(x) = std::env::var("EXTISM_COREDUMP") {
debug_options.coredump = Some(std::path::PathBuf::from(x));
};
if let Ok(x) = std::env::var("EXTISM_MEMDUMP") {
debug_options.memdump = Some(std::path::PathBuf::from(x));
};
let profiling_strategy = debug_options
.profiling_strategy
.map_or(ProfilingStrategy::None, |_| profiling_strategy());
debug_options.profiling_strategy = Some(profiling_strategy);
// Setup wasmtime types
let engine = Engine::new(
Config::new()
.epoch_interruption(true)
.debug_info(debug_options.debug_info)
.coredump_on_trap(debug_options.coredump.is_some())
.profiler(profiling_strategy)
.wasm_tail_call(true)
.wasm_function_references(true),
)?;
let (manifest, modules) = manifest::load(&engine, wasm.as_ref())?;
let mut config = Config::new();
config
.epoch_interruption(true)
.debug_info(debug_options.debug_info)
.coredump_on_trap(debug_options.coredump.is_some())
.profiler(debug_options.profiling_strategy)
.wasm_tail_call(true)
.wasm_function_references(true);
match cache_dir {
Some(None) => (),
Some(Some(path)) => {
config.cache_config_load(path)?;
}
None => {
if let Ok(env) = std::env::var("EXTISM_CACHE_CONFIG") {
if !env.is_empty() {
config.cache_config_load(&env)?;
}
} else {
config.cache_config_load_default()?;
}
}
}
let engine = Engine::new(&config)?;
let (manifest, modules) = manifest::load(&engine, wasm)?;
let available_pages = manifest.memory.max_pages;
debug!("Available pages: {available_pages:?}");

View File

@@ -1,29 +1,55 @@
use std::path::PathBuf;
use crate::{plugin::WasmInput, *};
#[derive(Default, Clone)]
pub(crate) struct DebugOptions {
pub(crate) profiling_strategy: Option<wasmtime::ProfilingStrategy>,
pub(crate) coredump: Option<std::path::PathBuf>,
pub(crate) memdump: Option<std::path::PathBuf>,
pub(crate) debug_info: bool,
#[derive(Clone)]
pub struct DebugOptions {
pub profiling_strategy: wasmtime::ProfilingStrategy,
pub coredump: Option<std::path::PathBuf>,
pub memdump: Option<std::path::PathBuf>,
pub debug_info: bool,
}
impl Default for DebugOptions {
fn default() -> Self {
let debug_info = std::env::var("EXTISM_DEBUG").is_ok();
let coredump = if let Ok(x) = std::env::var("EXTISM_COREDUMP") {
Some(std::path::PathBuf::from(x))
} else {
None
};
let memdump = if let Ok(x) = std::env::var("EXTISM_MEMDUMP") {
Some(std::path::PathBuf::from(x))
} else {
None
};
DebugOptions {
profiling_strategy: plugin::profiling_strategy(),
coredump,
memdump,
debug_info,
}
}
}
/// PluginBuilder is used to configure and create `Plugin` instances
pub struct PluginBuilder<'a> {
source: std::borrow::Cow<'a, [u8]>,
source: WasmInput<'a>,
wasi: bool,
functions: Vec<Function>,
debug_options: DebugOptions,
cache_config: Option<Option<PathBuf>>,
}
impl<'a> PluginBuilder<'a> {
/// Create a new `PluginBuilder` from a `Manifest` or raw Wasm bytes
pub fn new(plugin: impl WasmInput<'a>) -> Self {
pub fn new(plugin: impl Into<WasmInput<'a>>) -> Self {
PluginBuilder {
source: plugin.into(),
wasi: false,
functions: vec![],
debug_options: DebugOptions::default(),
cache_config: None,
}
}
@@ -80,28 +106,56 @@ impl<'a> PluginBuilder<'a> {
self
}
/// Set profiling strategy
pub fn with_profiling_strategy(mut self, p: wasmtime::ProfilingStrategy) -> Self {
self.debug_options.profiling_strategy = Some(p);
self.debug_options.profiling_strategy = p;
self
}
pub fn with_coredump(mut self, path: impl AsRef<std::path::Path>) -> Self {
self.debug_options.coredump = Some(path.as_ref().to_path_buf());
/// Enable Wasmtime coredump on trap
pub fn with_coredump(mut self, path: impl Into<std::path::PathBuf>) -> Self {
self.debug_options.coredump = Some(path.into());
self
}
pub fn with_memdump(mut self, path: impl AsRef<std::path::Path>) -> Self {
self.debug_options.memdump = Some(path.as_ref().to_path_buf());
/// Enable Extism memory dump when plugin calls return an error
pub fn with_memdump(mut self, path: impl Into<std::path::PathBuf>) -> Self {
self.debug_options.memdump = Some(path.into());
self
}
/// Compile with debug info
pub fn with_debug_info(mut self) -> Self {
self.debug_options.debug_info = true;
self
}
/// Configure debug options
pub fn with_debug_options(mut self, options: DebugOptions) -> Self {
self.debug_options = options;
self
}
/// Set wasmtime compilation cache config path
pub fn with_cache_config(mut self, dir: impl Into<PathBuf>) -> Self {
self.cache_config = Some(Some(dir.into()));
self
}
/// Turn wasmtime compilation caching off
pub fn with_cache_disabled(mut self) -> Self {
self.cache_config = Some(None);
self
}
/// Generate a new plugin with the configured settings
pub fn build(self) -> Result<Plugin, Error> {
Plugin::build_new(self.source, self.functions, self.wasi, self.debug_options)
Plugin::build_new(
self.source,
self.functions,
self.wasi,
self.debug_options,
self.cache_config,
)
}
}

View File

@@ -539,3 +539,36 @@ fn test_http_post() {
assert!(!res.is_empty());
assert!(res.contains(&data));
}
#[test]
fn test_disable_cache() {
// Warmup cache
let _plugin: CountVowelsPlugin = PluginBuilder::new(WASM_NO_FUNCTIONS)
.build()
.unwrap()
.try_into()
.unwrap();
// This should be fast
let start = std::time::Instant::now();
let mut plugin: CountVowelsPlugin = PluginBuilder::new(WASM_NO_FUNCTIONS)
.build()
.unwrap()
.try_into()
.unwrap();
let t = std::time::Instant::now() - start;
let _output: Json<Count> = plugin.count_vowels("abc123").unwrap();
// This should take longer than the first run
let start = std::time::Instant::now();
let mut plugin: CountVowelsPlugin = PluginBuilder::new(WASM_NO_FUNCTIONS)
.with_cache_disabled()
.build()
.unwrap()
.try_into()
.unwrap();
let t1 = std::time::Instant::now() - start;
let _output: Json<Count> = plugin.count_vowels("abc123").unwrap();
assert!(t < t1);
}