Reimplement the pooling instance allocation strategy (#5661)

* Reimplement the pooling instance allocation strategy

This commit is a reimplementation of the strategy by which the pooling
instance allocator selects a slot for a module. Previously there was a
choice amongst three different algorithms: "reuse affinity", "next
available", and "random". The default was "reuse affinity" but some new
data has come to light which shows that this may not always be a good
default.

Notably the pooling allocator will retain some memory per-slot in the
pooling instance allocator, for example instance data or memory data
if-so-configured. This means that a currently unused, but previously
used, slot can contribute to the RSS usage of a program using Wasmtime.
Consequently the RSS impact here is O(max slots) which can be
counter-intuitive for embedders. This particularly affects "reuse
affinity" because the algorithm for picking a slot when there are no
affine slots is "pick a random slot", which means eventually all slots
will get used.

In discussions about possible ways to tackle this, an alternative to
"pick a strategy" arose and is now implemented in this commit.
Concretely the new allocation algorithm for a slot is now:

* First pick the most recently used affine slot, if one exists.
* Otherwise if the number of affine slots to other modules is above some
  threshold N then pick the least-recently used affine slot.
* Otherwise pick a slot that's affine to nothing.

The "N" in this algorithm is configurable and setting it to 0 is the
same as the old "next available" strategy while setting it to infinity
is the same as the "reuse affinity" algorithm. Setting it to something
in the middle provides a knob to allow a modest "cache" of affine slots
while not allowing the total set of slots used to grow too much beyond
the maximal concurrent set of modules. The "random" strategy is now no
longer possible and was removed to help simplify the allocator.

* Resolve rustdoc warnings in `wasmtime-runtime` crate

* Remove `max_cold` as it duplicates the `slot_state.len()`

* More descriptive names

* Add a comment and debug assertion

* Add some list assertions
This commit is contained in:
Alex Crichton
2023-02-01 11:43:51 -06:00
committed by GitHub
parent cb3b6c621f
commit 8ffbb9cfd7
7 changed files with 444 additions and 440 deletions

View File

@@ -6,7 +6,7 @@ use arbitrary::{Arbitrary, Unstructured};
#[derive(Debug, Clone, Eq, PartialEq, Hash)] #[derive(Debug, Clone, Eq, PartialEq, Hash)]
#[allow(missing_docs)] #[allow(missing_docs)]
pub struct PoolingAllocationConfig { pub struct PoolingAllocationConfig {
pub strategy: PoolingAllocationStrategy, pub max_unused_warm_slots: u32,
pub instance_count: u32, pub instance_count: u32,
pub instance_memories: u32, pub instance_memories: u32,
pub instance_tables: u32, pub instance_tables: u32,
@@ -24,7 +24,7 @@ impl PoolingAllocationConfig {
pub fn to_wasmtime(&self) -> wasmtime::PoolingAllocationConfig { pub fn to_wasmtime(&self) -> wasmtime::PoolingAllocationConfig {
let mut cfg = wasmtime::PoolingAllocationConfig::default(); let mut cfg = wasmtime::PoolingAllocationConfig::default();
cfg.strategy(self.strategy.to_wasmtime()) cfg.max_unused_warm_slots(self.max_unused_warm_slots)
.instance_count(self.instance_count) .instance_count(self.instance_count)
.instance_memories(self.instance_memories) .instance_memories(self.instance_memories)
.instance_tables(self.instance_tables) .instance_tables(self.instance_tables)
@@ -48,13 +48,15 @@ impl<'a> Arbitrary<'a> for PoolingAllocationConfig {
const MAX_MEMORY_PAGES: u64 = 160; // 10 MiB const MAX_MEMORY_PAGES: u64 = 160; // 10 MiB
const MAX_SIZE: usize = 1 << 20; // 1 MiB const MAX_SIZE: usize = 1 << 20; // 1 MiB
let instance_count = u.int_in_range(1..=MAX_COUNT)?;
Ok(Self { Ok(Self {
strategy: u.arbitrary()?, max_unused_warm_slots: u.int_in_range(0..=instance_count + 10)?,
instance_tables: u.int_in_range(0..=MAX_TABLES)?, instance_tables: u.int_in_range(0..=MAX_TABLES)?,
instance_memories: u.int_in_range(0..=MAX_MEMORIES)?, instance_memories: u.int_in_range(0..=MAX_MEMORIES)?,
instance_table_elements: u.int_in_range(0..=MAX_ELEMENTS)?, instance_table_elements: u.int_in_range(0..=MAX_ELEMENTS)?,
instance_memory_pages: u.int_in_range(0..=MAX_MEMORY_PAGES)?, instance_memory_pages: u.int_in_range(0..=MAX_MEMORY_PAGES)?,
instance_count: u.int_in_range(1..=MAX_COUNT)?, instance_count,
instance_size: u.int_in_range(0..=MAX_SIZE)?, instance_size: u.int_in_range(0..=MAX_SIZE)?,
async_stack_zeroing: u.arbitrary()?, async_stack_zeroing: u.arbitrary()?,
async_stack_keep_resident: u.int_in_range(0..=1 << 20)?, async_stack_keep_resident: u.int_in_range(0..=1 << 20)?,
@@ -63,28 +65,3 @@ impl<'a> Arbitrary<'a> for PoolingAllocationConfig {
}) })
} }
} }
/// Configuration for `wasmtime::PoolingAllocationStrategy`.
#[derive(Arbitrary, Clone, Debug, PartialEq, Eq, Hash)]
pub enum PoolingAllocationStrategy {
/// Use next available instance slot.
NextAvailable,
/// Use random instance slot.
Random,
/// Use an affinity-based strategy.
ReuseAffinity,
}
impl PoolingAllocationStrategy {
fn to_wasmtime(&self) -> wasmtime::PoolingAllocationStrategy {
match self {
PoolingAllocationStrategy::NextAvailable => {
wasmtime::PoolingAllocationStrategy::NextAvailable
}
PoolingAllocationStrategy::Random => wasmtime::PoolingAllocationStrategy::Random,
PoolingAllocationStrategy::ReuseAffinity => {
wasmtime::PoolingAllocationStrategy::ReuseAffinity
}
}
}
}

View File

@@ -305,7 +305,7 @@ impl ModuleMemoryImages {
/// middle of it. Pictorially this data structure manages a virtual memory /// middle of it. Pictorially this data structure manages a virtual memory
/// region that looks like: /// region that looks like:
/// ///
/// ```ignore /// ```text
/// +--------------------+-------------------+--------------+--------------+ /// +--------------------+-------------------+--------------+--------------+
/// | anonymous | optional | anonymous | PROT_NONE | /// | anonymous | optional | anonymous | PROT_NONE |
/// | zero | memory | zero | memory | /// | zero | memory | zero | memory |
@@ -333,7 +333,7 @@ impl ModuleMemoryImages {
/// `accessible` limits are. Initially there is assumed to be no image in linear /// `accessible` limits are. Initially there is assumed to be no image in linear
/// memory. /// memory.
/// ///
/// When [`MemoryImageSlot::instantiate`] is called then the method will perform /// When `MemoryImageSlot::instantiate` is called then the method will perform
/// a "synchronization" to take the image from its prior state to the new state /// a "synchronization" to take the image from its prior state to the new state
/// for the image specified. The first instantiation for example will mmap the /// for the image specified. The first instantiation for example will mmap the
/// heap image into place. Upon reuse of a slot nothing happens except possibly /// heap image into place. Upon reuse of a slot nothing happens except possibly
@@ -343,7 +343,7 @@ impl ModuleMemoryImages {
/// A `MemoryImageSlot` is either `dirty` or it isn't. When a `MemoryImageSlot` /// A `MemoryImageSlot` is either `dirty` or it isn't. When a `MemoryImageSlot`
/// is dirty then it is assumed that any memory beneath `self.accessible` could /// is dirty then it is assumed that any memory beneath `self.accessible` could
/// have any value. Instantiation cannot happen into a `dirty` slot, however, so /// have any value. Instantiation cannot happen into a `dirty` slot, however, so
/// the [`MemoryImageSlot::clear_and_remain_ready`] returns this memory back to /// the `MemoryImageSlot::clear_and_remain_ready` returns this memory back to
/// its original state to mark `dirty = false`. This is done by resetting all /// its original state to mark `dirty = false`. This is done by resetting all
/// anonymous memory back to zero and the image itself back to its initial /// anonymous memory back to zero and the image itself back to its initial
/// contents. /// contents.

View File

@@ -19,10 +19,7 @@ use wasmtime_environ::{
mod pooling; mod pooling;
#[cfg(feature = "pooling-allocator")] #[cfg(feature = "pooling-allocator")]
pub use self::pooling::{ pub use self::pooling::{InstanceLimits, PoolingInstanceAllocator, PoolingInstanceAllocatorConfig};
InstanceLimits, PoolingAllocationStrategy, PoolingInstanceAllocator,
PoolingInstanceAllocatorConfig,
};
/// Represents a request for a new runtime instance. /// Represents a request for a new runtime instance.
pub struct InstanceAllocationRequest<'a> { pub struct InstanceAllocationRequest<'a> {

View File

@@ -83,25 +83,6 @@ impl Default for InstanceLimits {
} }
} }
/// The allocation strategy to use for the pooling instance allocator.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PoolingAllocationStrategy {
/// Allocate from the next available instance.
NextAvailable,
/// Allocate from a random available instance.
Random,
/// Try to allocate an instance slot that was previously used for
/// the same module, potentially enabling faster instantiation by
/// reusing e.g. memory mappings.
ReuseAffinity,
}
impl Default for PoolingAllocationStrategy {
fn default() -> Self {
Self::ReuseAffinity
}
}
/// Represents a pool of maximal `Instance` structures. /// Represents a pool of maximal `Instance` structures.
/// ///
/// Each index in the pool provides enough space for a maximal `Instance` /// Each index in the pool provides enough space for a maximal `Instance`
@@ -142,7 +123,7 @@ impl InstancePool {
mapping, mapping,
instance_size, instance_size,
max_instances, max_instances,
index_allocator: IndexAllocator::new(config.strategy, max_instances), index_allocator: IndexAllocator::new(config.limits.count, config.max_unused_warm_slots),
memories: MemoryPool::new(&config.limits, tunables)?, memories: MemoryPool::new(&config.limits, tunables)?,
tables: TablePool::new(&config.limits)?, tables: TablePool::new(&config.limits)?,
linear_memory_keep_resident: config.linear_memory_keep_resident, linear_memory_keep_resident: config.linear_memory_keep_resident,
@@ -248,7 +229,7 @@ impl InstancePool {
// touched again until we write a fresh Instance in-place with // touched again until we write a fresh Instance in-place with
// std::ptr::write in allocate() above. // std::ptr::write in allocate() above.
self.index_allocator.free(SlotId(index)); self.index_allocator.free(SlotId(index as u32));
} }
fn allocate_instance_resources( fn allocate_instance_resources(
@@ -546,7 +527,7 @@ impl InstancePool {
// any sort of infinite loop since this should be the final operation // any sort of infinite loop since this should be the final operation
// working with `module`. // working with `module`.
while let Some(index) = self.index_allocator.alloc_affine_and_clear_affinity(module) { while let Some(index) = self.index_allocator.alloc_affine_and_clear_affinity(module) {
self.memories.clear_images(index.0); self.memories.clear_images(index.index());
self.index_allocator.free(index); self.index_allocator.free(index);
} }
} }
@@ -892,15 +873,10 @@ impl StackPool {
page_size, page_size,
async_stack_zeroing: config.async_stack_zeroing, async_stack_zeroing: config.async_stack_zeroing,
async_stack_keep_resident: config.async_stack_keep_resident, async_stack_keep_resident: config.async_stack_keep_resident,
// We always use a `NextAvailable` strategy for stack // Note that `max_unused_warm_slots` is set to zero since stacks
// allocation. We don't want or need an affinity policy // have no affinity so there's no need to keep intentionally unused
// here: stacks do not benefit from being allocated to the // warm slots around.
// same compiled module with the same image (they always index_allocator: IndexAllocator::new(config.limits.count, 0),
// start zeroed just the same for everyone).
index_allocator: IndexAllocator::new(
PoolingAllocationStrategy::NextAvailable,
max_instances,
),
}) })
} }
@@ -965,7 +941,7 @@ impl StackPool {
self.zero_stack(bottom_of_stack, stack_size); self.zero_stack(bottom_of_stack, stack_size);
} }
self.index_allocator.free(SlotId(index)); self.index_allocator.free(SlotId(index as u32));
} }
fn zero_stack(&self, bottom: usize, size: usize) { fn zero_stack(&self, bottom: usize, size: usize) {
@@ -994,9 +970,8 @@ impl StackPool {
/// construction. /// construction.
#[derive(Copy, Clone, Debug)] #[derive(Copy, Clone, Debug)]
pub struct PoolingInstanceAllocatorConfig { pub struct PoolingInstanceAllocatorConfig {
/// Allocation strategy to use for slot indexes in the pooling instance /// See `PoolingAllocatorConfig::max_unused_warm_slots` in `wasmtime`
/// allocator. pub max_unused_warm_slots: u32,
pub strategy: PoolingAllocationStrategy,
/// The size, in bytes, of async stacks to allocate (not including the guard /// The size, in bytes, of async stacks to allocate (not including the guard
/// page). /// page).
pub stack_size: usize, pub stack_size: usize,
@@ -1025,7 +1000,7 @@ pub struct PoolingInstanceAllocatorConfig {
impl Default for PoolingInstanceAllocatorConfig { impl Default for PoolingInstanceAllocatorConfig {
fn default() -> PoolingInstanceAllocatorConfig { fn default() -> PoolingInstanceAllocatorConfig {
PoolingInstanceAllocatorConfig { PoolingInstanceAllocatorConfig {
strategy: Default::default(), max_unused_warm_slots: 100,
stack_size: 2 << 20, stack_size: 2 << 20,
limits: InstanceLimits::default(), limits: InstanceLimits::default(),
async_stack_zeroing: false, async_stack_zeroing: false,
@@ -1177,7 +1152,7 @@ mod test {
#[test] #[test]
fn test_instance_pool() -> Result<()> { fn test_instance_pool() -> Result<()> {
let mut config = PoolingInstanceAllocatorConfig::default(); let mut config = PoolingInstanceAllocatorConfig::default();
config.strategy = PoolingAllocationStrategy::NextAvailable; config.max_unused_warm_slots = 0;
config.limits = InstanceLimits { config.limits = InstanceLimits {
count: 3, count: 3,
tables: 1, tables: 1,
@@ -1199,10 +1174,7 @@ mod test {
assert_eq!(instances.instance_size, 1008); // round 1000 up to alignment assert_eq!(instances.instance_size, 1008); // round 1000 up to alignment
assert_eq!(instances.max_instances, 3); assert_eq!(instances.max_instances, 3);
assert_eq!( assert_eq!(instances.index_allocator.testing_freelist(), []);
instances.index_allocator.testing_freelist(),
[SlotId(0), SlotId(1), SlotId(2)]
);
let mut handles = Vec::new(); let mut handles = Vec::new();
let module = Arc::new(Module::default()); let module = Arc::new(Module::default());
@@ -1248,7 +1220,7 @@ mod test {
assert_eq!( assert_eq!(
instances.index_allocator.testing_freelist(), instances.index_allocator.testing_freelist(),
[SlotId(2), SlotId(1), SlotId(0)] [SlotId(0), SlotId(1), SlotId(2)]
); );
Ok(()) Ok(())
@@ -1353,26 +1325,12 @@ mod test {
assert_eq!(pool.max_instances, 10); assert_eq!(pool.max_instances, 10);
assert_eq!(pool.page_size, native_page_size); assert_eq!(pool.page_size, native_page_size);
assert_eq!( assert_eq!(pool.index_allocator.testing_freelist(), []);
pool.index_allocator.testing_freelist(),
[
SlotId(0),
SlotId(1),
SlotId(2),
SlotId(3),
SlotId(4),
SlotId(5),
SlotId(6),
SlotId(7),
SlotId(8),
SlotId(9)
],
);
let base = pool.mapping.as_ptr() as usize; let base = pool.mapping.as_ptr() as usize;
let mut stacks = Vec::new(); let mut stacks = Vec::new();
for i in (0..10).rev() { for i in 0..10 {
let stack = pool.allocate().expect("allocation should succeed"); let stack = pool.allocate().expect("allocation should succeed");
assert_eq!( assert_eq!(
((stack.top().unwrap() as usize - base) / pool.stack_size) - 1, ((stack.top().unwrap() as usize - base) / pool.stack_size) - 1,
@@ -1392,16 +1350,16 @@ mod test {
assert_eq!( assert_eq!(
pool.index_allocator.testing_freelist(), pool.index_allocator.testing_freelist(),
[ [
SlotId(9), SlotId(0),
SlotId(8),
SlotId(7),
SlotId(6),
SlotId(5),
SlotId(4),
SlotId(3),
SlotId(2),
SlotId(1), SlotId(1),
SlotId(0) SlotId(2),
SlotId(3),
SlotId(4),
SlotId(5),
SlotId(6),
SlotId(7),
SlotId(8),
SlotId(9)
], ],
); );
@@ -1475,7 +1433,7 @@ mod test {
#[test] #[test]
fn test_stack_zeroed() -> Result<()> { fn test_stack_zeroed() -> Result<()> {
let config = PoolingInstanceAllocatorConfig { let config = PoolingInstanceAllocatorConfig {
strategy: PoolingAllocationStrategy::NextAvailable, max_unused_warm_slots: 0,
limits: InstanceLimits { limits: InstanceLimits {
count: 1, count: 1,
table_elements: 0, table_elements: 0,
@@ -1511,7 +1469,7 @@ mod test {
#[test] #[test]
fn test_stack_unzeroed() -> Result<()> { fn test_stack_unzeroed() -> Result<()> {
let config = PoolingInstanceAllocatorConfig { let config = PoolingInstanceAllocatorConfig {
strategy: PoolingAllocationStrategy::NextAvailable, max_unused_warm_slots: 0,
limits: InstanceLimits { limits: InstanceLimits {
count: 1, count: 1,
table_elements: 0, table_elements: 0,

View File

@@ -1,40 +1,18 @@
//! Index/slot allocator policies for the pooling allocator. //! Index/slot allocator policies for the pooling allocator.
use super::PoolingAllocationStrategy;
use crate::CompiledModuleId; use crate::CompiledModuleId;
use rand::rngs::SmallRng; use std::collections::hash_map::{Entry, HashMap};
use rand::{Rng, SeedableRng}; use std::mem;
use std::collections::HashMap;
use std::sync::Mutex; use std::sync::Mutex;
/// A slot index. The job of this allocator is to hand out these /// A slot index. The job of this allocator is to hand out these
/// indices. /// indices.
#[derive(Clone, Copy, Debug, PartialEq, Eq)] #[derive(Hash, Clone, Copy, Debug, PartialEq, Eq)]
pub struct SlotId(pub usize); pub struct SlotId(pub u32);
impl SlotId { impl SlotId {
/// The index of this slot. /// The index of this slot.
pub fn index(self) -> usize { pub fn index(self) -> usize {
self.0 self.0 as usize
}
}
/// An index in the global freelist.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct GlobalFreeListIndex(usize);
impl GlobalFreeListIndex {
/// The index of this slot.
fn index(self) -> usize {
self.0
}
}
/// An index in a per-module freelist.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct PerModuleFreeListIndex(usize);
impl PerModuleFreeListIndex {
/// The index of this slot.
fn index(self) -> usize {
self.0
} }
} }
@@ -43,153 +21,93 @@ pub struct IndexAllocator(Mutex<Inner>);
#[derive(Debug)] #[derive(Debug)]
struct Inner { struct Inner {
strategy: PoolingAllocationStrategy, /// Maximum number of "unused warm slots" which will be allowed during
rng: SmallRng, /// allocation.
/// Free-list of all slots.
/// ///
/// We use this to pick a victim when we don't have an appropriate slot with /// This is a user-configurable knob which can be used to influence the
/// the preferred affinity. /// maximum number of unused slots at any one point in time. A "warm slot"
free_list: Vec<SlotId>, /// is one that's considered having been previously allocated.
max_unused_warm_slots: u32,
/// Affine slot management which tracks which slots are free and were last /// Current count of "warm slots", or those that were previously allocated
/// used with the specified `CompiledModuleId`. /// which are now no longer in use.
/// ///
/// Invariant: any module ID in this hashmap must have a non-empty list of /// This is the size of the `warm` list.
/// free slots (otherwise we remove it). We remove a module's freelist when unused_warm_slots: u32,
/// we have no more slots with affinity for that module.
per_module: HashMap<CompiledModuleId, Vec<SlotId>>, /// A linked list (via indices) which enumerates all "warm and unused"
/// slots, or those which have previously been allocated and then free'd.
warm: List,
/// Last slot that was allocated for the first time ever.
///
/// This is initially 0 and is incremented during `pick_cold`. If this
/// matches `max_cold`, there are no more cold slots left.
last_cold: u32,
/// The state of any given slot. /// The state of any given slot.
/// ///
/// Records indices in the above list (empty) or two lists (with affinity), /// Records indices in the above list (empty) or two lists (with affinity),
/// and these indices are kept up-to-date to allow fast removal. /// and these indices are kept up-to-date to allow fast removal.
slot_state: Vec<SlotState>, slot_state: Vec<SlotState>,
/// Affine slot management which tracks which slots are free and were last
/// used with the specified `CompiledModuleId`.
///
/// The `List` here is appended to during deallocation and removal happens
/// from the tail during allocation.
module_affine: HashMap<CompiledModuleId, List>,
}
/// A helper "linked list" data structure which is based on indices.
#[derive(Default, Debug)]
struct List {
head: Option<SlotId>,
tail: Option<SlotId>,
}
/// A helper data structure for an intrusive linked list, coupled with the
/// `List` type.
#[derive(Default, Debug, Copy, Clone)]
struct Link {
prev: Option<SlotId>,
next: Option<SlotId>,
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub(crate) enum SlotState { enum SlotState {
/// Currently allocated. /// This slot is currently in use and is affine to the specified module.
Used(Option<CompiledModuleId>),
/// This slot is not currently used, and has never been used.
UnusedCold,
/// This slot is not currently used, but was previously allocated.
/// ///
/// Invariant: no slot in this state has its index in either /// The payload here is metadata about the lists that this slot is contained
/// `free_list` or any list in `per_module`. /// within.
Taken(Option<CompiledModuleId>), UnusedWarm(Unused),
/// Currently free. A free slot is able to be allocated for any
/// request, but may have affinity to a certain module that we
/// prefer to use it for.
///
/// Invariant: every slot in this state has its index in at least
/// `free_list`, and possibly a `per_module` free-list; see
/// FreeSlotState.
Free(FreeSlotState),
} }
impl SlotState { impl SlotState {
fn unwrap_free(&self) -> &FreeSlotState { fn unwrap_unused(&mut self) -> &mut Unused {
match self { match self {
&Self::Free(ref free) => free, SlotState::UnusedWarm(u) => u,
_ => panic!("Slot not free"), _ => unreachable!(),
}
}
fn unwrap_free_mut(&mut self) -> &mut FreeSlotState {
match self {
&mut Self::Free(ref mut free) => free,
_ => panic!("Slot not free"),
}
}
fn unwrap_module_id(&self) -> Option<CompiledModuleId> {
match self {
&Self::Taken(module_id) => module_id,
_ => panic!("Slot not in Taken state"),
} }
} }
} }
#[derive(Clone, Debug)] #[derive(Default, Copy, Clone, Debug)]
pub(crate) enum FreeSlotState { struct Unused {
/// The slot is free, and has no affinity. /// Which module this slot was historically affine to, if any.
/// affinity: Option<CompiledModuleId>,
/// Invariant: every slot in this state has its index in
/// `free_list`. No slot in this state has its index in any other
/// (per-module) free-list.
NoAffinity {
/// Index in the global free list.
///
/// Invariant: free_list[slot_state[i].free_list_index] == i.
free_list_index: GlobalFreeListIndex,
},
/// The slot is free, and has an affinity for some module. This
/// means we prefer to choose this slot (or some other one with
/// the same affinity) given a request to allocate a slot for this
/// module. It can, however, still be used for any other module if
/// needed.
///
/// Invariant: every slot in this state has its index in both
/// `free_list` *and* exactly one list in `per_module`.
Affinity {
module: CompiledModuleId,
/// Index in the global free list.
///
/// Invariant: free_list[slot_state[i].free_list_index] == i.
free_list_index: GlobalFreeListIndex,
/// Index in a per-module free list.
///
/// Invariant: per_module[slot_state[i].module][slot_state[i].per_module_index]
/// == i.
per_module_index: PerModuleFreeListIndex,
},
}
impl FreeSlotState { /// Metadata about the linked list for all slots affine to `affinity`.
/// Get the index of this slot in the global free list. affine_list_link: Link,
fn free_list_index(&self) -> GlobalFreeListIndex {
match self {
&Self::NoAffinity { free_list_index }
| &Self::Affinity {
free_list_index, ..
} => free_list_index,
}
}
/// Update the index of this slot in the global free list. /// Metadata within the `warm` list of the main allocator.
fn update_free_list_index(&mut self, index: GlobalFreeListIndex) { unused_list_link: Link,
match self {
&mut Self::NoAffinity {
ref mut free_list_index,
}
| &mut Self::Affinity {
ref mut free_list_index,
..
} => {
*free_list_index = index;
}
}
}
/// Get the index of this slot in its per-module free list.
fn per_module_index(&self) -> PerModuleFreeListIndex {
match self {
&Self::Affinity {
per_module_index, ..
} => per_module_index,
_ => panic!("per_module_index on slot with no affinity"),
}
}
/// Update the index of this slot in its per-module free list.
fn update_per_module_index(&mut self, index: PerModuleFreeListIndex) {
match self {
&mut Self::Affinity {
ref mut per_module_index,
..
} => {
*per_module_index = index;
}
_ => panic!("per_module_index on slot with no affinity"),
}
}
} }
enum AllocMode { enum AllocMode {
@@ -199,29 +117,14 @@ enum AllocMode {
impl IndexAllocator { impl IndexAllocator {
/// Create the default state for this strategy. /// Create the default state for this strategy.
pub fn new(strategy: PoolingAllocationStrategy, max_instances: usize) -> Self { pub fn new(max_instances: u32, max_unused_warm_slots: u32) -> Self {
let ids = (0..max_instances).map(|i| SlotId(i)).collect::<Vec<_>>();
// Use a deterministic seed during fuzzing to improve reproducibility of
// test cases, but otherwise outside of fuzzing use a random seed to
// shake things up.
let seed = if cfg!(fuzzing) {
[0; 32]
} else {
rand::thread_rng().gen()
};
let rng = SmallRng::from_seed(seed);
IndexAllocator(Mutex::new(Inner { IndexAllocator(Mutex::new(Inner {
rng, last_cold: 0,
strategy, max_unused_warm_slots,
free_list: ids, unused_warm_slots: 0,
per_module: HashMap::new(), module_affine: HashMap::new(),
slot_state: (0..max_instances) slot_state: (0..max_instances).map(|_| SlotState::UnusedCold).collect(),
.map(|i| { warm: List::default(),
SlotState::Free(FreeSlotState::NoAffinity {
free_list_index: GlobalFreeListIndex(i),
})
})
.collect(),
})) }))
} }
@@ -248,59 +151,51 @@ impl IndexAllocator {
let mut inner = self.0.lock().unwrap(); let mut inner = self.0.lock().unwrap();
let inner = &mut *inner; let inner = &mut *inner;
// Determine which `SlotId` will be chosen first. Below the free list // As a first-pass always attempt an affine allocation. This will
// metadata will be updated with our choice. // succeed if any slots are considered affine to `module_id` (if it's
let slot_id = match mode { // specified). Failing that something else is attempted to be chosen.
// If any slot is desired then the pooling allocation strategy let slot_id = inner.pick_affine(module_id).or_else(|| {
// determines which index is chosen. match mode {
// If any slot is requested then this is a normal instantiation
// looking for an index. Without any affine candidates there are
// two options here:
//
// 1. Pick a slot amongst previously allocated slots
// 2. Pick a slot that's never been used before
//
// The choice here is guided by the initial configuration of
// `max_unused_warm_slots`. If our unused warm slots, which are
// likely all affine, is below this threshold then the affinity
// of the warm slots isn't tampered with and first a cold slot
// is chosen. If the cold slot allocation fails, however, a warm
// slot is evicted.
//
// The opposite happens when we're above our threshold for the
// maximum number of warm slots, meaning that a warm slot is
// attempted to be picked from first with a cold slot following
// that. Note that the warm slot allocation in this case should
// only fail of `max_unused_warm_slots` is 0, otherwise
// `pick_warm` will always succeed.
AllocMode::AnySlot => { AllocMode::AnySlot => {
match inner.strategy { if inner.unused_warm_slots < inner.max_unused_warm_slots {
PoolingAllocationStrategy::NextAvailable => inner.pick_last_used()?, inner.pick_cold().or_else(|| inner.pick_warm())
PoolingAllocationStrategy::Random => inner.pick_random()?, } else {
// First attempt an affine allocation where the slot inner.pick_warm().or_else(|| {
// returned was previously used by `id`, but if that fails debug_assert!(inner.max_unused_warm_slots == 0);
// pick a random free slot ID. inner.pick_cold()
// })
// Note that we do this to maintain an unbiased stealing
// distribution: we want the likelihood of our taking a slot
// from some other module's freelist to be proportional to
// that module's freelist length. Or in other words, every
// *slot* should be equally likely to be stolen. The
// alternative, where we pick the victim module freelist
// first, means that either a module with an affinity
// freelist of one slot has the same chances of losing that
// slot as one with a hundred slots; or else we need a
// weighted random choice among modules, which is just as
// complex as this process.
//
// We don't bother picking an empty slot (no established
// affinity) before a random slot, because this is more
// complex, and in the steady state, all slots will see at
// least one instantiation very quickly, so there will never
// (past an initial phase) be a slot with no affinity.
PoolingAllocationStrategy::ReuseAffinity => inner
.pick_affine(module_id)
.or_else(|| inner.pick_random())?,
} }
} }
// In this mode an affinity-based allocation is always performed as // In this mode an affinity-based allocation is always performed
// the purpose here is to clear out slots relevant to `module_id` // as the purpose here is to clear out slots relevant to
// during module teardown. // `module_id` during module teardown. This means that there's
AllocMode::ForceAffineAndClear => inner.pick_affine(module_id)?, // no consulting non-affine slots in this path.
}; AllocMode::ForceAffineAndClear => None,
// Update internal metadata about the allocation of `slot_id` to
// `module_id`, meaning that it's removed from the per-module freelist
// if it was previously affine and additionally it's removed from the
// global freelist.
inner.remove_global_free_list_item(slot_id);
if let &SlotState::Free(FreeSlotState::Affinity { module, .. }) =
&inner.slot_state[slot_id.index()]
{
inner.remove_module_free_list_item(module, slot_id);
} }
inner.slot_state[slot_id.index()] = SlotState::Taken(match mode { })?;
inner.slot_state[slot_id.index()] = SlotState::Used(match mode {
AllocMode::ForceAffineAndClear => None, AllocMode::ForceAffineAndClear => None,
AllocMode::AnySlot => module_id, AllocMode::AnySlot => module_id,
}); });
@@ -310,24 +205,43 @@ impl IndexAllocator {
pub(crate) fn free(&self, index: SlotId) { pub(crate) fn free(&self, index: SlotId) {
let mut inner = self.0.lock().unwrap(); let mut inner = self.0.lock().unwrap();
let free_list_index = GlobalFreeListIndex(inner.free_list.len()); let inner = &mut *inner;
inner.free_list.push(index); let module = match inner.slot_state[index.index()] {
let module_id = inner.slot_state[index.index()].unwrap_module_id(); SlotState::Used(module) => module,
inner.slot_state[index.index()] = if let Some(id) = module_id { _ => unreachable!(),
let per_module_list = inner
.per_module
.entry(id)
.or_insert_with(|| Vec::with_capacity(1));
let per_module_index = PerModuleFreeListIndex(per_module_list.len());
per_module_list.push(index);
SlotState::Free(FreeSlotState::Affinity {
module: id,
free_list_index,
per_module_index,
})
} else {
SlotState::Free(FreeSlotState::NoAffinity { free_list_index })
}; };
// Bump the number of warm slots since this slot is now considered
// previously used. Afterwards append it to the linked list of all
// unused and warm slots.
inner.unused_warm_slots += 1;
let unused_list_link = inner
.warm
.append(index, &mut inner.slot_state, |s| &mut s.unused_list_link);
let affine_list_link = match module {
// If this slot is affine to a particular module then append this
// index to the linked list for the affine module. Otherwise insert
// a new one-element linked list.
Some(module) => match inner.module_affine.entry(module) {
Entry::Occupied(mut e) => e
.get_mut()
.append(index, &mut inner.slot_state, |s| &mut s.affine_list_link),
Entry::Vacant(v) => {
v.insert(List::new(index));
Link::default()
}
},
// If this slot has no affinity then the affine link is empty.
None => Link::default(),
};
inner.slot_state[index.index()] = SlotState::UnusedWarm(Unused {
affinity: module,
affine_list_link,
unused_list_link,
});
} }
/// For testing only, we want to be able to assert what is on the /// For testing only, we want to be able to assert what is on the
@@ -335,7 +249,10 @@ impl IndexAllocator {
#[cfg(test)] #[cfg(test)]
pub(crate) fn testing_freelist(&self) -> Vec<SlotId> { pub(crate) fn testing_freelist(&self) -> Vec<SlotId> {
let inner = self.0.lock().unwrap(); let inner = self.0.lock().unwrap();
inner.free_list.clone() inner
.warm
.iter(&inner.slot_state, |s| &s.unused_list_link)
.collect()
} }
/// For testing only, get the list of all modules with at least /// For testing only, get the list of all modules with at least
@@ -343,72 +260,151 @@ impl IndexAllocator {
#[cfg(test)] #[cfg(test)]
pub(crate) fn testing_module_affinity_list(&self) -> Vec<CompiledModuleId> { pub(crate) fn testing_module_affinity_list(&self) -> Vec<CompiledModuleId> {
let inner = self.0.lock().unwrap(); let inner = self.0.lock().unwrap();
let mut ret = vec![]; inner.module_affine.keys().copied().collect()
for (module, list) in inner.per_module.iter() {
assert!(!list.is_empty());
ret.push(*module);
}
ret
} }
} }
impl Inner { impl Inner {
fn pick_last_used(&self) -> Option<SlotId> {
self.free_list.last().copied()
}
fn pick_random(&mut self) -> Option<SlotId> {
if self.free_list.len() == 0 {
return None;
}
let i = self.rng.gen_range(0..self.free_list.len());
Some(self.free_list[i])
}
/// Attempts to allocate a slot already affine to `id`, returning `None` if /// Attempts to allocate a slot already affine to `id`, returning `None` if
/// `id` is `None` or if there are no affine slots. /// `id` is `None` or if there are no affine slots.
fn pick_affine(&self, module_id: Option<CompiledModuleId>) -> Option<SlotId> { fn pick_affine(&mut self, module_id: Option<CompiledModuleId>) -> Option<SlotId> {
let free = self.per_module.get(&module_id?)?; // Note that the `tail` is chosen here of the affine list as it's the
free.last().copied() // most recently used, which for affine allocations is what we want --
// maximizing temporal reuse.
let ret = self.module_affine.get(&module_id?)?.tail?;
self.remove(ret);
Some(ret)
} }
/// Remove a slot-index from the global free list. fn pick_warm(&mut self) -> Option<SlotId> {
fn remove_global_free_list_item(&mut self, index: SlotId) { // Insertions into the `unused` list happen at the `tail`, so the
let free_list_index = self.slot_state[index.index()] // least-recently-used item will be at the head. That's our goal here,
.unwrap_free() // pick the least-recently-used slot since something "warm" is being
.free_list_index(); // evicted anyway.
assert_eq!(index, self.free_list.swap_remove(free_list_index.index())); let head = self.warm.head?;
if free_list_index.index() < self.free_list.len() { self.remove(head);
let replaced = self.free_list[free_list_index.index()]; Some(head)
self.slot_state[replaced.index()] }
.unwrap_free_mut()
.update_free_list_index(free_list_index); fn remove(&mut self, slot: SlotId) {
// Decrement the size of the warm list, and additionally remove it from
// the `warm` linked list.
self.unused_warm_slots -= 1;
self.warm
.remove(slot, &mut self.slot_state, |u| &mut u.unused_list_link);
// If this slot is affine to a module then additionally remove it from
// that module's affinity linked list. Note that if the module's affine
// list is empty then the module's entry in the map is completely
// removed as well.
let module = self.slot_state[slot.index()].unwrap_unused().affinity;
if let Some(module) = module {
let mut list = match self.module_affine.entry(module) {
Entry::Occupied(e) => e,
Entry::Vacant(_) => unreachable!(),
};
list.get_mut()
.remove(slot, &mut self.slot_state, |u| &mut u.affine_list_link);
if list.get_mut().head.is_none() {
list.remove();
}
} }
} }
/// Remove a slot-index from a per-module free list. fn pick_cold(&mut self) -> Option<SlotId> {
fn remove_module_free_list_item(&mut self, module_id: CompiledModuleId, index: SlotId) { if (self.last_cold as usize) == self.slot_state.len() {
debug_assert!( None
self.per_module.contains_key(&module_id), } else {
"per_module list for given module should not be empty" let ret = Some(SlotId(self.last_cold));
); self.last_cold += 1;
ret
let per_module_list = self.per_module.get_mut(&module_id).unwrap();
debug_assert!(!per_module_list.is_empty());
let per_module_index = self.slot_state[index.index()]
.unwrap_free()
.per_module_index();
assert_eq!(index, per_module_list.swap_remove(per_module_index.index()));
if per_module_index.index() < per_module_list.len() {
let replaced = per_module_list[per_module_index.index()];
self.slot_state[replaced.index()]
.unwrap_free_mut()
.update_per_module_index(per_module_index);
} }
if per_module_list.is_empty() {
self.per_module.remove(&module_id);
} }
}
impl List {
/// Creates a new one-element list pointing at `id`.
fn new(id: SlotId) -> List {
List {
head: Some(id),
tail: Some(id),
}
}
/// Appends the `id` to this list whose links are determined by `link`.
fn append(
&mut self,
id: SlotId,
states: &mut [SlotState],
link: fn(&mut Unused) -> &mut Link,
) -> Link {
// This `id` is the new tail...
let tail = mem::replace(&mut self.tail, Some(id));
// If the tail was present, then update its `next` field to ourselves as
// we've been appended, otherwise update the `head` since the list was
// previously empty.
match tail {
Some(tail) => link(states[tail.index()].unwrap_unused()).next = Some(id),
None => self.head = Some(id),
}
Link {
prev: tail,
next: None,
}
}
/// Removes `id` from this list whose links are determined by `link`.
fn remove(
&mut self,
id: SlotId,
slot_state: &mut [SlotState],
link: fn(&mut Unused) -> &mut Link,
) -> Unused {
let mut state = *slot_state[id.index()].unwrap_unused();
let next = link(&mut state).next;
let prev = link(&mut state).prev;
// If a `next` node is present for this link, then its previous was our
// own previous now. Otherwise we are the tail so the new tail is our
// previous.
match next {
Some(next) => link(slot_state[next.index()].unwrap_unused()).prev = prev,
None => self.tail = prev,
}
// Same as the `next` node, except everything is in reverse.
match prev {
Some(prev) => link(slot_state[prev.index()].unwrap_unused()).next = next,
None => self.head = next,
}
state
}
#[cfg(test)]
fn iter<'a>(
&'a self,
states: &'a [SlotState],
link: fn(&Unused) -> &Link,
) -> impl Iterator<Item = SlotId> + 'a {
let mut cur = self.head;
let mut prev = None;
std::iter::from_fn(move || {
if cur.is_none() {
assert_eq!(prev, self.tail);
}
let ret = cur?;
match &states[ret.index()] {
SlotState::UnusedWarm(u) => {
assert_eq!(link(u).prev, prev);
prev = Some(ret);
cur = link(u).next
}
_ => unreachable!(),
}
Some(ret)
})
} }
} }
@@ -416,29 +412,13 @@ impl Inner {
mod test { mod test {
use super::{IndexAllocator, SlotId}; use super::{IndexAllocator, SlotId};
use crate::CompiledModuleIdAllocator; use crate::CompiledModuleIdAllocator;
use crate::PoolingAllocationStrategy;
#[test] #[test]
fn test_next_available_allocation_strategy() { fn test_next_available_allocation_strategy() {
let strat = PoolingAllocationStrategy::NextAvailable;
for size in 0..20 { for size in 0..20 {
let state = IndexAllocator::new(strat, size); let state = IndexAllocator::new(size, 0);
for i in 0..size { for i in 0..size {
assert_eq!(state.alloc(None).unwrap().index(), size - i - 1); assert_eq!(state.alloc(None).unwrap().index(), i as usize);
}
assert!(state.alloc(None).is_none());
}
}
#[test]
fn test_random_allocation_strategy() {
let strat = PoolingAllocationStrategy::Random;
for size in 0..20 {
let state = IndexAllocator::new(strat, size);
for _ in 0..size {
assert!(state.alloc(None).unwrap().index() < size);
} }
assert!(state.alloc(None).is_none()); assert!(state.alloc(None).is_none());
} }
@@ -446,16 +426,15 @@ mod test {
#[test] #[test]
fn test_affinity_allocation_strategy() { fn test_affinity_allocation_strategy() {
let strat = PoolingAllocationStrategy::ReuseAffinity;
let id_alloc = CompiledModuleIdAllocator::new(); let id_alloc = CompiledModuleIdAllocator::new();
let id1 = id_alloc.alloc(); let id1 = id_alloc.alloc();
let id2 = id_alloc.alloc(); let id2 = id_alloc.alloc();
let state = IndexAllocator::new(strat, 100); let state = IndexAllocator::new(100, 100);
let index1 = state.alloc(Some(id1)).unwrap(); let index1 = state.alloc(Some(id1)).unwrap();
assert!(index1.index() < 100); assert_eq!(index1.index(), 0);
let index2 = state.alloc(Some(id2)).unwrap(); let index2 = state.alloc(Some(id2)).unwrap();
assert!(index2.index() < 100); assert_eq!(index2.index(), 1);
assert_ne!(index1, index2); assert_ne!(index1, index2);
state.free(index1); state.free(index1);
@@ -503,12 +482,8 @@ mod test {
let id_alloc = CompiledModuleIdAllocator::new(); let id_alloc = CompiledModuleIdAllocator::new();
let id = id_alloc.alloc(); let id = id_alloc.alloc();
for strat in [ for max_unused_warm_slots in [0, 1, 2] {
PoolingAllocationStrategy::ReuseAffinity, let state = IndexAllocator::new(100, max_unused_warm_slots);
PoolingAllocationStrategy::NextAvailable,
PoolingAllocationStrategy::Random,
] {
let state = IndexAllocator::new(strat, 100);
let index1 = state.alloc(Some(id)).unwrap(); let index1 = state.alloc(Some(id)).unwrap();
let index2 = state.alloc(Some(id)).unwrap(); let index2 = state.alloc(Some(id)).unwrap();
@@ -525,12 +500,11 @@ mod test {
use rand::Rng; use rand::Rng;
let mut rng = rand::thread_rng(); let mut rng = rand::thread_rng();
let strat = PoolingAllocationStrategy::ReuseAffinity;
let id_alloc = CompiledModuleIdAllocator::new(); let id_alloc = CompiledModuleIdAllocator::new();
let ids = std::iter::repeat_with(|| id_alloc.alloc()) let ids = std::iter::repeat_with(|| id_alloc.alloc())
.take(10) .take(10)
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let state = IndexAllocator::new(strat, 1000); let state = IndexAllocator::new(1000, 1000);
let mut allocated: Vec<SlotId> = vec![]; let mut allocated: Vec<SlotId> = vec![];
let mut last_id = vec![None; 1000]; let mut last_id = vec![None; 1000];
@@ -566,4 +540,59 @@ mod test {
hits hits
); );
} }
#[test]
fn test_affinity_threshold() {
let id_alloc = CompiledModuleIdAllocator::new();
let id1 = id_alloc.alloc();
let id2 = id_alloc.alloc();
let id3 = id_alloc.alloc();
let state = IndexAllocator::new(10, 2);
// Set some slot affinities
assert_eq!(state.alloc(Some(id1)), Some(SlotId(0)));
state.free(SlotId(0));
assert_eq!(state.alloc(Some(id2)), Some(SlotId(1)));
state.free(SlotId(1));
// Only 2 slots are allowed to be unused and warm, so we're at our
// threshold, meaning one must now be evicted.
assert_eq!(state.alloc(Some(id3)), Some(SlotId(0)));
state.free(SlotId(0));
// pickup `id2` again, it should be affine.
assert_eq!(state.alloc(Some(id2)), Some(SlotId(1)));
// with only one warm slot available allocation for `id1` should pick a
// fresh slot
assert_eq!(state.alloc(Some(id1)), Some(SlotId(2)));
state.free(SlotId(1));
state.free(SlotId(2));
// ensure everything stays affine
assert_eq!(state.alloc(Some(id1)), Some(SlotId(2)));
assert_eq!(state.alloc(Some(id2)), Some(SlotId(1)));
assert_eq!(state.alloc(Some(id3)), Some(SlotId(0)));
state.free(SlotId(1));
state.free(SlotId(2));
state.free(SlotId(0));
// LRU is 1, so that should be picked
assert_eq!(state.alloc(Some(id_alloc.alloc())), Some(SlotId(1)));
// Pick another LRU entry, this time 2
assert_eq!(state.alloc(Some(id_alloc.alloc())), Some(SlotId(2)));
// This should preserve slot `0` and pick up something new
assert_eq!(state.alloc(Some(id_alloc.alloc())), Some(SlotId(3)));
state.free(SlotId(1));
state.free(SlotId(2));
state.free(SlotId(3));
// for good measure make sure id3 is still affine
assert_eq!(state.alloc(Some(id3)), Some(SlotId(0)));
}
} }

View File

@@ -56,8 +56,7 @@ pub use crate::instance::{
}; };
#[cfg(feature = "pooling-allocator")] #[cfg(feature = "pooling-allocator")]
pub use crate::instance::{ pub use crate::instance::{
InstanceLimits, PoolingAllocationStrategy, PoolingInstanceAllocator, InstanceLimits, PoolingInstanceAllocator, PoolingInstanceAllocatorConfig,
PoolingInstanceAllocatorConfig,
}; };
pub use crate::memory::{ pub use crate::memory::{
DefaultMemoryCreator, Memory, RuntimeLinearMemory, RuntimeMemoryCreator, SharedMemory, DefaultMemoryCreator, Memory, RuntimeLinearMemory, RuntimeMemoryCreator, SharedMemory,
@@ -156,7 +155,7 @@ pub unsafe trait Store {
/// is chiefly needed for lazy initialization of various bits of /// is chiefly needed for lazy initialization of various bits of
/// instance state. /// instance state.
/// ///
/// When an instance is created, it holds an Arc<dyn ModuleRuntimeInfo> /// When an instance is created, it holds an `Arc<dyn ModuleRuntimeInfo>`
/// so that it can get to signatures, metadata on functions, memory and /// so that it can get to signatures, metadata on functions, memory and
/// funcref-table images, etc. All of these things are ordinarily known /// funcref-table images, etc. All of these things are ordinarily known
/// by the higher-level layers of Wasmtime. Specifically, the main /// by the higher-level layers of Wasmtime. Specifically, the main

View File

@@ -1712,17 +1712,61 @@ pub struct PoolingAllocationConfig {
config: wasmtime_runtime::PoolingInstanceAllocatorConfig, config: wasmtime_runtime::PoolingInstanceAllocatorConfig,
} }
#[cfg(feature = "pooling-allocator")]
pub use wasmtime_runtime::PoolingAllocationStrategy;
#[cfg(feature = "pooling-allocator")] #[cfg(feature = "pooling-allocator")]
impl PoolingAllocationConfig { impl PoolingAllocationConfig {
/// Configures the method by which slots in the pooling allocator are /// Configures the maximum number of "unused warm slots" to retain in the
/// allocated to instances /// pooling allocator.
/// ///
/// This defaults to [`PoolingAllocationStrategy::ReuseAffinity`] . /// The pooling allocator operates over slots to allocate from, and each
pub fn strategy(&mut self, strategy: PoolingAllocationStrategy) -> &mut Self { /// slot is considered "cold" if it's never been used before or "warm" if
self.config.strategy = strategy; /// it's been used by some module in the past. Slots in the pooling
/// allocator additionally track an "affinity" flag to a particular core
/// wasm module. When a module is instantiated into a slot then the slot is
/// considered affine to that module, even after the instance has been
/// dealloocated.
///
/// When a new instance is created then a slot must be chosen, and the
/// current algorithm for selecting a slot is:
///
/// * If there are slots that are affine to the module being instantiated,
/// then the most recently used slot is selected to be allocated from.
/// This is done to improve reuse of resources such as memory mappings and
/// additionally try to benefit from temporal locality for things like
/// caches.
///
/// * Otherwise if there are more than N affine slots to other modules, then
/// one of those affine slots is chosen to be allocated. The slot chosen
/// is picked on a least-recently-used basis.
///
/// * Finally, if there are less than N affine slots to other modules, then
/// the non-affine slots are allocated from.
///
/// This setting, `max_unused_warm_slots`, is the value for N in the above
/// algorithm. The purpose of this setting is to have a knob over the RSS
/// impact of "unused slots" for a long-running wasm server.
///
/// If this setting is set to 0, for example, then affine slots are
/// aggressively resused on a least-recently-used basis. A "cold" slot is
/// only used if there are no affine slots available to allocate from. This
/// means that the set of slots used over the lifetime of a program is the
/// same as the maximum concurrent number of wasm instances.
///
/// If this setting is set to infinity, however, then cold slots are
/// prioritized to be allocated from. This means that the set of slots used
/// over the lifetime of a program will approach
/// [`PoolingAllocationConfig::instance_count`], or the maximum number of
/// slots in the pooling allocator.
///
/// Wasmtime does not aggressively decommit all resources associated with a
/// slot when the slot is not in use. For example the
/// [`PoolingAllocationConfig::linear_memory_keep_resident`] option can be
/// used to keep memory associated with a slot, even when it's not in use.
/// This means that the total set of used slots in the pooling instance
/// allocator can impact the overall RSS usage of a program.
///
/// The default value for this option is 100.
pub fn max_unused_warm_slots(&mut self, max: u32) -> &mut Self {
self.config.max_unused_warm_slots = max;
self self
} }