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