spin_core/
lib.rs

1//! Spin core execution engine
2//!
3//! This crate provides low-level Wasm functionality required by Spin. Most of
4//! this functionality consists of wrappers around [`wasmtime`] that narrow the
5//! flexibility of `wasmtime` to the set of features used by Spin (such as only
6//! supporting `wasmtime`'s async calling style).
7
8#![deny(missing_docs)]
9
10mod limits;
11mod store;
12
13use std::sync::OnceLock;
14use std::{path::PathBuf, time::Duration};
15
16use anyhow::Result;
17use tracing::instrument;
18use wasmtime::{InstanceAllocationStrategy, PoolingAllocationConfig};
19
20pub use async_trait::async_trait;
21pub use wasmtime::Engine as WasmtimeEngine;
22pub use wasmtime::{
23    self,
24    component::{Component, Instance, InstancePre, Linker},
25    Instance as ModuleInstance, Module, Trap,
26};
27
28pub use store::{AsState, Store, StoreBuilder};
29
30/// The default [`EngineBuilder::epoch_tick_interval`].
31pub const DEFAULT_EPOCH_TICK_INTERVAL: Duration = Duration::from_millis(10);
32
33const MB: u64 = 1 << 20;
34const GB: usize = 1 << 30;
35
36/// Global configuration for `EngineBuilder`.
37///
38/// This is currently only used for advanced (undocumented) use cases.
39pub struct Config {
40    inner: wasmtime::Config,
41}
42
43impl Config {
44    /// Borrow the inner wasmtime::Config mutably.
45    /// WARNING: This is inherently unstable and may break at any time!
46    #[doc(hidden)]
47    pub fn wasmtime_config(&mut self) -> &mut wasmtime::Config {
48        &mut self.inner
49    }
50
51    /// Enable the Wasmtime compilation cache. If `path` is given it will override
52    /// the system default path.
53    ///
54    /// For more information, see the [Wasmtime cache config documentation][docs].
55    ///
56    /// [docs]: https://docs.wasmtime.dev/cli-cache.html
57    pub fn enable_cache(&mut self, config_path: &Option<PathBuf>) -> Result<()> {
58        match config_path {
59            Some(p) => self.inner.cache_config_load(p)?,
60            None => self.inner.cache_config_load_default()?,
61        };
62
63        Ok(())
64    }
65
66    /// Disable the pooling instance allocator.
67    pub fn disable_pooling(&mut self) -> &mut Self {
68        self.inner
69            .allocation_strategy(wasmtime::InstanceAllocationStrategy::OnDemand);
70        self
71    }
72}
73
74impl Default for Config {
75    fn default() -> Self {
76        let mut inner = wasmtime::Config::new();
77        inner.async_support(true);
78        inner.epoch_interruption(true);
79        inner.wasm_component_model(true);
80        // If targeting musl, disable native unwind to address this issue:
81        // https://github.com/spinframework/spin/issues/2889
82        // TODO: remove this when wasmtime is updated to >= v27.0.0
83        #[cfg(all(target_os = "linux", target_env = "musl"))]
84        inner.native_unwind_info(false);
85
86        if use_pooling_allocator_by_default() {
87            // By default enable the pooling instance allocator in Wasmtime. This
88            // drastically reduces syscall/kernel overhead for wasm execution,
89            // especially in async contexts where async stacks must be allocated.
90            // The general goal here is that the default settings here rarely, if
91            // ever, need to be modified. As a result there aren't fine-grained
92            // knobs for each of these settings just yet and instead they're
93            // generally set to defaults. Environment-variable-based fallbacks are
94            // supported though as an escape valve for if this is a problem.
95            let mut pooling_config = PoolingAllocationConfig::default();
96            pooling_config
97                .total_component_instances(env("SPIN_WASMTIME_INSTANCE_COUNT", 1_000))
98                // This number accounts for internal data structures that Wasmtime allocates for each instance.
99                // Instance allocation is proportional to the number of "things" in a wasm module like functions,
100                // globals, memories, etc. Instance allocations are relatively small and are largely inconsequential
101                // compared to other runtime state, but a number needs to be chosen here so a relatively large threshold
102                // of 10MB is arbitrarily chosen. It should be unlikely that any reasonably-sized module hits this limit.
103                .max_component_instance_size(env("SPIN_WASMTIME_INSTANCE_SIZE", 10 * MB) as usize)
104                .max_core_instance_size(env("SPIN_WASMTIME_CORE_INSTANCE_SIZE", 10 * MB) as usize)
105                .max_core_instances_per_component(env("SPIN_WASMTIME_CORE_INSTANCE_COUNT", 200))
106                .max_tables_per_component(env("SPIN_WASMTIME_INSTANCE_TABLES", 20))
107                .table_elements(env("SPIN_WASMTIME_INSTANCE_TABLE_ELEMENTS", 100_000))
108                // The number of memories an instance can have effectively limits the number of inner components
109                // a composed component can have (since each inner component has its own memory). We default to 32 for now, and
110                // we'll see how often this limit gets reached.
111                .max_memories_per_component(env("SPIN_WASMTIME_INSTANCE_MEMORIES", 32))
112                .total_memories(env("SPIN_WASMTIME_TOTAL_MEMORIES", 1_000))
113                .total_tables(env("SPIN_WASMTIME_TOTAL_TABLES", 2_000))
114                // Nothing is lost from allowing the maximum size of memory for
115                // all instance as it's still limited through other the normal
116                // `StoreLimitsAsync` accounting method too.
117                .max_memory_size(4 * GB)
118                // These numbers are completely arbitrary at something above 0.
119                .linear_memory_keep_resident(env(
120                    "SPIN_WASMTIME_LINEAR_MEMORY_KEEP_RESIDENT",
121                    2 * MB,
122                ) as usize)
123                .table_keep_resident(env("SPIN_WASMTIME_TABLE_KEEP_RESIDENT", MB / 2) as usize);
124            inner.allocation_strategy(InstanceAllocationStrategy::Pooling(pooling_config));
125        }
126
127        return Self { inner };
128
129        fn env<T>(name: &str, default: T) -> T
130        where
131            T: std::str::FromStr,
132            T::Err: std::fmt::Display,
133        {
134            match std::env::var(name) {
135                Ok(val) => val
136                    .parse()
137                    .unwrap_or_else(|e| panic!("failed to parse env var `{name}={val}`: {e}")),
138                Err(_) => default,
139            }
140        }
141    }
142}
143
144/// The pooling allocator is tailor made for the `spin up` use case, so
145/// try to use it when we can. The main cost of the pooling allocator, however,
146/// is the virtual memory required to run it. Not all systems support the same
147/// amount of virtual memory, for example some aarch64 and riscv64 configuration
148/// only support 39 bits of virtual address space.
149///
150/// The pooling allocator, by default, will request 1000 linear memories each
151/// sized at 6G per linear memory. This is 6T of virtual memory which ends up
152/// being about 42 bits of the address space. This exceeds the 39 bit limit of
153/// some systems, so there the pooling allocator will fail by default.
154///
155/// This function attempts to dynamically determine the hint for the pooling
156/// allocator. This returns `true` if the pooling allocator should be used
157/// by default, or `false` otherwise.
158///
159/// The method for testing this is to allocate a 0-sized 64-bit linear memory
160/// with a maximum size that's N bits large where we force all memories to be
161/// static. This should attempt to acquire N bits of the virtual address space.
162/// If successful that should mean that the pooling allocator is OK to use, but
163/// if it fails then the pooling allocator is not used and the normal mmap-based
164/// implementation is used instead.
165fn use_pooling_allocator_by_default() -> bool {
166    static USE_POOLING: OnceLock<bool> = OnceLock::new();
167    const BITS_TO_TEST: u32 = 42;
168
169    *USE_POOLING.get_or_init(|| {
170        // Enable manual control through env vars as an escape hatch
171        match std::env::var("SPIN_WASMTIME_POOLING") {
172            Ok(s) if s == "1" => return true,
173            Ok(s) if s == "0" => return false,
174            Ok(s) => panic!("SPIN_WASMTIME_POOLING={s} not supported, only 1/0 supported"),
175            Err(_) => {}
176        }
177
178        // If the env var isn't set then perform the dynamic runtime probe
179        let mut config = wasmtime::Config::new();
180        config.wasm_memory64(true);
181        config.memory_reservation(1 << BITS_TO_TEST);
182
183        match wasmtime::Engine::new(&config) {
184            Ok(engine) => {
185                let mut store = wasmtime::Store::new(&engine, ());
186                // NB: the maximum size is in wasm pages so take out the 16-bits
187                // of wasm page size here from the maximum size.
188                let ty = wasmtime::MemoryType::new64(0, Some(1 << (BITS_TO_TEST - 16)));
189                wasmtime::Memory::new(&mut store, ty).is_ok()
190            }
191            Err(_) => {
192                tracing::debug!(
193                    "unable to create an engine to test the pooling \
194                     allocator, disabling pooling allocation"
195                );
196                false
197            }
198        }
199    })
200}
201
202/// Host state data associated with individual [Store]s and [Instance]s.
203#[derive(Default)]
204pub struct State {
205    store_limits: limits::StoreLimitsAsync,
206}
207
208impl State {
209    /// Get the amount of memory in bytes consumed by instances in the store
210    pub fn memory_consumed(&self) -> u64 {
211        self.store_limits.memory_consumed()
212    }
213}
214
215/// A builder interface for configuring a new [`Engine`].
216///
217/// A new [`EngineBuilder`] can be obtained with [`Engine::builder`].
218pub struct EngineBuilder<T> {
219    engine: wasmtime::Engine,
220    linker: Linker<T>,
221    epoch_tick_interval: Duration,
222    epoch_ticker_thread: bool,
223}
224
225impl<T> EngineBuilder<T> {
226    fn new(config: &Config) -> Result<Self> {
227        let engine = wasmtime::Engine::new(&config.inner)?;
228        let linker: Linker<T> = Linker::new(&engine);
229        Ok(Self {
230            engine,
231            linker,
232            epoch_tick_interval: DEFAULT_EPOCH_TICK_INTERVAL,
233            epoch_ticker_thread: true,
234        })
235    }
236
237    /// Returns a reference to the [`Linker`] for this [`Engine`].
238    pub fn linker(&mut self) -> &mut Linker<T> {
239        &mut self.linker
240    }
241
242    /// Sets the epoch tick internal for the built [`Engine`].
243    ///
244    /// This is used by [`Store::set_deadline`] to calculate the number of
245    /// "ticks" for epoch interruption, and by the default epoch ticker thread.
246    /// The default is [`DEFAULT_EPOCH_TICK_INTERVAL`].
247    ///
248    /// See [`EngineBuilder::epoch_ticker_thread`] and
249    /// [`wasmtime::Config::epoch_interruption`](https://docs.rs/wasmtime/latest/wasmtime/struct.Config.html#method.epoch_interruption).
250    pub fn epoch_tick_interval(&mut self, interval: Duration) {
251        self.epoch_tick_interval = interval;
252    }
253
254    /// Configures whether the epoch ticker thread will be spawned when this
255    /// [`Engine`] is built.
256    ///
257    /// Enabled by default; if disabled, the user must arrange to call
258    /// `engine.as_ref().increment_epoch()` every `epoch_tick_interval` or
259    /// interrupt-based features like `Store::set_deadline` will not work.
260    pub fn epoch_ticker_thread(&mut self, enable: bool) {
261        self.epoch_ticker_thread = enable;
262    }
263
264    fn maybe_spawn_epoch_ticker(&self) {
265        if !self.epoch_ticker_thread {
266            return;
267        }
268        let engine_weak = self.engine.weak();
269        let interval = self.epoch_tick_interval;
270        std::thread::spawn(move || loop {
271            std::thread::sleep(interval);
272            let Some(engine) = engine_weak.upgrade() else {
273                break;
274            };
275            engine.increment_epoch();
276        });
277    }
278
279    /// Builds an [`Engine`] from this builder.
280    pub fn build(self) -> Engine<T> {
281        self.maybe_spawn_epoch_ticker();
282        Engine {
283            inner: self.engine,
284            linker: self.linker,
285            epoch_tick_interval: self.epoch_tick_interval,
286        }
287    }
288}
289
290/// An `Engine` is a global context for the initialization and execution of
291/// Spin components.
292pub struct Engine<T> {
293    inner: wasmtime::Engine,
294    linker: Linker<T>,
295    epoch_tick_interval: Duration,
296}
297
298impl<T> Engine<T> {
299    /// Creates a new [`EngineBuilder`] with the given [`Config`].
300    pub fn builder(config: &Config) -> Result<EngineBuilder<T>> {
301        EngineBuilder::new(config)
302    }
303
304    /// Creates a new [`StoreBuilder`].
305    pub fn store_builder(&self) -> StoreBuilder {
306        StoreBuilder::new(self.inner.clone(), self.epoch_tick_interval)
307    }
308
309    /// Creates a new [`InstancePre`] for the given [`Component`].
310    #[instrument(skip_all, level = "debug")]
311    pub fn instantiate_pre(&self, component: &Component) -> Result<InstancePre<T>> {
312        self.linker.instantiate_pre(component)
313    }
314}
315
316impl<T> AsRef<wasmtime::Engine> for Engine<T> {
317    fn as_ref(&self) -> &wasmtime::Engine {
318        &self.inner
319    }
320}