1 //! A queue for batching decommits together.
2 //!
3 //! We don't immediately decommit a Wasm table/memory/stack/etc... eagerly, but
4 //! instead batch them up to be decommitted together. This module implements
5 //! that queuing and batching.
6 //!
7 //! Even when batching is "disabled" we still use this queue. Batching is
8 //! disabled by specifying a batch size of one, in which case, this queue will
9 //! immediately get flushed every time we push onto it.
10 
11 use super::PoolingInstanceAllocator;
12 use crate::vm::{MemoryAllocationIndex, MemoryImageSlot, Table, TableAllocationIndex};
13 use smallvec::SmallVec;
14 use std::io;
15 
16 #[cfg(feature = "async")]
17 use wasmtime_fiber::FiberStack;
18 
19 #[cfg(unix)]
20 #[expect(non_camel_case_types, reason = "matching libc naming")]
21 type iovec = libc::iovec;
22 
23 #[cfg(not(unix))]
24 #[expect(non_camel_case_types, reason = "matching libc naming")]
25 struct iovec {
26     iov_base: *mut libc::c_void,
27     iov_len: libc::size_t,
28 }
29 
30 #[repr(transparent)]
31 struct IoVec(iovec);
32 
33 unsafe impl Send for IoVec {}
34 unsafe impl Sync for IoVec {}
35 
36 impl std::fmt::Debug for IoVec {
fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result37     fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
38         f.debug_struct("IoVec")
39             .field("base", &self.0.iov_base)
40             .field("len", &self.0.iov_len)
41             .finish()
42     }
43 }
44 
45 #[cfg(feature = "async")]
46 struct SendSyncStack(FiberStack);
47 #[cfg(feature = "async")]
48 unsafe impl Send for SendSyncStack {}
49 #[cfg(feature = "async")]
50 unsafe impl Sync for SendSyncStack {}
51 
52 #[derive(Default)]
53 pub struct DecommitQueue {
54     raw: SmallVec<[IoVec; 2]>,
55     memories: SmallVec<[(MemoryAllocationIndex, MemoryImageSlot, usize); 1]>,
56     tables: SmallVec<[(TableAllocationIndex, Table, usize); 1]>,
57     #[cfg(feature = "async")]
58     stacks: SmallVec<[(SendSyncStack, usize); 1]>,
59     //
60     // TODO: GC heaps are not well-integrated with the pooling allocator
61     // yet. Once we better integrate them, we should start (optionally) zeroing
62     // them, and batching that up here.
63     //
64     // #[cfg(feature = "gc")]
65     // pub gc_heaps: SmallVec<[(GcHeapAllocationIndex, Box<dyn GcHeap>); 1]>,
66 }
67 
68 impl std::fmt::Debug for DecommitQueue {
fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result69     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
70         f.debug_struct("DecommitQueue")
71             .field("raw", &self.raw)
72             .finish_non_exhaustive()
73     }
74 }
75 
76 impl DecommitQueue {
77     /// Append another queue to this queue.
78     pub fn append(
79         &mut self,
80         Self {
81             raw,
82             memories,
83             tables,
84             #[cfg(feature = "async")]
85             stacks,
86         }: &mut Self,
87     ) {
88         self.raw.append(raw);
89         self.memories.append(memories);
90         self.tables.append(tables);
91         #[cfg(feature = "async")]
92         self.stacks.append(stacks);
93     }
94 
95     /// How many raw memory regions are enqueued for decommit?
raw_len(&self) -> usize96     pub fn raw_len(&self) -> usize {
97         self.raw.len()
98     }
99 
100     /// Enqueue a region of memory for decommit.
101     ///
102     /// It is the caller's responsibility to push the associated data via
103     /// `self.push_{memory,table,stack}` as appropriate.
104     ///
105     /// # Safety
106     ///
107     /// The enqueued memory regions must be safe to decommit when `flush` is
108     /// called (no other references, not in use, won't be otherwise unmapped,
109     /// etc...).
push_raw(&mut self, ptr: *mut u8, len: usize)110     pub unsafe fn push_raw(&mut self, ptr: *mut u8, len: usize) {
111         self.raw.push(IoVec(iovec {
112             iov_base: ptr.cast(),
113             iov_len: len,
114         }));
115     }
116 
117     /// Push a memory into the queue.
118     ///
119     /// # Safety
120     ///
121     /// This memory should not be in use, and its decommit regions must have
122     /// already been enqueued via `self.enqueue_raw`.
push_memory( &mut self, allocation_index: MemoryAllocationIndex, image: MemoryImageSlot, bytes_resident: usize, )123     pub unsafe fn push_memory(
124         &mut self,
125         allocation_index: MemoryAllocationIndex,
126         image: MemoryImageSlot,
127         bytes_resident: usize,
128     ) {
129         self.memories
130             .push((allocation_index, image, bytes_resident));
131     }
132 
133     /// Push a table into the queue.
134     ///
135     /// # Safety
136     ///
137     /// This table should not be in use, and its decommit regions must have
138     /// already been enqueued via `self.enqueue_raw`.
push_table( &mut self, allocation_index: TableAllocationIndex, table: Table, bytes_resident: usize, )139     pub unsafe fn push_table(
140         &mut self,
141         allocation_index: TableAllocationIndex,
142         table: Table,
143         bytes_resident: usize,
144     ) {
145         self.tables.push((allocation_index, table, bytes_resident));
146     }
147 
148     /// Push a stack into the queue.
149     ///
150     /// # Safety
151     ///
152     /// This stack should not be in use, and its decommit regions must have
153     /// already been enqueued via `self.enqueue_raw`.
154     #[cfg(feature = "async")]
push_stack(&mut self, stack: FiberStack, bytes_resident: usize)155     pub unsafe fn push_stack(&mut self, stack: FiberStack, bytes_resident: usize) {
156         self.stacks.push((SendSyncStack(stack), bytes_resident));
157     }
158 
159     /// Returns if any decommit call failed.
decommit_all_raw(&mut self) -> io::Result<()>160     fn decommit_all_raw(&mut self) -> io::Result<()> {
161         for iovec in self.raw.drain(..) {
162             unsafe {
163                 crate::vm::sys::vm::decommit_pages(iovec.0.iov_base.cast(), iovec.0.iov_len)?;
164             }
165         }
166         Ok(())
167     }
168 
169     /// Flush this queue, decommitting all enqueued regions in batch.
170     ///
171     /// Returns `true` if we did any decommits and returned their entities to
172     /// the associated free lists; `false` if the queue was empty.
flush(mut self, pool: &PoolingInstanceAllocator) -> bool173     pub fn flush(mut self, pool: &PoolingInstanceAllocator) -> bool {
174         // First, do the raw decommit syscall(s).
175         let decommit_succeeded = self.decommit_all_raw().is_ok();
176 
177         // Second, restore the various entities to their associated pools' free
178         // lists. This is safe, and they are ready for reuse, now that their
179         // memory regions have been decommitted.
180         //
181         // Note that for memory images the images are all dropped here and
182         // ignored if any decommits failed. This signifies how the state of the
183         // slot is unknown and needs to be paved over in the future. Also note
184         // that `bytes_resident` is probably too low, but there's no other
185         // precise way to know, so it's left here as-is and it'll get reset when
186         // the slot is reused.
187         let mut deallocated_any = false;
188         for (allocation_index, image, bytes_resident) in self.memories {
189             deallocated_any = true;
190             let image = if decommit_succeeded {
191                 Some(image)
192             } else {
193                 None
194             };
195             unsafe {
196                 pool.memories
197                     .deallocate(allocation_index, image, bytes_resident);
198             }
199         }
200         for (allocation_index, table, bytes_resident) in self.tables {
201             deallocated_any = true;
202             unsafe {
203                 pool.tables
204                     .deallocate(allocation_index, table, bytes_resident);
205             }
206         }
207         #[cfg(feature = "async")]
208         for (stack, bytes_resident) in self.stacks {
209             deallocated_any = true;
210             unsafe {
211                 pool.stacks.deallocate(stack.0, bytes_resident);
212             }
213         }
214 
215         deallocated_any
216     }
217 }
218