Skip to main content

wasmtime/runtime/vm/instance/allocator/
pooling.rs

1//! Implements the pooling instance allocator.
2//!
3//! The pooling instance allocator maps memory in advance and allocates
4//! instances, memories, tables, and stacks from a pool of available resources.
5//! Using the pooling instance allocator can speed up module instantiation when
6//! modules can be constrained based on configurable limits
7//! ([`InstanceLimits`]). Each new instance is stored in a "slot"; as instances
8//! are allocated and freed, these slots are either filled or emptied:
9//!
10//! ```text
11//! ┌──────┬──────┬──────┬──────┬──────┐
12//! │Slot 0│Slot 1│Slot 2│Slot 3│......│
13//! └──────┴──────┴──────┴──────┴──────┘
14//! ```
15//!
16//! Each slot has a "slot ID"--an index into the pool. Slot IDs are handed out
17//! by the [`index_allocator`] module. Note that each kind of pool-allocated
18//! item is stored in its own separate pool: [`memory_pool`], [`table_pool`],
19//! [`stack_pool`]. See those modules for more details.
20
21mod decommit_queue;
22mod index_allocator;
23mod memory_pool;
24mod metrics;
25mod table_pool;
26
27#[cfg(feature = "gc")]
28mod gc_heap_pool;
29
30#[cfg(all(feature = "async"))]
31mod generic_stack_pool;
32#[cfg(all(feature = "async", unix, not(miri)))]
33mod unix_stack_pool;
34
35#[cfg(all(feature = "async"))]
36cfg_if::cfg_if! {
37    if #[cfg(all(unix, not(miri), not(asan)))] {
38        use unix_stack_pool as stack_pool;
39    } else {
40        use generic_stack_pool as stack_pool;
41    }
42}
43
44use self::decommit_queue::DecommitQueue;
45use self::memory_pool::MemoryPool;
46pub use self::metrics::PoolingAllocatorMetrics;
47use self::table_pool::TablePool;
48use super::{
49    InstanceAllocationRequest, InstanceAllocator, MemoryAllocationIndex, TableAllocationIndex,
50};
51use crate::Enabled;
52use crate::config::PoolingAllocationConfig;
53use crate::prelude::*;
54use crate::runtime::vm::{
55    CompiledModuleId, Memory, Table,
56    instance::Instance,
57    mpk::{self, ProtectionKey, ProtectionMask},
58    sys::vm::PageMap,
59};
60use core::future::Future;
61use core::pin::Pin;
62use core::sync::atomic::AtomicUsize;
63use std::borrow::Cow;
64use std::fmt::Display;
65use std::sync::{Mutex, MutexGuard};
66use std::{
67    mem,
68    sync::atomic::{AtomicU64, Ordering},
69};
70use wasmtime_environ::{
71    DefinedMemoryIndex, DefinedTableIndex, HostPtr, MemoryKind, Module, Tunables, VMOffsets,
72};
73
74#[cfg(feature = "gc")]
75use super::GcHeapAllocationIndex;
76#[cfg(feature = "gc")]
77use crate::runtime::vm::{GcHeap, GcRuntime};
78#[cfg(feature = "gc")]
79use gc_heap_pool::GcHeapPool;
80
81/// Pad a value out to a full cache line (or two, on aarch64 prefetch
82/// granularity) so neighboring shards don't false-share.
83#[repr(align(128))]
84#[derive(Debug)]
85struct CachePadded<T>(T);
86
87/// Identifier of one shard of the pooling allocator's sharded data
88/// structures (the decommit queues and each pool's index allocator).
89#[derive(Copy, Clone, Debug, PartialEq, Eq)]
90pub(crate) struct ShardId(u32);
91
92impl ShardId {
93    pub(crate) fn from_index(index: usize) -> ShardId {
94        ShardId(u32::try_from(index).unwrap())
95    }
96
97    pub(crate) fn index(self) -> usize {
98        usize::try_from(self.0).unwrap()
99    }
100}
101
102/// The number of shards used for the pooling allocator's sharded data
103/// structures: one per available CPU, capped to 16.
104///
105/// The cap bounds worst-case probing when pools run near-full, the
106/// dilution of per-shard warm-slot budgets, and per-shard memory
107/// overhead, while still being enough shards to make lock collisions
108/// rare given the very short critical sections involved.
109pub(crate) fn default_shard_count() -> u32 {
110    let n = std::thread::available_parallelism()
111        .map(|n| n.get())
112        .unwrap_or(1)
113        .min(16);
114    u32::try_from(n).unwrap()
115}
116
117/// Pick this thread's shard (used for both the sharded decommit queue and
118/// the sharded index allocators): assigned round-robin at first use per
119/// thread, cached in a thread-local.
120pub(crate) fn thread_shard(nshards: usize) -> ShardId {
121    static NEXT_SHARD: AtomicUsize = AtomicUsize::new(0);
122    std::thread_local! {
123        static SHARD: usize = NEXT_SHARD.fetch_add(1, Ordering::Relaxed);
124    }
125    ShardId::from_index(SHARD.with(|s| *s) % nshards)
126}
127
128/// Enumerate all shard ids for a sharded structure with `nshards` shards,
129/// starting with the current thread's home shard and wrapping around.
130pub(crate) fn shard_ids_from_home(nshards: usize) -> impl Iterator<Item = ShardId> {
131    let home = thread_shard(nshards).index();
132    (0..nshards).map(move |i| ShardId::from_index((home + i) % nshards))
133}
134
135#[cfg(feature = "async")]
136use stack_pool::StackPool;
137
138#[cfg(feature = "component-model")]
139use wasmtime_environ::{
140    StaticModuleIndex,
141    component::{Component, VMComponentOffsets},
142};
143
144fn round_up_to_pow2(n: usize, to: usize) -> usize {
145    debug_assert!(to > 0);
146    debug_assert!(to.is_power_of_two());
147    (n + to - 1) & !(to - 1)
148}
149
150impl PoolingAllocationConfig {
151    /// Tests whether [`Self::pagemap_scan`] is available or not on the host
152    /// system.
153    pub fn is_pagemap_scan_available() -> bool {
154        PageMap::new().is_some()
155    }
156}
157
158/// An error returned when the pooling allocator cannot allocate a table,
159/// memory, etc... because the maximum number of concurrent allocations for that
160/// entity has been reached.
161#[derive(Debug)]
162pub struct PoolConcurrencyLimitError {
163    limit: usize,
164    kind: Cow<'static, str>,
165}
166
167impl core::error::Error for PoolConcurrencyLimitError {}
168
169impl Display for PoolConcurrencyLimitError {
170    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
171        let limit = self.limit;
172        let kind = &self.kind;
173        write!(f, "maximum concurrent limit of {limit} for {kind} reached")
174    }
175}
176
177impl PoolConcurrencyLimitError {
178    fn new(limit: usize, kind: impl Into<Cow<'static, str>>) -> Self {
179        Self {
180            limit,
181            kind: kind.into(),
182        }
183    }
184}
185
186/// Implements the pooling instance allocator.
187///
188/// This allocator internally maintains pools of instances, memories, tables,
189/// and stacks.
190///
191/// Note: the resource pools are manually dropped so that the fault handler
192/// terminates correctly.
193#[derive(Debug)]
194pub struct PoolingInstanceAllocator {
195    // The number of live core module and component instances at any given
196    // time. Note that this can temporarily go over the configured limit. This
197    // doesn't mean we have actually overshot, but that we attempted to allocate
198    // a new instance and incremented the counter, we've seen (or are about to
199    // see) that the counter is beyond the configured threshold, and are going
200    // to decrement the counter and return an error but haven't done so yet. See
201    // the increment trait methods for more details.
202    live_core_instances: AtomicU64,
203    live_component_instances: AtomicU64,
204
205    /// Sharded to avoid a single global mutex on every deallocation when
206    /// decommit batching is enabled: each thread appends to its own shard
207    /// (assigned round-robin at first use) and flushes that shard when it
208    /// reaches the configured batch size. Slot-exhaustion paths flush all
209    /// shards.
210    decommit_queues: Box<[CachePadded<Mutex<DecommitQueue>>]>,
211
212    memories: MemoryPool,
213    live_memories: AtomicUsize,
214
215    tables: TablePool,
216    live_tables: AtomicUsize,
217
218    #[cfg(feature = "gc")]
219    gc_heaps: Option<GcHeapPool>,
220    #[cfg(feature = "gc")]
221    live_gc_heaps: AtomicUsize,
222
223    #[cfg(feature = "async")]
224    stacks: StackPool,
225    #[cfg(feature = "async")]
226    live_stacks: AtomicUsize,
227
228    pagemap: Option<PageMap>,
229    config: PoolingAllocationConfig,
230}
231
232impl Drop for PoolingInstanceAllocator {
233    fn drop(&mut self) {
234        if !cfg!(debug_assertions) {
235            return;
236        }
237
238        // NB: when cfg(not(debug_assertions)) it is okay that we don't flush
239        // the queue, as the sub-pools will unmap those ranges anyways, so
240        // there's no point in decommitting them. But we do need to flush the
241        // queue when debug assertions are enabled to make sure that all
242        // entities get returned to their associated sub-pools and we can
243        // differentiate between a leaking slot and an enqueued-for-decommit
244        // slot.
245        self.flush_all_decommit_queues();
246
247        debug_assert_eq!(self.live_component_instances.load(Ordering::Acquire), 0);
248        debug_assert_eq!(self.live_core_instances.load(Ordering::Acquire), 0);
249        debug_assert_eq!(self.live_memories.load(Ordering::Acquire), 0);
250        debug_assert_eq!(self.live_tables.load(Ordering::Acquire), 0);
251
252        debug_assert!(self.memories.is_empty());
253        debug_assert!(self.tables.is_empty());
254
255        #[cfg(feature = "gc")]
256        if let Some(gc_heaps) = &self.gc_heaps {
257            debug_assert!(gc_heaps.is_empty());
258            debug_assert_eq!(self.live_gc_heaps.load(Ordering::Acquire), 0);
259        }
260
261        #[cfg(feature = "async")]
262        {
263            debug_assert!(self.stacks.is_empty());
264            debug_assert_eq!(self.live_stacks.load(Ordering::Acquire), 0);
265        }
266    }
267}
268
269impl PoolingInstanceAllocator {
270    /// Creates a new pooling instance allocator with the given strategy and limits.
271    pub fn new(config: &PoolingAllocationConfig, tunables: &Tunables) -> Result<Self> {
272        Ok(Self {
273            live_component_instances: AtomicU64::new(0),
274            live_core_instances: AtomicU64::new(0),
275            decommit_queues: (0..default_shard_count())
276                .map(|_| CachePadded(Mutex::new(DecommitQueue::default())))
277                .try_collect::<Box<[_]>, OutOfMemory>()?,
278            memories: MemoryPool::new(config, tunables)?,
279            live_memories: AtomicUsize::new(0),
280            tables: TablePool::new(config)?,
281            live_tables: AtomicUsize::new(0),
282            #[cfg(feature = "gc")]
283            gc_heaps: if tunables.collector.is_some() {
284                Some(GcHeapPool::new(config, tunables)?)
285            } else {
286                None
287            },
288            #[cfg(feature = "gc")]
289            live_gc_heaps: AtomicUsize::new(0),
290            #[cfg(feature = "async")]
291            stacks: StackPool::new(config)?,
292            #[cfg(feature = "async")]
293            live_stacks: AtomicUsize::new(0),
294            pagemap: match config.pagemap_scan {
295                Enabled::Auto => PageMap::new(),
296                Enabled::Yes => Some(PageMap::new().ok_or_else(|| {
297                    format_err!(
298                        "required to enable PAGEMAP_SCAN but this system \
299                         does not support it"
300                    )
301                })?),
302                Enabled::No => None,
303            },
304            config: config.clone(),
305        })
306    }
307
308    fn core_instance_size(&self) -> usize {
309        round_up_to_pow2(
310            self.config.limits.core_instance_size,
311            mem::align_of::<Instance>(),
312        )
313    }
314
315    fn validate_table_plans(&self, module: &Module) -> Result<()> {
316        self.tables.validate(module)
317    }
318
319    fn validate_memory_plans(&self, module: &Module) -> Result<()> {
320        self.memories.validate_memories(module)
321    }
322
323    fn validate_core_instance_size(&self, offsets: &VMOffsets<HostPtr>) -> Result<()> {
324        let layout = Instance::alloc_layout(offsets);
325        if layout.size() <= self.core_instance_size() {
326            return Ok(());
327        }
328
329        // If this `module` exceeds the allocation size allotted to it then an
330        // error will be reported here. The error of "required N bytes but
331        // cannot allocate that" is pretty opaque, however, because it's not
332        // clear what the breakdown of the N bytes are and what to optimize
333        // next. To help provide a better error message here some fancy-ish
334        // logic is done here to report the breakdown of the byte request into
335        // the largest portions and where it's coming from.
336        let mut message = format!(
337            "instance allocation for this module \
338             requires {} bytes which exceeds the configured maximum \
339             of {} bytes; breakdown of allocation requirement:\n\n",
340            layout.size(),
341            self.core_instance_size(),
342        );
343
344        let mut remaining = layout.size();
345        let mut push = |name: &str, bytes: usize| {
346            assert!(remaining >= bytes);
347            remaining -= bytes;
348
349            // If the `name` region is more than 5% of the allocation request
350            // then report it here, otherwise ignore it. We have less than 20
351            // fields so we're guaranteed that something should be reported, and
352            // otherwise it's not particularly interesting to learn about 5
353            // different fields that are all 8 or 0 bytes. Only try to report
354            // the "major" sources of bytes here.
355            if bytes > layout.size() / 20 {
356                message.push_str(&format!(
357                    " * {:.02}% - {} bytes - {}\n",
358                    ((bytes as f32) / (layout.size() as f32)) * 100.0,
359                    bytes,
360                    name,
361                ));
362            }
363        };
364
365        // The `Instance` itself requires some size allocated to it.
366        push("instance state management", mem::size_of::<Instance>());
367
368        // Afterwards the `VMContext`'s regions are why we're requesting bytes,
369        // so ask it for descriptions on each region's byte size.
370        for (desc, size) in offsets.region_sizes() {
371            push(desc, size as usize);
372        }
373
374        // double-check we accounted for all the bytes
375        assert_eq!(remaining, 0);
376
377        bail!("{message}")
378    }
379
380    #[cfg(feature = "component-model")]
381    fn validate_component_instance_size(
382        &self,
383        offsets: &VMComponentOffsets<HostPtr>,
384        core_instances_aggregate_size: usize,
385    ) -> Result<()> {
386        let vmcomponentctx_size = usize::try_from(offsets.size_of_vmctx()).unwrap();
387        let total_instance_size = core_instances_aggregate_size.saturating_add(vmcomponentctx_size);
388        if total_instance_size <= self.config.limits.component_instance_size {
389            return Ok(());
390        }
391
392        // TODO: Add context with detailed accounting of what makes up all the
393        // `VMComponentContext`'s space like we do for module instances.
394        bail!(
395            "instance allocation for this component requires {total_instance_size} bytes of `VMComponentContext` \
396             and aggregated core instance runtime space which exceeds the configured maximum of {} bytes. \
397             `VMComponentContext` used {vmcomponentctx_size} bytes, `core module instances` used \
398             {core_instances_aggregate_size} bytes.",
399            self.config.limits.component_instance_size
400        )
401    }
402
403    /// Returns the decommit-queue shard for `shard`.
404    fn decommit_queue(&self, shard: ShardId) -> &Mutex<DecommitQueue> {
405        &self.decommit_queues[shard.index()].0
406    }
407
408    /// Enumerate all decommit-queue shard ids, starting with the current
409    /// thread's home shard.
410    fn decommit_shard_ids(&self) -> impl Iterator<Item = ShardId> {
411        shard_ids_from_home(self.decommit_queues.len())
412    }
413
414    fn flush_decommit_queue(&self, mut locked_queue: MutexGuard<'_, DecommitQueue>) -> bool {
415        // Take the queue out of the mutex and drop the lock, to minimize
416        // contention.
417        let queue = mem::take(&mut *locked_queue);
418        drop(locked_queue);
419        queue.flush(self)
420    }
421
422    /// Flush every shard of the decommit queue, e.g. on allocator drop.
423    /// Returns whether any slot was returned to any pool.
424    fn flush_all_decommit_queues(&self) -> bool {
425        let mut any = false;
426        for shard in self.decommit_shard_ids() {
427            let queue = self.decommit_queue(shard).lock().unwrap();
428            any |= self.flush_decommit_queue(queue);
429        }
430        any
431    }
432
433    /// Execute `f` and if it returns `Err(PoolConcurrencyLimitError)`, then try
434    /// flushing the decommit queue. If flushing the queue freed up slots, then
435    /// try running `f` again.
436    ///
437    /// Queue shards are flushed one at a time, retrying `f` after each flush
438    /// that returned slots to a pool, rather than eagerly flushing all
439    /// shards: one flushed shard is often enough to satisfy the allocation,
440    /// and this avoids acquiring every shard's lock (at the cost of raising
441    /// the chances that another thread steals the freshly-flushed slots
442    /// before we get a chance to grab one, in which case we keep flushing).
443    ///
444    /// Note that [`Self::flush_decommit_queue`] takes the shard's queue out
445    /// of its mutex and drops the lock immediately, so no queue lock is held
446    /// while decommitting or while `f` runs.
447    #[cfg(feature = "async")]
448    fn with_flush_and_retry<T>(&self, mut f: impl FnMut() -> Result<T>) -> Result<T> {
449        let mut result = f();
450        for shard in self.decommit_shard_ids() {
451            match &result {
452                Err(e) if e.is::<PoolConcurrencyLimitError>() => {}
453                _ => break,
454            }
455            let queue = self.decommit_queue(shard).lock().unwrap();
456            if self.flush_decommit_queue(queue) {
457                result = f();
458            }
459        }
460        result
461    }
462
463    fn merge_or_flush(&self, mut local_queue: DecommitQueue) {
464        match local_queue.raw_len() {
465            // If we didn't enqueue any regions for decommit, then we must have
466            // either memset the whole entity or eagerly remapped it to zero
467            // because we don't have linux's `madvise(DONTNEED)` semantics. In
468            // either case, the entity slot is ready for reuse immediately.
469            0 => {
470                local_queue.flush(self);
471            }
472
473            // We enqueued at least our batch size of regions for decommit, so
474            // flush the local queue immediately. Don't bother inspecting (or
475            // locking!) the shared queue.
476            n if n >= self.config.decommit_batch_size => {
477                local_queue.flush(self);
478            }
479
480            // If we enqueued some regions for decommit, but did not reach our
481            // batch size, so we don't want to flush it yet, then merge the
482            // local queue into this thread's shard of the shared queue.
483            n => {
484                debug_assert!(n < self.config.decommit_batch_size);
485                let shard = thread_shard(self.decommit_queues.len());
486                let mut shared_queue = self.decommit_queue(shard).lock().unwrap();
487                shared_queue.append(&mut local_queue);
488                // And if this shard now has at least as many regions enqueued
489                // for decommit as our batch size, then we can flush it.
490                if shared_queue.raw_len() >= self.config.decommit_batch_size {
491                    self.flush_decommit_queue(shared_queue);
492                }
493            }
494        }
495    }
496
497    pub fn config(&self) -> &PoolingAllocationConfig {
498        &self.config
499    }
500}
501
502unsafe impl InstanceAllocator for PoolingInstanceAllocator {
503    #[cfg(feature = "component-model")]
504    fn validate_component<'a>(
505        &self,
506        component: &Component,
507        offsets: &VMComponentOffsets<HostPtr>,
508        get_module: &'a dyn Fn(StaticModuleIndex) -> &'a Module,
509    ) -> Result<()> {
510        let mut num_core_instances = 0;
511        let mut num_memories = 0;
512        let mut num_tables = 0;
513        let mut core_instances_aggregate_size: usize = 0;
514        for init in &component.initializers {
515            use wasmtime_environ::component::GlobalInitializer::*;
516            use wasmtime_environ::component::InstantiateModule;
517            match init {
518                InstantiateModule(InstantiateModule::Import(_, _), _) => {
519                    num_core_instances += 1;
520                    // Can't statically account for the total vmctx size, number
521                    // of memories, and number of tables in this component.
522                }
523                InstantiateModule(InstantiateModule::Static(static_module_index, _), _) => {
524                    let module = get_module(*static_module_index);
525                    let offsets = VMOffsets::new(HostPtr, &module);
526                    let layout = Instance::alloc_layout(&offsets);
527                    self.validate_module(module, &offsets)?;
528                    num_core_instances += 1;
529                    num_memories += module.num_defined_memories();
530                    num_tables += module.num_defined_tables();
531                    core_instances_aggregate_size += layout.size();
532                }
533                LowerImport { .. }
534                | ExtractMemory(_)
535                | ExtractTable(_)
536                | ExtractRealloc(_)
537                | ExtractCallback(_)
538                | ExtractPostReturn(_)
539                | Resource(_) => {}
540            }
541        }
542
543        if num_core_instances
544            > usize::try_from(self.config.limits.max_core_instances_per_component).unwrap()
545        {
546            bail!(
547                "The component transitively contains {num_core_instances} core module instances, \
548                 which exceeds the configured maximum of {} in the pooling allocator",
549                self.config.limits.max_core_instances_per_component
550            );
551        }
552
553        if num_memories > usize::try_from(self.config.limits.max_memories_per_component).unwrap() {
554            bail!(
555                "The component transitively contains {num_memories} Wasm linear memories, which \
556                 exceeds the configured maximum of {} in the pooling allocator",
557                self.config.limits.max_memories_per_component
558            );
559        }
560
561        if num_tables > usize::try_from(self.config.limits.max_tables_per_component).unwrap() {
562            bail!(
563                "The component transitively contains {num_tables} tables, which exceeds the \
564                 configured maximum of {} in the pooling allocator",
565                self.config.limits.max_tables_per_component
566            );
567        }
568
569        self.validate_component_instance_size(offsets, core_instances_aggregate_size)
570            .context("component instance size does not fit in pooling allocator requirements")?;
571
572        Ok(())
573    }
574
575    fn validate_module(&self, module: &Module, offsets: &VMOffsets<HostPtr>) -> Result<()> {
576        self.validate_memory_plans(module)
577            .context("module memory does not fit in pooling allocator requirements")?;
578        self.validate_table_plans(module)
579            .context("module table does not fit in pooling allocator requirements")?;
580        self.validate_core_instance_size(offsets)
581            .context("module instance size does not fit in pooling allocator requirements")?;
582        Ok(())
583    }
584
585    #[cfg(feature = "gc")]
586    fn validate_memory(&self, memory: &wasmtime_environ::Memory) -> Result<()> {
587        self.memories.validate_memory(memory)
588    }
589
590    #[cfg(feature = "component-model")]
591    fn increment_component_instance_count(&self) -> Result<()> {
592        let old_count = self.live_component_instances.fetch_add(1, Ordering::AcqRel);
593        if old_count >= u64::from(self.config.limits.total_component_instances) {
594            self.decrement_component_instance_count();
595            return Err(PoolConcurrencyLimitError::new(
596                usize::try_from(self.config.limits.total_component_instances).unwrap(),
597                "component instances",
598            )
599            .into());
600        }
601        Ok(())
602    }
603
604    #[cfg(feature = "component-model")]
605    fn decrement_component_instance_count(&self) {
606        self.live_component_instances.fetch_sub(1, Ordering::AcqRel);
607    }
608
609    fn increment_core_instance_count(&self) -> Result<()> {
610        let old_count = self.live_core_instances.fetch_add(1, Ordering::AcqRel);
611        if old_count >= u64::from(self.config.limits.total_core_instances) {
612            self.decrement_core_instance_count();
613            return Err(PoolConcurrencyLimitError::new(
614                usize::try_from(self.config.limits.total_core_instances).unwrap(),
615                "core instances",
616            )
617            .into());
618        }
619        Ok(())
620    }
621
622    fn decrement_core_instance_count(&self) {
623        self.live_core_instances.fetch_sub(1, Ordering::AcqRel);
624    }
625
626    fn allocate_memory<'a, 'b: 'a, 'c: 'a>(
627        &'a self,
628        request: &'a mut InstanceAllocationRequest<'b, 'c>,
629        ty: &'a wasmtime_environ::Memory,
630        memory_index: Option<DefinedMemoryIndex>,
631        _memory_kind: MemoryKind,
632    ) -> Pin<Box<dyn Future<Output = Result<(MemoryAllocationIndex, Memory)>> + Send + 'a>> {
633        crate::runtime::box_future(async move {
634            async {
635                // FIXME(rust-lang/rust#145127) this should ideally use a version of
636                // `with_flush_and_retry` but adapted for async closures instead of only
637                // sync closures. Right now that won't compile though so this is the
638                // manually expanded version of the method.
639                let mut e = match self.memories.allocate(request, ty, memory_index).await {
640                    Ok(result) => return Ok(result),
641                    Err(e) => e,
642                };
643
644                for shard in self.decommit_shard_ids() {
645                    if !e.is::<PoolConcurrencyLimitError>() {
646                        break;
647                    }
648                    let queue = self.decommit_queue(shard).lock().unwrap();
649                    if self.flush_decommit_queue(queue) {
650                        match self.memories.allocate(request, ty, memory_index).await {
651                            Ok(result) => return Ok(result),
652                            Err(err) => e = err,
653                        }
654                    }
655                }
656
657                Err(e)
658            }
659            .await
660            .inspect(|_| {
661                self.live_memories.fetch_add(1, Ordering::Relaxed);
662            })
663        })
664    }
665
666    unsafe fn deallocate_memory(
667        &self,
668        _memory_index: Option<DefinedMemoryIndex>,
669        allocation_index: MemoryAllocationIndex,
670        memory: Memory,
671    ) {
672        let prev = self.live_memories.fetch_sub(1, Ordering::Relaxed);
673        debug_assert!(prev > 0);
674
675        // Reset the image slot. Depending on whether this is successful or not
676        // the `image` is preserved for future use. On success it's queued up to
677        // get deallocated later, and on failure the slot is deallocated
678        // immediately without preserving the image.
679        let mut image = memory.unwrap_static_image();
680        let mut queue = DecommitQueue::default();
681        let bytes_resident = image.clear_and_remain_ready(
682            self.pagemap.as_ref(),
683            self.memories.keep_resident,
684            |ptr, len| {
685                // SAFETY: the memory in `image` won't be used until this
686                // decommit queue is flushed, and by definition the memory is
687                // not in use when calling this function.
688                unsafe {
689                    queue.push_raw(ptr, len);
690                }
691            },
692        );
693
694        match bytes_resident {
695            Ok(bytes_resident) => {
696                // SAFETY: this image is not in use and its memory regions were enqueued
697                // with `push_raw` above.
698                unsafe {
699                    queue.push_memory(allocation_index, image, bytes_resident);
700                }
701                self.merge_or_flush(queue);
702            }
703            Err(e) => {
704                log::warn!("ignoring clear_and_remain_ready error {e}");
705                // SAFETY: `allocation_index` comes from this pool, as an unsafe
706                // contract of this function itself, and it's guaranteed to be no
707                // longer in use so safe to deallocate. The slot couldn't be
708                // preserved so it's dropped here.
709                //
710                // Note that at this point it's not clear how many bytes are
711                // resident in memory, so it's inevitably going to leave statistics
712                // a little off. Also note though that non-Linux platforms don't
713                // keep track of resident bytes anyway, and this path is only
714                // reachable on non-Linux platforms because Linux can't return an
715                // error.
716                unsafe {
717                    self.memories.deallocate(allocation_index, None, 0);
718                }
719            }
720        }
721    }
722
723    fn allocate_table<'a, 'b: 'a, 'c: 'a>(
724        &'a self,
725        request: &'a mut InstanceAllocationRequest<'b, 'c>,
726        ty: &'a wasmtime_environ::Table,
727        _table_index: DefinedTableIndex,
728    ) -> Pin<Box<dyn Future<Output = Result<(super::TableAllocationIndex, Table)>> + Send + 'a>>
729    {
730        crate::runtime::box_future(async move {
731            async {
732                // FIXME: see `allocate_memory` above for comments about duplication
733                // with `with_flush_and_retry`.
734                let mut e = match self.tables.allocate(request, ty).await {
735                    Ok(result) => return Ok(result),
736                    Err(e) => e,
737                };
738
739                for shard in self.decommit_shard_ids() {
740                    if !e.is::<PoolConcurrencyLimitError>() {
741                        break;
742                    }
743                    let queue = self.decommit_queue(shard).lock().unwrap();
744                    if self.flush_decommit_queue(queue) {
745                        match self.tables.allocate(request, ty).await {
746                            Ok(result) => return Ok(result),
747                            Err(err) => e = err,
748                        }
749                    }
750                }
751
752                Err(e)
753            }
754            .await
755            .inspect(|_| {
756                self.live_tables.fetch_add(1, Ordering::Relaxed);
757            })
758        })
759    }
760
761    unsafe fn deallocate_table(
762        &self,
763        _table_index: DefinedTableIndex,
764        allocation_index: TableAllocationIndex,
765        mut table: Table,
766    ) {
767        let prev = self.live_tables.fetch_sub(1, Ordering::Relaxed);
768        debug_assert!(prev > 0);
769
770        let mut queue = DecommitQueue::default();
771        // SAFETY: This table is no longer in use by the allocator when this
772        // method is called and additionally all image ranges are pushed with
773        // the understanding that the memory won't get used until the whole
774        // queue is flushed.
775        let bytes_resident = unsafe {
776            self.tables.reset_table_pages_to_zero(
777                self.pagemap.as_ref(),
778                allocation_index,
779                &mut table,
780                |ptr, len| {
781                    queue.push_raw(ptr, len);
782                },
783            )
784        };
785
786        // SAFETY: the table has had all its memory regions enqueued above.
787        unsafe {
788            queue.push_table(allocation_index, table, bytes_resident);
789        }
790        self.merge_or_flush(queue);
791    }
792
793    #[cfg(feature = "async")]
794    fn allocate_fiber_stack(&self) -> Result<wasmtime_fiber::FiberStack> {
795        let ret = self.with_flush_and_retry(|| self.stacks.allocate())?;
796        self.live_stacks.fetch_add(1, Ordering::Relaxed);
797        Ok(ret)
798    }
799
800    #[cfg(feature = "async")]
801    unsafe fn deallocate_fiber_stack(&self, mut stack: wasmtime_fiber::FiberStack) {
802        self.live_stacks.fetch_sub(1, Ordering::Relaxed);
803        let mut queue = DecommitQueue::default();
804        // SAFETY: the stack is no longer in use by definition when this
805        // function is called and memory ranges pushed here are otherwise no
806        // longer in use.
807        let bytes_resident = unsafe {
808            self.stacks
809                .zero_stack(&mut stack, |ptr, len| queue.push_raw(ptr, len))
810        };
811        // SAFETY: this stack's memory regions were enqueued above.
812        unsafe {
813            queue.push_stack(stack, bytes_resident);
814        }
815        self.merge_or_flush(queue);
816    }
817
818    fn purge_module(&self, module: CompiledModuleId) {
819        self.memories.purge_module(module);
820    }
821
822    fn next_available_pkey(&self) -> Option<ProtectionKey> {
823        self.memories.next_available_pkey()
824    }
825
826    fn restrict_to_pkey(&self, pkey: ProtectionKey) {
827        mpk::allow(ProtectionMask::zero().or(pkey));
828    }
829
830    fn allow_all_pkeys(&self) {
831        mpk::allow(ProtectionMask::all());
832    }
833
834    #[cfg(feature = "gc")]
835    fn allocate_gc_heap(
836        &self,
837        engine: &crate::Engine,
838        gc_runtime: &dyn GcRuntime,
839        memory_alloc_index: MemoryAllocationIndex,
840    ) -> Result<(GcHeapAllocationIndex, Box<dyn GcHeap>)> {
841        let ret =
842            self.gc_heaps
843                .as_ref()
844                .unwrap()
845                .allocate(engine, gc_runtime, memory_alloc_index)?;
846        self.live_gc_heaps.fetch_add(1, Ordering::Relaxed);
847        Ok(ret)
848    }
849
850    #[cfg(feature = "gc")]
851    fn deallocate_gc_heap(
852        &self,
853        allocation_index: GcHeapAllocationIndex,
854        gc_heap: Box<dyn GcHeap>,
855    ) -> MemoryAllocationIndex {
856        let gc_heaps = self.gc_heaps.as_ref().unwrap();
857        self.live_gc_heaps.fetch_sub(1, Ordering::Relaxed);
858        gc_heaps.deallocate(allocation_index, gc_heap)
859    }
860
861    fn as_pooling(&self) -> Option<&PoolingInstanceAllocator> {
862        Some(self)
863    }
864}
865
866#[cfg(test)]
867#[cfg(target_pointer_width = "64")]
868mod test {
869    use super::*;
870    use crate::config::InstanceLimits;
871
872    #[test]
873    fn test_pooling_allocator_with_memory_pages_exceeded() {
874        let config = PoolingAllocationConfig {
875            limits: InstanceLimits {
876                total_memories: 1,
877                max_memory_size: 0x100010000,
878                ..Default::default()
879            },
880            ..PoolingAllocationConfig::default()
881        };
882        assert_eq!(
883            PoolingInstanceAllocator::new(
884                &config,
885                &Tunables {
886                    memory_reservation: 0x10000,
887                    ..Tunables::default_host()
888                },
889            )
890            .map_err(|e| e.to_string())
891            .expect_err("expected a failure constructing instance allocator"),
892            "maximum memory size of 0x100010000 bytes exceeds the configured \
893             memory reservation of 0x10000 bytes"
894        );
895    }
896
897    #[cfg(all(
898        unix,
899        target_pointer_width = "64",
900        feature = "async",
901        not(miri),
902        not(asan)
903    ))]
904    #[test]
905    fn test_stack_zeroed() -> Result<()> {
906        let config = PoolingAllocationConfig {
907            max_unused_warm_slots: 0,
908            limits: InstanceLimits {
909                total_stacks: 1,
910                total_memories: 0,
911                total_tables: 0,
912                ..Default::default()
913            },
914            stack_size: 128,
915            async_stack_zeroing: true,
916            ..PoolingAllocationConfig::default()
917        };
918        let allocator = PoolingInstanceAllocator::new(&config, &Tunables::default_host())?;
919
920        unsafe {
921            for _ in 0..255 {
922                let stack = allocator.allocate_fiber_stack()?;
923
924                // The stack pointer is at the top, so decrement it first
925                let addr = stack.top().unwrap().sub(1);
926
927                assert_eq!(*addr, 0);
928                *addr = 1;
929
930                allocator.deallocate_fiber_stack(stack);
931            }
932        }
933
934        Ok(())
935    }
936
937    #[cfg(all(
938        unix,
939        target_pointer_width = "64",
940        feature = "async",
941        not(miri),
942        not(asan)
943    ))]
944    #[test]
945    fn test_stack_unzeroed() -> Result<()> {
946        let config = PoolingAllocationConfig {
947            max_unused_warm_slots: 0,
948            limits: InstanceLimits {
949                total_stacks: 1,
950                total_memories: 0,
951                total_tables: 0,
952                ..Default::default()
953            },
954            stack_size: 128,
955            async_stack_zeroing: false,
956            ..PoolingAllocationConfig::default()
957        };
958        let allocator = PoolingInstanceAllocator::new(&config, &Tunables::default_host())?;
959
960        unsafe {
961            for i in 0..255 {
962                let stack = allocator.allocate_fiber_stack()?;
963
964                // The stack pointer is at the top, so decrement it first
965                let addr = stack.top().unwrap().sub(1);
966
967                assert_eq!(*addr, i);
968                *addr = i + 1;
969
970                allocator.deallocate_fiber_stack(stack);
971            }
972        }
973
974        Ok(())
975    }
976}