1 use core::sync::atomic::Ordering; 2 3 use crate::{Engine, vm::PoolingInstanceAllocator}; 4 5 /// `PoolingAllocatorMetrics` provides access to runtime metrics of a pooling 6 /// allocator configured with [`crate::InstanceAllocationStrategy::Pooling`]. 7 /// 8 /// This is a cheap cloneable handle which can be obtained with 9 /// [`Engine::pooling_allocator_metrics`]. 10 #[derive(Clone)] 11 pub struct PoolingAllocatorMetrics { 12 engine: Engine, 13 } 14 15 impl PoolingAllocatorMetrics { new(engine: &Engine) -> Option<Self>16 pub(crate) fn new(engine: &Engine) -> Option<Self> { 17 engine.allocator().as_pooling().map(|_| Self { 18 engine: engine.clone(), 19 }) 20 } 21 22 /// Returns the number of core (module) instances currently allocated. core_instances(&self) -> u6423 pub fn core_instances(&self) -> u64 { 24 self.allocator().live_core_instances.load(Ordering::Relaxed) 25 } 26 27 /// Returns the number of component instances currently allocated. component_instances(&self) -> u6428 pub fn component_instances(&self) -> u64 { 29 self.allocator() 30 .live_component_instances 31 .load(Ordering::Relaxed) 32 } 33 34 /// Returns the number of WebAssembly memories currently allocated. memories(&self) -> usize35 pub fn memories(&self) -> usize { 36 self.allocator().live_memories.load(Ordering::Relaxed) 37 } 38 39 /// Returns the number of WebAssembly tables currently allocated. tables(&self) -> usize40 pub fn tables(&self) -> usize { 41 self.allocator().live_tables.load(Ordering::Relaxed) 42 } 43 44 /// Returns the number of WebAssembly stacks currently allocated. 45 #[cfg(feature = "async")] stacks(&self) -> usize46 pub fn stacks(&self) -> usize { 47 self.allocator().live_stacks.load(Ordering::Relaxed) 48 } 49 50 /// Returns the number of WebAssembly GC heaps currently allocated. 51 #[cfg(feature = "gc")] gc_heaps(&self) -> usize52 pub fn gc_heaps(&self) -> usize { 53 self.allocator().live_gc_heaps.load(Ordering::Relaxed) 54 } 55 56 /// Returns the number of slots for linear memories in this allocator which 57 /// are not currently in use but were previously used. 58 /// 59 /// A "warm" slot means that there was a previous instantiation of a memory 60 /// in that slot. Warm slots are favored in general for allocating new 61 /// memories over using a slot that has never been used before. unused_warm_memories(&self) -> u3262 pub fn unused_warm_memories(&self) -> u32 { 63 self.allocator().memories.unused_warm_slots() 64 } 65 66 /// Returns the number of bytes in this pooling allocator which are not part 67 /// of any in-used linear memory slot but were previously used and are kept 68 /// resident via the `*_keep_resident` configuration options. unused_memory_bytes_resident(&self) -> usize69 pub fn unused_memory_bytes_resident(&self) -> usize { 70 self.allocator().memories.unused_bytes_resident() 71 } 72 73 /// Returns the number of slots for tables in this allocator which are not 74 /// currently in use but were previously used. 75 /// 76 /// A "warm" slot means that there was a previous instantiation of a table 77 /// in that slot. Warm slots are favored in general for allocating new 78 /// tables over using a slot that has never been used before. unused_warm_tables(&self) -> u3279 pub fn unused_warm_tables(&self) -> u32 { 80 self.allocator().tables.unused_warm_slots() 81 } 82 83 /// Returns the number of bytes in this pooling allocator which are not part 84 /// of any in-used linear table slot but were previously used and are kept 85 /// resident via the `*_keep_resident` configuration options. unused_table_bytes_resident(&self) -> usize86 pub fn unused_table_bytes_resident(&self) -> usize { 87 self.allocator().tables.unused_bytes_resident() 88 } 89 90 /// Returns the number of slots for stacks in this allocator which are not 91 /// currently in use but were previously used. 92 /// 93 /// A "warm" slot means that there was a previous use of a stack 94 /// in that slot. Warm slots are favored in general for allocating new 95 /// stacks over using a slot that has never been used before. 96 #[cfg(feature = "async")] unused_warm_stacks(&self) -> u3297 pub fn unused_warm_stacks(&self) -> u32 { 98 self.allocator().stacks.unused_warm_slots() 99 } 100 101 /// Returns the number of bytes in this pooling allocator which are not part 102 /// of any in-used linear stack slot but were previously used and are kept 103 /// resident via the `*_keep_resident` configuration options. 104 /// 105 /// This returns `None` if the `async_stack_zeroing` option is disabled or 106 /// if the platform doesn't manage stacks (e.g. Windows returns `None`). 107 #[cfg(feature = "async")] unused_stack_bytes_resident(&self) -> Option<usize>108 pub fn unused_stack_bytes_resident(&self) -> Option<usize> { 109 self.allocator().stacks.unused_bytes_resident() 110 } 111 allocator(&self) -> &PoolingInstanceAllocator112 fn allocator(&self) -> &PoolingInstanceAllocator { 113 self.engine 114 .allocator() 115 .as_pooling() 116 .expect("engine should have pooling allocator") 117 } 118 } 119 120 #[cfg(test)] 121 mod tests { 122 use crate::vm::instance::allocator::pooling::StackPool; 123 use crate::{ 124 Config, Enabled, InstanceAllocationStrategy, Module, PoolingAllocationConfig, Result, 125 Store, 126 component::{Component, Linker}, 127 }; 128 use std::vec::Vec; 129 130 use super::*; 131 132 // A component with 1 core instance, 1 memory, 1 table 133 const TEST_COMPONENT: &[u8] = b" 134 (component 135 (core module $m 136 (memory 1) 137 (table 1 funcref) 138 ) 139 (core instance (instantiate (module $m))) 140 ) 141 "; 142 small_pool_config() -> PoolingAllocationConfig143 pub(crate) fn small_pool_config() -> PoolingAllocationConfig { 144 let mut config = PoolingAllocationConfig::new(); 145 146 config.total_memories(10); 147 config.max_memory_size(2 << 16); 148 config.total_tables(10); 149 config.table_elements(10); 150 config.total_stacks(1); 151 152 config 153 } 154 155 #[test] 156 #[cfg_attr(miri, ignore)] smoke_test()157 fn smoke_test() { 158 // Start with nothing 159 let engine = Engine::new(&Config::new().allocation_strategy(small_pool_config())).unwrap(); 160 let metrics = engine.pooling_allocator_metrics().unwrap(); 161 162 assert_eq!(metrics.core_instances(), 0); 163 assert_eq!(metrics.component_instances(), 0); 164 assert_eq!(metrics.memories(), 0); 165 assert_eq!(metrics.tables(), 0); 166 167 // Instantiate one of each 168 let mut store = Store::new(&engine, ()); 169 let component = Component::new(&engine, TEST_COMPONENT).unwrap(); 170 let linker = Linker::new(&engine); 171 let instance = linker.instantiate(&mut store, &component).unwrap(); 172 173 assert_eq!(metrics.core_instances(), 1); 174 assert_eq!(metrics.component_instances(), 1); 175 assert_eq!(metrics.memories(), 1); 176 assert_eq!(metrics.tables(), 1); 177 178 // Back to nothing 179 let _ = (instance, store); 180 181 assert_eq!(metrics.core_instances(), 0); 182 assert_eq!(metrics.component_instances(), 0); 183 assert_eq!(metrics.memories(), 0); 184 assert_eq!(metrics.tables(), 0); 185 } 186 187 #[test] test_non_pooling_allocator()188 fn test_non_pooling_allocator() { 189 let engine = 190 Engine::new(&Config::new().allocation_strategy(InstanceAllocationStrategy::OnDemand)) 191 .unwrap(); 192 193 let maybe_metrics = engine.pooling_allocator_metrics(); 194 assert!(maybe_metrics.is_none()); 195 } 196 197 #[test] 198 #[cfg_attr(any(miri, not(target_os = "linux")), ignore)] unused_memories_tables_and_more() -> Result<()>199 fn unused_memories_tables_and_more() -> Result<()> { 200 let mut pool = small_pool_config(); 201 pool.linear_memory_keep_resident(65536); 202 pool.table_keep_resident(65536); 203 pool.pagemap_scan(Enabled::Auto); 204 let mut config = Config::new(); 205 config.allocation_strategy(pool); 206 let engine = Engine::new(&config)?; 207 208 let metrics = engine.pooling_allocator_metrics().unwrap(); 209 let host_page_size = crate::vm::host_page_size(); 210 211 assert_eq!(metrics.memories(), 0); 212 assert_eq!(metrics.core_instances(), 0); 213 assert_eq!(metrics.component_instances(), 0); 214 assert_eq!(metrics.memories(), 0); 215 assert_eq!(metrics.tables(), 0); 216 assert_eq!(metrics.unused_warm_memories(), 0); 217 assert_eq!(metrics.unused_memory_bytes_resident(), 0); 218 assert_eq!(metrics.unused_warm_tables(), 0); 219 assert_eq!(metrics.unused_table_bytes_resident(), 0); 220 221 let m1 = Module::new( 222 &engine, 223 r#" 224 (module (memory (export "m") 1) (table 1 funcref)) 225 "#, 226 )?; 227 228 let mut store = Store::new(&engine, ()); 229 crate::Instance::new(&mut store, &m1, &[])?; 230 assert_eq!(metrics.memories(), 1); 231 assert_eq!(metrics.tables(), 1); 232 assert_eq!(metrics.core_instances(), 1); 233 assert_eq!(metrics.component_instances(), 0); 234 drop(store); 235 236 assert_eq!(metrics.memories(), 0); 237 assert_eq!(metrics.tables(), 0); 238 assert_eq!(metrics.core_instances(), 0); 239 assert_eq!(metrics.unused_warm_memories(), 1); 240 assert_eq!(metrics.unused_warm_tables(), 1); 241 if PoolingAllocationConfig::is_pagemap_scan_available() { 242 assert_eq!(metrics.unused_memory_bytes_resident(), 0); 243 assert_eq!(metrics.unused_table_bytes_resident(), host_page_size); 244 } else { 245 assert_eq!(metrics.unused_memory_bytes_resident(), 65536); 246 assert_eq!(metrics.unused_table_bytes_resident(), host_page_size); 247 } 248 249 let mut store = Store::new(&engine, ()); 250 let i = crate::Instance::new(&mut store, &m1, &[])?; 251 assert_eq!(metrics.memories(), 1); 252 assert_eq!(metrics.tables(), 1); 253 assert_eq!(metrics.core_instances(), 1); 254 assert_eq!(metrics.component_instances(), 0); 255 assert_eq!(metrics.unused_warm_memories(), 0); 256 assert_eq!(metrics.unused_warm_tables(), 0); 257 assert_eq!(metrics.unused_memory_bytes_resident(), 0); 258 assert_eq!(metrics.unused_table_bytes_resident(), 0); 259 let m = i.get_memory(&mut store, "m").unwrap(); 260 m.data_mut(&mut store)[0] = 1; 261 m.grow(&mut store, 1)?; 262 drop(store); 263 264 assert_eq!(metrics.memories(), 0); 265 assert_eq!(metrics.tables(), 0); 266 assert_eq!(metrics.core_instances(), 0); 267 assert_eq!(metrics.unused_warm_memories(), 1); 268 assert_eq!(metrics.unused_warm_tables(), 1); 269 if PoolingAllocationConfig::is_pagemap_scan_available() { 270 assert_eq!(metrics.unused_memory_bytes_resident(), host_page_size); 271 assert_eq!(metrics.unused_table_bytes_resident(), host_page_size); 272 } else { 273 assert_eq!(metrics.unused_memory_bytes_resident(), 65536); 274 assert_eq!(metrics.unused_table_bytes_resident(), host_page_size); 275 } 276 277 let stores = (0..10) 278 .map(|_| { 279 let mut store = Store::new(&engine, ()); 280 crate::Instance::new(&mut store, &m1, &[]).unwrap(); 281 store 282 }) 283 .collect::<Vec<_>>(); 284 285 assert_eq!(metrics.memories(), 10); 286 assert_eq!(metrics.tables(), 10); 287 assert_eq!(metrics.core_instances(), 10); 288 assert_eq!(metrics.unused_warm_memories(), 0); 289 assert_eq!(metrics.unused_warm_tables(), 0); 290 assert_eq!(metrics.unused_memory_bytes_resident(), 0); 291 assert_eq!(metrics.unused_table_bytes_resident(), 0); 292 293 drop(stores); 294 295 assert_eq!(metrics.memories(), 00); 296 assert_eq!(metrics.tables(), 00); 297 assert_eq!(metrics.core_instances(), 00); 298 assert_eq!(metrics.unused_warm_memories(), 10); 299 assert_eq!(metrics.unused_warm_tables(), 10); 300 if PoolingAllocationConfig::is_pagemap_scan_available() { 301 assert_eq!(metrics.unused_memory_bytes_resident(), host_page_size); 302 assert_eq!(metrics.unused_table_bytes_resident(), 10 * host_page_size); 303 } else { 304 assert_eq!(metrics.unused_memory_bytes_resident(), 10 * 65536); 305 assert_eq!(metrics.unused_table_bytes_resident(), 10 * host_page_size); 306 } 307 308 Ok(()) 309 } 310 311 #[test] 312 #[cfg_attr(miri, ignore)] gc_heaps() -> Result<()>313 fn gc_heaps() -> Result<()> { 314 let pool = small_pool_config(); 315 let mut config = Config::new(); 316 config.allocation_strategy(pool); 317 let engine = Engine::new(&config)?; 318 319 let metrics = engine.pooling_allocator_metrics().unwrap(); 320 321 assert_eq!(metrics.gc_heaps(), 0); 322 let mut store = Store::new(&engine, ()); 323 crate::ExternRef::new(&mut store, ())?; 324 assert_eq!(metrics.gc_heaps(), 1); 325 drop(store); 326 assert_eq!(metrics.gc_heaps(), 0); 327 328 Ok(()) 329 } 330 331 #[tokio::test] 332 #[cfg_attr(miri, ignore)] stacks() -> Result<()>333 async fn stacks() -> Result<()> { 334 let pool = small_pool_config(); 335 let mut config = Config::new(); 336 config.allocation_strategy(pool); 337 let engine = Engine::new(&config)?; 338 339 let metrics = engine.pooling_allocator_metrics().unwrap(); 340 341 assert_eq!(metrics.stacks(), 0); 342 assert_eq!(metrics.unused_warm_stacks(), 0); 343 let mut store = Store::new(&engine, ()); 344 345 crate::Func::wrap(&mut store, || {}) 346 .call_async(&mut store, &[], &mut []) 347 .await?; 348 assert_eq!(metrics.stacks(), 1); 349 drop(store); 350 assert_eq!(metrics.stacks(), 0); 351 assert_eq!(metrics.unused_stack_bytes_resident(), None); 352 if StackPool::enabled() { 353 assert_eq!(metrics.unused_warm_stacks(), 1); 354 } else { 355 assert_eq!(metrics.unused_warm_stacks(), 0); 356 } 357 358 Ok(()) 359 } 360 } 361