wasmtime/runtime/vm/instance/allocator/pooling/
unix_stack_pool.rs1#![cfg_attr(asan, allow(dead_code))]
2
3use super::index_allocator::{SimpleIndexAllocator, SlotId};
4use crate::prelude::*;
5use crate::runtime::vm::{
6 HostAlignedByteCount, Mmap, PoolingInstanceAllocatorConfig, mmap::AlignedLength,
7};
8
9#[derive(Debug)]
20pub struct StackPool {
21 mapping: Mmap<AlignedLength>,
22 stack_size: HostAlignedByteCount,
23 max_stacks: usize,
24 page_size: HostAlignedByteCount,
25 index_allocator: SimpleIndexAllocator,
26 async_stack_zeroing: bool,
27 async_stack_keep_resident: HostAlignedByteCount,
28}
29
30impl StackPool {
31 #[cfg(test)]
32 pub fn enabled() -> bool {
33 true
34 }
35
36 pub fn new(config: &PoolingInstanceAllocatorConfig) -> Result<Self> {
37 use rustix::mm::{MprotectFlags, mprotect};
38
39 let page_size = HostAlignedByteCount::host_page_size();
40
41 let stack_size = if config.stack_size == 0 {
43 HostAlignedByteCount::ZERO
44 } else {
45 HostAlignedByteCount::new_rounded_up(config.stack_size)
46 .and_then(|size| size.checked_add(HostAlignedByteCount::host_page_size()))
47 .context("stack size exceeds addressable memory")?
48 };
49
50 let max_stacks = usize::try_from(config.limits.total_stacks).unwrap();
51
52 let allocation_size = stack_size
53 .checked_mul(max_stacks)
54 .context("total size of execution stacks exceeds addressable memory")?;
55
56 let mapping = Mmap::accessible_reserved(allocation_size, allocation_size)
57 .context("failed to create stack pool mapping")?;
58
59 if !allocation_size.is_zero() {
61 unsafe {
62 for i in 0..max_stacks {
63 let offset = stack_size.unchecked_mul(i);
66 let bottom_of_stack = mapping.as_ptr().add(offset.byte_count()).cast_mut();
68 mprotect(
69 bottom_of_stack.cast(),
70 page_size.byte_count(),
71 MprotectFlags::empty(),
72 )
73 .context("failed to protect stack guard page")?;
74 }
75 }
76 }
77
78 Ok(Self {
79 mapping,
80 stack_size,
81 max_stacks,
82 page_size,
83 async_stack_zeroing: config.async_stack_zeroing,
84 async_stack_keep_resident: HostAlignedByteCount::new_rounded_up(
85 config.async_stack_keep_resident,
86 )?,
87 index_allocator: SimpleIndexAllocator::new(config.limits.total_stacks),
88 })
89 }
90
91 pub fn is_empty(&self) -> bool {
93 self.index_allocator.is_empty()
94 }
95
96 pub fn allocate(&self) -> Result<wasmtime_fiber::FiberStack> {
98 if self.stack_size.is_zero() {
99 bail!("pooling allocator not configured to enable fiber stack allocation");
100 }
101
102 let index = self
103 .index_allocator
104 .alloc()
105 .ok_or_else(|| super::PoolConcurrencyLimitError::new(self.max_stacks, "fibers"))?
106 .index();
107
108 assert!(index < self.max_stacks);
109
110 unsafe {
111 let size_without_guard = self.stack_size.checked_sub(self.page_size).expect(
113 "self.stack_size is host-page-aligned and is > 0,\
114 so it must be >= self.page_size",
115 );
116
117 let bottom_of_stack = self
118 .mapping
119 .as_ptr()
120 .add(self.stack_size.unchecked_mul(index).byte_count())
121 .cast_mut();
122
123 let stack = wasmtime_fiber::FiberStack::from_raw_parts(
124 bottom_of_stack,
125 self.page_size.byte_count(),
126 size_without_guard.byte_count(),
127 )?;
128 Ok(stack)
129 }
130 }
131
132 pub unsafe fn zero_stack(
148 &self,
149 stack: &mut wasmtime_fiber::FiberStack,
150 mut decommit: impl FnMut(*mut u8, usize),
151 ) -> usize {
152 assert!(stack.is_from_raw_parts());
153 assert!(
154 !self.stack_size.is_zero(),
155 "pooling allocator not configured to enable fiber stack allocation \
156 (Self::allocate should have returned an error)"
157 );
158
159 if !self.async_stack_zeroing {
160 return 0;
161 }
162
163 let top = stack
164 .top()
165 .expect("fiber stack not allocated from the pool") as usize;
166
167 let base = self.mapping.as_ptr() as usize;
168 let len = self.mapping.len();
169 assert!(
170 top > base && top <= (base + len),
171 "fiber stack top pointer not in range"
172 );
173
174 let stack_size = self.stack_size.checked_sub(self.page_size).expect(
176 "self.stack_size is host-page-aligned and is > 0,\
177 so it must be >= self.page_size",
178 );
179 let bottom_of_stack = top - stack_size.byte_count();
180 let start_of_stack = bottom_of_stack - self.page_size.byte_count();
181 assert!(start_of_stack >= base && start_of_stack < (base + len));
182 assert!((start_of_stack - base) % self.stack_size.byte_count() == 0);
183
184 let size_to_memset = stack_size.min(self.async_stack_keep_resident);
192 let rest = stack_size
193 .checked_sub(size_to_memset)
194 .expect("stack_size >= size_to_memset");
195
196 unsafe {
199 std::ptr::write_bytes(
200 (bottom_of_stack + rest.byte_count()) as *mut u8,
201 0,
202 size_to_memset.byte_count(),
203 );
204 }
205
206 decommit(bottom_of_stack as _, rest.byte_count());
208
209 size_to_memset.byte_count()
210 }
211
212 pub unsafe fn deallocate(&self, stack: wasmtime_fiber::FiberStack, bytes_resident: usize) {
222 assert!(stack.is_from_raw_parts());
223
224 let top = stack
225 .top()
226 .expect("fiber stack not allocated from the pool") as usize;
227
228 let base = self.mapping.as_ptr() as usize;
229 let len = self.mapping.len();
230 assert!(
231 top > base && top <= (base + len),
232 "fiber stack top pointer not in range"
233 );
234
235 let stack_size = self.stack_size.byte_count() - self.page_size.byte_count();
237 let bottom_of_stack = top - stack_size;
238 let start_of_stack = bottom_of_stack - self.page_size.byte_count();
239 assert!(start_of_stack >= base && start_of_stack < (base + len));
240 assert!((start_of_stack - base) % self.stack_size.byte_count() == 0);
241
242 let index = (start_of_stack - base) / self.stack_size.byte_count();
243 assert!(index < self.max_stacks);
244 let index = u32::try_from(index).unwrap();
245
246 self.index_allocator.free(SlotId(index), bytes_resident);
247 }
248
249 pub fn unused_warm_slots(&self) -> u32 {
250 self.index_allocator.unused_warm_slots()
251 }
252
253 pub fn unused_bytes_resident(&self) -> Option<usize> {
254 if self.async_stack_zeroing {
255 Some(self.index_allocator.unused_bytes_resident())
256 } else {
257 None
258 }
259 }
260}
261
262#[cfg(all(test, unix, feature = "async", not(miri), not(asan)))]
263mod tests {
264 use super::*;
265 use crate::runtime::vm::InstanceLimits;
266
267 #[test]
268 fn test_stack_pool() -> Result<()> {
269 let config = PoolingInstanceAllocatorConfig {
270 limits: InstanceLimits {
271 total_stacks: 10,
272 ..Default::default()
273 },
274 stack_size: 1,
275 async_stack_zeroing: true,
276 ..PoolingInstanceAllocatorConfig::default()
277 };
278 let pool = StackPool::new(&config)?;
279
280 let native_page_size = crate::runtime::vm::host_page_size();
281 assert_eq!(pool.stack_size, 2 * native_page_size);
282 assert_eq!(pool.max_stacks, 10);
283 assert_eq!(pool.page_size, native_page_size);
284
285 assert_eq!(pool.index_allocator.testing_freelist(), []);
286
287 let base = pool.mapping.as_ptr() as usize;
288
289 let mut stacks = Vec::new();
290 for i in 0..10 {
291 let stack = pool.allocate().expect("allocation should succeed");
292 assert_eq!(
293 ((stack.top().unwrap() as usize - base) / pool.stack_size.byte_count()) - 1,
294 i
295 );
296 stacks.push(stack);
297 }
298
299 assert_eq!(pool.index_allocator.testing_freelist(), []);
300
301 assert!(pool.allocate().is_err(), "allocation should fail");
302
303 for stack in stacks {
304 unsafe {
305 pool.deallocate(stack, 0);
306 }
307 }
308
309 assert_eq!(
310 pool.index_allocator.testing_freelist(),
311 [
312 SlotId(0),
313 SlotId(1),
314 SlotId(2),
315 SlotId(3),
316 SlotId(4),
317 SlotId(5),
318 SlotId(6),
319 SlotId(7),
320 SlotId(8),
321 SlotId(9)
322 ],
323 );
324
325 Ok(())
326 }
327}