Skip to main content

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, Instance as ModuleInstance, Module, Trap,
24    component::{Component, Instance, InstancePre, Linker},
25};
26
27pub use store::{AsState, Store, StoreBuilder};
28
29/// The default [`EngineBuilder::epoch_tick_interval`].
30pub const DEFAULT_EPOCH_TICK_INTERVAL: Duration = Duration::from_millis(10);
31
32const MB: u64 = 1 << 20;
33const GB: usize = 1 << 30;
34
35/// Global configuration for `EngineBuilder`.
36///
37/// This is currently only used for advanced (undocumented) use cases.
38pub struct Config {
39    inner: wasmtime::Config,
40}
41
42impl Config {
43    /// Borrow the inner wasmtime::Config mutably.
44    /// WARNING: This is inherently unstable and may break at any time!
45    #[doc(hidden)]
46    pub fn wasmtime_config(&mut self) -> &mut wasmtime::Config {
47        &mut self.inner
48    }
49
50    /// Enable the Wasmtime compilation cache. If `path` is given it will override
51    /// the system default path.
52    ///
53    /// For more information, see the [Wasmtime cache config documentation][docs].
54    ///
55    /// [docs]: https://docs.wasmtime.dev/cli-cache.html
56    pub fn enable_cache(&mut self, config_path: &Option<PathBuf>) -> Result<()> {
57        self.inner
58            .cache(Some(wasmtime::Cache::from_file(config_path.as_deref())?));
59
60        Ok(())
61    }
62
63    /// Disable the pooling instance allocator.
64    pub fn disable_pooling(&mut self) -> &mut Self {
65        self.inner
66            .allocation_strategy(wasmtime::InstanceAllocationStrategy::OnDemand);
67        self
68    }
69
70    /// Enable DWARF debug info emission and disable optimizations to allow
71    /// debugging Wasm guests with native debuggers (gdb/lldb).
72    pub fn enable_debug_info(&mut self) -> &mut Self {
73        self.inner
74            .debug_info(true)
75            .cranelift_opt_level(wasmtime::OptLevel::None);
76        self
77    }
78}
79
80impl Default for Config {
81    fn default() -> Self {
82        let mut inner = wasmtime::Config::new();
83        inner.epoch_interruption(true);
84        inner.wasm_component_model(true);
85        inner.wasm_component_model_async(true);
86        // If targeting musl, disable native unwind to address this issue:
87        // https://github.com/spinframework/spin/issues/2889
88        // TODO: remove this when wasmtime is updated to >= v27.0.0
89        #[cfg(all(target_os = "linux", target_env = "musl"))]
90        inner.native_unwind_info(false);
91
92        if use_pooling_allocator_by_default() {
93            // Baseline for the maximum number of instances in spin through
94            // which a number of other defaults are derived below.
95            let max_instances = env("SPIN_MAX_INSTANCE_COUNT", 1_000);
96
97            // By default enable the pooling instance allocator in Wasmtime. This
98            // drastically reduces syscall/kernel overhead for wasm execution,
99            // especially in async contexts where async stacks must be allocated.
100            // The general goal here is that the default settings here rarely, if
101            // ever, need to be modified. As a result there aren't fine-grained
102            // knobs for each of these settings just yet and instead they're
103            // generally set to defaults. Environment-variable-based fallbacks are
104            // supported though as an escape valve for if this is a problem.
105            let mut pooling_config = PoolingAllocationConfig::default();
106            pooling_config
107                // Configuration parameters which affect the total size of the
108                // allocation pool as well as the maximum number of concurrently
109                // live instances at once. These can be configured individually
110                // but otherwise default to a factor-of-`max_instances` above.
111                //
112                // * Component instances are the maximum live number of
113                //   component instances or instantiations. In other words this
114                //   is the maximal concurrency that Spin can serve in terms of
115                //   HTTP requests.
116                //
117                // * Memories mostly affect how big the virtual address space
118                //   reservation is for the pooling allocator. Memories require
119                //   ~4G of virtual address space meaning that we can run out
120                //   pretty quickly.
121                //
122                // * Tables are not as costly as memories in terms of virtual
123                //   memory and mostly just need to be in the same order of
124                //   magnitude to run that many components.
125                //
126                // * Core instances do not have a virtual memory reservation at
127                //   this time, it's just a counter to cap the maximum amount of
128                //   memory allocated (multiplied by `max_core_instance_size`
129                //   below) so the limit is more liberal.
130                //
131                // * Table elements limit the maximum size of any allocated
132                //   table, so it's set generously large. This does affect
133                //   virtual memory reservation but it's just 8 bytes per table
134                //   slot.
135                .total_component_instances(env("SPIN_WASMTIME_INSTANCE_COUNT", max_instances))
136                .total_memories(env("SPIN_WASMTIME_TOTAL_MEMORIES", max_instances))
137                .total_tables(env("SPIN_WASMTIME_TOTAL_TABLES", 2 * max_instances))
138                .total_stacks(env("SPIN_WASMTIME_TOTAL_STACKS", max_instances))
139                .total_core_instances(env("SPIN_WASMTIME_TOTAL_CORE_INSTANCES", 4 * max_instances))
140                .table_elements(env("SPIN_WASMTIME_INSTANCE_TABLE_ELEMENTS", 100_000))
141                // This number accounts for internal data structures that Wasmtime allocates for each instance.
142                // Instance allocation is proportional to the number of "things" in a wasm module like functions,
143                // globals, memories, etc. Instance allocations are relatively small and are largely inconsequential
144                // compared to other runtime state, but a number needs to be chosen here so a relatively large threshold
145                // of 10MB is arbitrarily chosen. It should be unlikely that any reasonably-sized module hits this limit.
146                .max_component_instance_size(env("SPIN_WASMTIME_INSTANCE_SIZE", 10 * MB) as usize)
147                .max_core_instance_size(env("SPIN_WASMTIME_CORE_INSTANCE_SIZE", 10 * MB) as usize)
148                // Configuration knobs for hard limits per-component for various
149                // items that require allocations. Note that these are
150                // per-component limits and instantiating a component still has
151                // to fit into the `total_*` limits above at runtime.
152                //
153                // * Core instances are more or less a reflection of how many
154                //   nested components can be in a component (e.g. via
155                //   composition)
156                // * The number of memories an instance can have effectively
157                //   limits the number of inner components a composed component
158                //   can have (since each inner component has its own memory).
159                //   We default to 32 for now, and we'll see how often this
160                //   limit gets reached.
161                // * Tables here are roughly similar to memories but are set a
162                //   bit higher as it's more likely to have more tables than
163                //   memories in a component.
164                .max_core_instances_per_component(env("SPIN_WASMTIME_CORE_INSTANCE_COUNT", 200))
165                .max_tables_per_component(env("SPIN_WASMTIME_INSTANCE_TABLES", 64))
166                .max_memories_per_component(env("SPIN_WASMTIME_INSTANCE_MEMORIES", 32))
167                // Similar knobs as above, but as specified per-module instead
168                // of per-component. Note that these limits are much lower as
169                // core modules typically only have one of each.
170                .max_tables_per_module(env("SPIN_WASMTIME_MAX_TABLES_PER_MODULE", 2))
171                .max_memories_per_module(env("SPIN_WASMTIME_MAX_MEMORIES_PER_MODULE", 2))
172                // Nothing is lost from allowing the maximum size of memory for
173                // all instance as it's still limited through other the normal
174                // `StoreLimitsAsync` accounting method too.
175                .max_memory_size(4 * GB)
176                // These numbers are completely arbitrary at something above 0.
177                .linear_memory_keep_resident(env(
178                    "SPIN_WASMTIME_LINEAR_MEMORY_KEEP_RESIDENT",
179                    2 * MB,
180                ) as usize)
181                .table_keep_resident(env("SPIN_WASMTIME_TABLE_KEEP_RESIDENT", MB / 2) as usize);
182            inner.allocation_strategy(InstanceAllocationStrategy::Pooling(pooling_config));
183        }
184
185        return Self { inner };
186
187        fn env<T>(name: &str, default: T) -> T
188        where
189            T: std::str::FromStr,
190            T::Err: std::fmt::Display,
191        {
192            match std::env::var(name) {
193                Ok(val) => val
194                    .parse()
195                    .unwrap_or_else(|e| panic!("failed to parse env var `{name}={val}`: {e}")),
196                Err(_) => default,
197            }
198        }
199    }
200}
201
202/// The pooling allocator is tailor made for the `spin up` use case, so
203/// try to use it when we can. The main cost of the pooling allocator, however,
204/// is the virtual memory required to run it. Not all systems support the same
205/// amount of virtual memory, for example some aarch64 and riscv64 configuration
206/// only support 39 bits of virtual address space.
207///
208/// The pooling allocator, by default, will request 1000 linear memories each
209/// sized at 6G per linear memory. This is 6T of virtual memory which ends up
210/// being about 42 bits of the address space. This exceeds the 39 bit limit of
211/// some systems, so there the pooling allocator will fail by default.
212///
213/// This function attempts to dynamically determine the hint for the pooling
214/// allocator. This returns `true` if the pooling allocator should be used
215/// by default, or `false` otherwise.
216///
217/// The method for testing this is to allocate a 0-sized 64-bit linear memory
218/// with a maximum size that's N bits large where we force all memories to be
219/// static. This should attempt to acquire N bits of the virtual address space.
220/// If successful that should mean that the pooling allocator is OK to use, but
221/// if it fails then the pooling allocator is not used and the normal mmap-based
222/// implementation is used instead.
223fn use_pooling_allocator_by_default() -> bool {
224    static USE_POOLING: OnceLock<bool> = OnceLock::new();
225    const BITS_TO_TEST: u32 = 42;
226
227    *USE_POOLING.get_or_init(|| {
228        // Enable manual control through env vars as an escape hatch
229        match std::env::var("SPIN_WASMTIME_POOLING") {
230            Ok(s) if s == "1" => return true,
231            Ok(s) if s == "0" => return false,
232            Ok(s) => panic!("SPIN_WASMTIME_POOLING={s} not supported, only 1/0 supported"),
233            Err(_) => {}
234        }
235
236        // If the env var isn't set then perform the dynamic runtime probe
237        let mut config = wasmtime::Config::new();
238        config.wasm_memory64(true);
239        config.memory_reservation(1 << BITS_TO_TEST);
240
241        match wasmtime::Engine::new(&config) {
242            Ok(engine) => {
243                let mut store = wasmtime::Store::new(&engine, ());
244                // NB: the maximum size is in wasm pages so take out the 16-bits
245                // of wasm page size here from the maximum size.
246                let ty = wasmtime::MemoryType::new64(0, Some(1 << (BITS_TO_TEST - 16)));
247                wasmtime::Memory::new(&mut store, ty).is_ok()
248            }
249            Err(_) => {
250                tracing::debug!(
251                    "unable to create an engine to test the pooling \
252                     allocator, disabling pooling allocation"
253                );
254                false
255            }
256        }
257    })
258}
259
260/// Host state data associated with individual [Store]s and [Instance]s.
261#[derive(Default)]
262pub struct State {
263    store_limits: limits::StoreLimitsAsync,
264}
265
266impl State {
267    /// Get the amount of memory in bytes consumed by instances in the store
268    pub fn memory_consumed(&self) -> u64 {
269        self.store_limits.memory_consumed()
270    }
271}
272
273/// A builder interface for configuring a new [`Engine`].
274///
275/// A new [`EngineBuilder`] can be obtained with [`Engine::builder`].
276pub struct EngineBuilder<T: 'static> {
277    engine: wasmtime::Engine,
278    linker: Linker<T>,
279    epoch_tick_interval: Duration,
280    epoch_ticker_thread: bool,
281}
282
283impl<T: 'static> EngineBuilder<T> {
284    fn new(config: &Config) -> Result<Self> {
285        let engine = wasmtime::Engine::new(&config.inner)?;
286        let linker: Linker<T> = Linker::new(&engine);
287        Ok(Self {
288            engine,
289            linker,
290            epoch_tick_interval: DEFAULT_EPOCH_TICK_INTERVAL,
291            epoch_ticker_thread: true,
292        })
293    }
294
295    /// Returns a reference to the [`Linker`] for this [`Engine`].
296    pub fn linker(&mut self) -> &mut Linker<T> {
297        &mut self.linker
298    }
299
300    /// Sets the epoch tick internal for the built [`Engine`].
301    ///
302    /// This is used by [`Store::set_deadline`] to calculate the number of
303    /// "ticks" for epoch interruption, and by the default epoch ticker thread.
304    /// The default is [`DEFAULT_EPOCH_TICK_INTERVAL`].
305    ///
306    /// See [`EngineBuilder::epoch_ticker_thread`] and
307    /// [`wasmtime::Config::epoch_interruption`](https://docs.rs/wasmtime/latest/wasmtime/struct.Config.html#method.epoch_interruption).
308    pub fn epoch_tick_interval(&mut self, interval: Duration) {
309        self.epoch_tick_interval = interval;
310    }
311
312    /// Configures whether the epoch ticker thread will be spawned when this
313    /// [`Engine`] is built.
314    ///
315    /// Enabled by default; if disabled, the user must arrange to call
316    /// `engine.as_ref().increment_epoch()` every `epoch_tick_interval` or
317    /// interrupt-based features like `Store::set_deadline` will not work.
318    pub fn epoch_ticker_thread(&mut self, enable: bool) {
319        self.epoch_ticker_thread = enable;
320    }
321
322    fn maybe_spawn_epoch_ticker(&self) {
323        if !self.epoch_ticker_thread {
324            return;
325        }
326        let engine_weak = self.engine.weak();
327        let interval = self.epoch_tick_interval;
328        std::thread::spawn(move || {
329            loop {
330                std::thread::sleep(interval);
331                let Some(engine) = engine_weak.upgrade() else {
332                    break;
333                };
334                engine.increment_epoch();
335            }
336        });
337    }
338
339    /// Builds an [`Engine`] from this builder.
340    pub fn build(self) -> Engine<T> {
341        self.maybe_spawn_epoch_ticker();
342        Engine {
343            inner: self.engine,
344            linker: self.linker,
345            epoch_tick_interval: self.epoch_tick_interval,
346        }
347    }
348}
349
350/// An `Engine` is a global context for the initialization and execution of
351/// Spin components.
352pub struct Engine<T: 'static> {
353    inner: wasmtime::Engine,
354    linker: Linker<T>,
355    epoch_tick_interval: Duration,
356}
357
358impl<T: 'static> Engine<T> {
359    /// Creates a new [`EngineBuilder`] with the given [`Config`].
360    pub fn builder(config: &Config) -> Result<EngineBuilder<T>> {
361        EngineBuilder::new(config)
362    }
363
364    /// Creates a new [`StoreBuilder`].
365    pub fn store_builder(&self) -> StoreBuilder {
366        StoreBuilder::new(self.inner.clone(), self.epoch_tick_interval)
367    }
368
369    /// Creates a new [`InstancePre`] for the given [`Component`].
370    #[instrument(skip_all, level = "debug")]
371    pub fn instantiate_pre(&self, component: &Component) -> Result<InstancePre<T>> {
372        Ok(self.linker.instantiate_pre(component)?)
373    }
374}
375
376impl<T> AsRef<wasmtime::Engine> for Engine<T> {
377    fn as_ref(&self) -> &wasmtime::Engine {
378        &self.inner
379    }
380}