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