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}