1 //! Implements the pooling instance allocator. 2 //! 3 //! The pooling instance allocator maps memory in advance and allocates 4 //! instances, memories, tables, and stacks from a pool of available resources. 5 //! Using the pooling instance allocator can speed up module instantiation when 6 //! modules can be constrained based on configurable limits 7 //! ([`InstanceLimits`]). Each new instance is stored in a "slot"; as instances 8 //! are allocated and freed, these slots are either filled or emptied: 9 //! 10 //! ```text 11 //! ┌──────┬──────┬──────┬──────┬──────┐ 12 //! │Slot 0│Slot 1│Slot 2│Slot 3│......│ 13 //! └──────┴──────┴──────┴──────┴──────┘ 14 //! ``` 15 //! 16 //! Each slot has a "slot ID"--an index into the pool. Slot IDs are handed out 17 //! by the [`index_allocator`] module. Note that each kind of pool-allocated 18 //! item is stored in its own separate pool: [`memory_pool`], [`table_pool`], 19 //! [`stack_pool`]. See those modules for more details. 20 21 mod index_allocator; 22 mod memory_pool; 23 mod table_pool; 24 25 #[cfg(feature = "gc")] 26 mod gc_heap_pool; 27 28 #[cfg(all(feature = "async"))] 29 mod generic_stack_pool; 30 #[cfg(all(feature = "async", unix, not(miri)))] 31 mod unix_stack_pool; 32 33 #[cfg(all(feature = "async"))] 34 cfg_if::cfg_if! { 35 if #[cfg(all(unix, not(miri), not(asan)))] { 36 use unix_stack_pool as stack_pool; 37 } else { 38 use generic_stack_pool as stack_pool; 39 } 40 } 41 42 use super::{ 43 InstanceAllocationRequest, InstanceAllocatorImpl, MemoryAllocationIndex, TableAllocationIndex, 44 }; 45 use crate::runtime::vm::{ 46 instance::Instance, 47 mpk::{self, MpkEnabled, ProtectionKey, ProtectionMask}, 48 CompiledModuleId, Memory, Table, 49 }; 50 use anyhow::{bail, Result}; 51 use memory_pool::MemoryPool; 52 use std::{ 53 mem, 54 sync::atomic::{AtomicU64, Ordering}, 55 }; 56 use table_pool::TablePool; 57 use wasmtime_environ::{ 58 DefinedMemoryIndex, DefinedTableIndex, HostPtr, MemoryPlan, Module, TablePlan, Tunables, 59 VMOffsets, 60 }; 61 62 #[cfg(feature = "gc")] 63 use super::GcHeapAllocationIndex; 64 #[cfg(feature = "gc")] 65 use crate::runtime::vm::{GcHeap, GcRuntime}; 66 #[cfg(feature = "gc")] 67 use gc_heap_pool::GcHeapPool; 68 69 #[cfg(feature = "async")] 70 use stack_pool::StackPool; 71 72 #[cfg(feature = "component-model")] 73 use wasmtime_environ::{ 74 component::{Component, VMComponentOffsets}, 75 StaticModuleIndex, 76 }; 77 78 fn round_up_to_pow2(n: usize, to: usize) -> usize { 79 debug_assert!(to > 0); 80 debug_assert!(to.is_power_of_two()); 81 (n + to - 1) & !(to - 1) 82 } 83 84 /// Instance-related limit configuration for pooling. 85 /// 86 /// More docs on this can be found at `wasmtime::PoolingAllocationConfig`. 87 #[derive(Debug, Copy, Clone)] 88 pub struct InstanceLimits { 89 /// The maximum number of component instances that may be allocated 90 /// concurrently. 91 pub total_component_instances: u32, 92 93 /// The maximum size of a component's `VMComponentContext`, not including 94 /// any of its inner core modules' `VMContext` sizes. 95 pub component_instance_size: usize, 96 97 /// The maximum number of core module instances that may be allocated 98 /// concurrently. 99 pub total_core_instances: u32, 100 101 /// The maximum number of core module instances that a single component may 102 /// transitively contain. 103 pub max_core_instances_per_component: u32, 104 105 /// The maximum number of Wasm linear memories that a component may 106 /// transitively contain. 107 pub max_memories_per_component: u32, 108 109 /// The maximum number of tables that a component may transitively contain. 110 pub max_tables_per_component: u32, 111 112 /// The total number of linear memories in the pool, across all instances. 113 pub total_memories: u32, 114 115 /// The total number of tables in the pool, across all instances. 116 pub total_tables: u32, 117 118 /// The total number of async stacks in the pool, across all instances. 119 #[cfg(feature = "async")] 120 pub total_stacks: u32, 121 122 /// Maximum size of a core instance's `VMContext`. 123 pub core_instance_size: usize, 124 125 /// Maximum number of tables per instance. 126 pub max_tables_per_module: u32, 127 128 /// Maximum number of table elements per table. 129 pub table_elements: u32, 130 131 /// Maximum number of linear memories per instance. 132 pub max_memories_per_module: u32, 133 134 /// Maximum number of Wasm pages for each linear memory. 135 pub memory_pages: u64, 136 137 /// The total number of GC heaps in the pool, across all instances. 138 #[cfg(feature = "gc")] 139 pub total_gc_heaps: u32, 140 } 141 142 impl Default for InstanceLimits { 143 fn default() -> Self { 144 // See doc comments for `wasmtime::PoolingAllocationConfig` for these 145 // default values 146 Self { 147 total_component_instances: 1000, 148 component_instance_size: 1 << 20, // 1 MiB 149 total_core_instances: 1000, 150 max_core_instances_per_component: 20, 151 max_memories_per_component: 20, 152 max_tables_per_component: 20, 153 total_memories: 1000, 154 total_tables: 1000, 155 #[cfg(feature = "async")] 156 total_stacks: 1000, 157 core_instance_size: 1 << 20, // 1 MiB 158 max_tables_per_module: 1, 159 table_elements: 10_000, 160 max_memories_per_module: 1, 161 memory_pages: 160, 162 #[cfg(feature = "gc")] 163 total_gc_heaps: 1000, 164 } 165 } 166 } 167 168 /// Configuration options for the pooling instance allocator supplied at 169 /// construction. 170 #[derive(Copy, Clone, Debug)] 171 pub struct PoolingInstanceAllocatorConfig { 172 /// See `PoolingAllocatorConfig::max_unused_warm_slots` in `wasmtime` 173 pub max_unused_warm_slots: u32, 174 /// The size, in bytes, of async stacks to allocate (not including the guard 175 /// page). 176 pub stack_size: usize, 177 /// The limits to apply to instances allocated within this allocator. 178 pub limits: InstanceLimits, 179 /// Whether or not async stacks are zeroed after use. 180 pub async_stack_zeroing: bool, 181 /// If async stack zeroing is enabled and the host platform is Linux this is 182 /// how much memory to zero out with `memset`. 183 /// 184 /// The rest of memory will be zeroed out with `madvise`. 185 pub async_stack_keep_resident: usize, 186 /// How much linear memory, in bytes, to keep resident after resetting for 187 /// use with the next instance. This much memory will be `memset` to zero 188 /// when a linear memory is deallocated. 189 /// 190 /// Memory exceeding this amount in the wasm linear memory will be released 191 /// with `madvise` back to the kernel. 192 /// 193 /// Only applicable on Linux. 194 pub linear_memory_keep_resident: usize, 195 /// Same as `linear_memory_keep_resident` but for tables. 196 pub table_keep_resident: usize, 197 /// Whether to enable memory protection keys. 198 pub memory_protection_keys: MpkEnabled, 199 /// How many memory protection keys to allocate. 200 pub max_memory_protection_keys: usize, 201 } 202 203 impl Default for PoolingInstanceAllocatorConfig { 204 fn default() -> PoolingInstanceAllocatorConfig { 205 PoolingInstanceAllocatorConfig { 206 max_unused_warm_slots: 100, 207 stack_size: 2 << 20, 208 limits: InstanceLimits::default(), 209 async_stack_zeroing: false, 210 async_stack_keep_resident: 0, 211 linear_memory_keep_resident: 0, 212 table_keep_resident: 0, 213 memory_protection_keys: MpkEnabled::Disable, 214 max_memory_protection_keys: 16, 215 } 216 } 217 } 218 219 /// Implements the pooling instance allocator. 220 /// 221 /// This allocator internally maintains pools of instances, memories, tables, 222 /// and stacks. 223 /// 224 /// Note: the resource pools are manually dropped so that the fault handler 225 /// terminates correctly. 226 #[derive(Debug)] 227 pub struct PoolingInstanceAllocator { 228 limits: InstanceLimits, 229 230 // The number of live core module and component instances at any given 231 // time. Note that this can temporarily go over the configured limit. This 232 // doesn't mean we have actually overshot, but that we attempted to allocate 233 // a new instance and incremented the counter, we've seen (or are about to 234 // see) that the counter is beyond the configured threshold, and are going 235 // to decrement the counter and return an error but haven't done so yet. See 236 // the increment trait methods for more details. 237 live_core_instances: AtomicU64, 238 live_component_instances: AtomicU64, 239 240 memories: MemoryPool, 241 tables: TablePool, 242 243 #[cfg(feature = "gc")] 244 gc_heaps: GcHeapPool, 245 246 #[cfg(feature = "async")] 247 stacks: StackPool, 248 } 249 250 impl Drop for PoolingInstanceAllocator { 251 fn drop(&mut self) { 252 debug_assert_eq!(self.live_component_instances.load(Ordering::Acquire), 0); 253 debug_assert_eq!(self.live_core_instances.load(Ordering::Acquire), 0); 254 255 debug_assert!(self.memories.is_empty()); 256 debug_assert!(self.tables.is_empty()); 257 258 #[cfg(feature = "gc")] 259 debug_assert!(self.gc_heaps.is_empty()); 260 261 #[cfg(feature = "async")] 262 debug_assert!(self.stacks.is_empty()); 263 } 264 } 265 266 impl PoolingInstanceAllocator { 267 /// Creates a new pooling instance allocator with the given strategy and limits. 268 pub fn new(config: &PoolingInstanceAllocatorConfig, tunables: &Tunables) -> Result<Self> { 269 Ok(Self { 270 limits: config.limits, 271 live_component_instances: AtomicU64::new(0), 272 live_core_instances: AtomicU64::new(0), 273 memories: MemoryPool::new(config, tunables)?, 274 tables: TablePool::new(config)?, 275 #[cfg(feature = "gc")] 276 gc_heaps: GcHeapPool::new(config)?, 277 #[cfg(feature = "async")] 278 stacks: StackPool::new(config)?, 279 }) 280 } 281 282 fn core_instance_size(&self) -> usize { 283 round_up_to_pow2(self.limits.core_instance_size, mem::align_of::<Instance>()) 284 } 285 286 fn validate_table_plans(&self, module: &Module) -> Result<()> { 287 self.tables.validate(module) 288 } 289 290 fn validate_memory_plans(&self, module: &Module) -> Result<()> { 291 self.memories.validate(module) 292 } 293 294 fn validate_core_instance_size(&self, offsets: &VMOffsets<HostPtr>) -> Result<()> { 295 let layout = Instance::alloc_layout(offsets); 296 if layout.size() <= self.core_instance_size() { 297 return Ok(()); 298 } 299 300 // If this `module` exceeds the allocation size allotted to it then an 301 // error will be reported here. The error of "required N bytes but 302 // cannot allocate that" is pretty opaque, however, because it's not 303 // clear what the breakdown of the N bytes are and what to optimize 304 // next. To help provide a better error message here some fancy-ish 305 // logic is done here to report the breakdown of the byte request into 306 // the largest portions and where it's coming from. 307 let mut message = format!( 308 "instance allocation for this module \ 309 requires {} bytes which exceeds the configured maximum \ 310 of {} bytes; breakdown of allocation requirement:\n\n", 311 layout.size(), 312 self.core_instance_size(), 313 ); 314 315 let mut remaining = layout.size(); 316 let mut push = |name: &str, bytes: usize| { 317 assert!(remaining >= bytes); 318 remaining -= bytes; 319 320 // If the `name` region is more than 5% of the allocation request 321 // then report it here, otherwise ignore it. We have less than 20 322 // fields so we're guaranteed that something should be reported, and 323 // otherwise it's not particularly interesting to learn about 5 324 // different fields that are all 8 or 0 bytes. Only try to report 325 // the "major" sources of bytes here. 326 if bytes > layout.size() / 20 { 327 message.push_str(&format!( 328 " * {:.02}% - {} bytes - {}\n", 329 ((bytes as f32) / (layout.size() as f32)) * 100.0, 330 bytes, 331 name, 332 )); 333 } 334 }; 335 336 // The `Instance` itself requires some size allocated to it. 337 push("instance state management", mem::size_of::<Instance>()); 338 339 // Afterwards the `VMContext`'s regions are why we're requesting bytes, 340 // so ask it for descriptions on each region's byte size. 341 for (desc, size) in offsets.region_sizes() { 342 push(desc, size as usize); 343 } 344 345 // double-check we accounted for all the bytes 346 assert_eq!(remaining, 0); 347 348 bail!("{}", message) 349 } 350 351 #[cfg(feature = "component-model")] 352 fn validate_component_instance_size( 353 &self, 354 offsets: &VMComponentOffsets<HostPtr>, 355 ) -> Result<()> { 356 if usize::try_from(offsets.size_of_vmctx()).unwrap() <= self.limits.component_instance_size 357 { 358 return Ok(()); 359 } 360 361 // TODO: Add context with detailed accounting of what makes up all the 362 // `VMComponentContext`'s space like we do for module instances. 363 bail!( 364 "instance allocation for this component requires {} bytes of `VMComponentContext` \ 365 space which exceeds the configured maximum of {} bytes", 366 offsets.size_of_vmctx(), 367 self.limits.component_instance_size 368 ) 369 } 370 } 371 372 unsafe impl InstanceAllocatorImpl for PoolingInstanceAllocator { 373 #[cfg(feature = "component-model")] 374 fn validate_component_impl<'a>( 375 &self, 376 component: &Component, 377 offsets: &VMComponentOffsets<HostPtr>, 378 get_module: &'a dyn Fn(StaticModuleIndex) -> &'a Module, 379 ) -> Result<()> { 380 self.validate_component_instance_size(offsets)?; 381 382 let mut num_core_instances = 0; 383 let mut num_memories = 0; 384 let mut num_tables = 0; 385 for init in &component.initializers { 386 use wasmtime_environ::component::GlobalInitializer::*; 387 use wasmtime_environ::component::InstantiateModule; 388 match init { 389 InstantiateModule(InstantiateModule::Import(_, _)) => { 390 num_core_instances += 1; 391 // Can't statically account for the total vmctx size, number 392 // of memories, and number of tables in this component. 393 } 394 InstantiateModule(InstantiateModule::Static(static_module_index, _)) => { 395 let module = get_module(*static_module_index); 396 let offsets = VMOffsets::new(HostPtr, &module); 397 self.validate_module_impl(module, &offsets)?; 398 num_core_instances += 1; 399 num_memories += module.memory_plans.len() - module.num_imported_memories; 400 num_tables += module.table_plans.len() - module.num_imported_tables; 401 } 402 LowerImport { .. } 403 | ExtractMemory(_) 404 | ExtractRealloc(_) 405 | ExtractPostReturn(_) 406 | Resource(_) => {} 407 } 408 } 409 410 if num_core_instances 411 > usize::try_from(self.limits.max_core_instances_per_component).unwrap() 412 { 413 bail!( 414 "The component transitively contains {num_core_instances} core module instances, \ 415 which exceeds the configured maximum of {}", 416 self.limits.max_core_instances_per_component 417 ); 418 } 419 420 if num_memories > usize::try_from(self.limits.max_memories_per_component).unwrap() { 421 bail!( 422 "The component transitively contains {num_memories} Wasm linear memories, which \ 423 exceeds the configured maximum of {}", 424 self.limits.max_memories_per_component 425 ); 426 } 427 428 if num_tables > usize::try_from(self.limits.max_tables_per_component).unwrap() { 429 bail!( 430 "The component transitively contains {num_tables} tables, which exceeds the \ 431 configured maximum of {}", 432 self.limits.max_tables_per_component 433 ); 434 } 435 436 Ok(()) 437 } 438 439 fn validate_module_impl(&self, module: &Module, offsets: &VMOffsets<HostPtr>) -> Result<()> { 440 self.validate_memory_plans(module)?; 441 self.validate_table_plans(module)?; 442 self.validate_core_instance_size(offsets)?; 443 Ok(()) 444 } 445 446 fn increment_component_instance_count(&self) -> Result<()> { 447 let old_count = self.live_component_instances.fetch_add(1, Ordering::AcqRel); 448 if old_count >= u64::from(self.limits.total_component_instances) { 449 self.decrement_component_instance_count(); 450 bail!( 451 "maximum concurrent component instance limit of {} reached", 452 self.limits.total_component_instances 453 ); 454 } 455 Ok(()) 456 } 457 458 fn decrement_component_instance_count(&self) { 459 self.live_component_instances.fetch_sub(1, Ordering::AcqRel); 460 } 461 462 fn increment_core_instance_count(&self) -> Result<()> { 463 let old_count = self.live_core_instances.fetch_add(1, Ordering::AcqRel); 464 if old_count >= u64::from(self.limits.total_core_instances) { 465 self.decrement_core_instance_count(); 466 bail!( 467 "maximum concurrent core instance limit of {} reached", 468 self.limits.total_core_instances 469 ); 470 } 471 Ok(()) 472 } 473 474 fn decrement_core_instance_count(&self) { 475 self.live_core_instances.fetch_sub(1, Ordering::AcqRel); 476 } 477 478 unsafe fn allocate_memory( 479 &self, 480 request: &mut InstanceAllocationRequest, 481 memory_plan: &MemoryPlan, 482 memory_index: DefinedMemoryIndex, 483 ) -> Result<(MemoryAllocationIndex, Memory)> { 484 self.memories.allocate(request, memory_plan, memory_index) 485 } 486 487 unsafe fn deallocate_memory( 488 &self, 489 _memory_index: DefinedMemoryIndex, 490 allocation_index: MemoryAllocationIndex, 491 memory: Memory, 492 ) { 493 self.memories.deallocate(allocation_index, memory); 494 } 495 496 unsafe fn allocate_table( 497 &self, 498 request: &mut InstanceAllocationRequest, 499 table_plan: &TablePlan, 500 _table_index: DefinedTableIndex, 501 ) -> Result<(super::TableAllocationIndex, Table)> { 502 self.tables.allocate(request, table_plan) 503 } 504 505 unsafe fn deallocate_table( 506 &self, 507 _table_index: DefinedTableIndex, 508 allocation_index: TableAllocationIndex, 509 table: Table, 510 ) { 511 self.tables.deallocate(allocation_index, table); 512 } 513 514 #[cfg(feature = "async")] 515 fn allocate_fiber_stack(&self) -> Result<wasmtime_fiber::FiberStack> { 516 self.stacks.allocate() 517 } 518 519 #[cfg(feature = "async")] 520 unsafe fn deallocate_fiber_stack(&self, stack: &wasmtime_fiber::FiberStack) { 521 self.stacks.deallocate(stack); 522 } 523 524 fn purge_module(&self, module: CompiledModuleId) { 525 self.memories.purge_module(module); 526 } 527 528 fn next_available_pkey(&self) -> Option<ProtectionKey> { 529 self.memories.next_available_pkey() 530 } 531 532 fn restrict_to_pkey(&self, pkey: ProtectionKey) { 533 mpk::allow(ProtectionMask::zero().or(pkey)); 534 } 535 536 fn allow_all_pkeys(&self) { 537 mpk::allow(ProtectionMask::all()); 538 } 539 540 #[cfg(feature = "gc")] 541 fn allocate_gc_heap( 542 &self, 543 gc_runtime: &dyn GcRuntime, 544 ) -> Result<(GcHeapAllocationIndex, Box<dyn GcHeap>)> { 545 self.gc_heaps.allocate(gc_runtime) 546 } 547 548 #[cfg(feature = "gc")] 549 fn deallocate_gc_heap( 550 &self, 551 allocation_index: GcHeapAllocationIndex, 552 gc_heap: Box<dyn GcHeap>, 553 ) { 554 self.gc_heaps.deallocate(allocation_index, gc_heap); 555 } 556 } 557 558 #[cfg(test)] 559 mod test { 560 use super::*; 561 562 #[test] 563 fn test_pooling_allocator_with_memory_pages_exceeded() { 564 let config = PoolingInstanceAllocatorConfig { 565 limits: InstanceLimits { 566 total_memories: 1, 567 memory_pages: 0x10001, 568 ..Default::default() 569 }, 570 ..PoolingInstanceAllocatorConfig::default() 571 }; 572 assert_eq!( 573 PoolingInstanceAllocator::new( 574 &config, 575 &Tunables { 576 static_memory_bound: 1, 577 ..Tunables::default_host() 578 }, 579 ) 580 .map_err(|e| e.to_string()) 581 .expect_err("expected a failure constructing instance allocator"), 582 "module memory page limit of 65537 exceeds the maximum of 65536" 583 ); 584 } 585 586 #[cfg(all(unix, target_pointer_width = "64", feature = "async", not(miri)))] 587 #[test] 588 fn test_stack_zeroed() -> Result<()> { 589 let config = PoolingInstanceAllocatorConfig { 590 max_unused_warm_slots: 0, 591 limits: InstanceLimits { 592 total_stacks: 1, 593 total_memories: 0, 594 total_tables: 0, 595 ..Default::default() 596 }, 597 stack_size: 128, 598 async_stack_zeroing: true, 599 ..PoolingInstanceAllocatorConfig::default() 600 }; 601 let allocator = PoolingInstanceAllocator::new(&config, &Tunables::default_host())?; 602 603 unsafe { 604 for _ in 0..255 { 605 let stack = allocator.allocate_fiber_stack()?; 606 607 // The stack pointer is at the top, so decrement it first 608 let addr = stack.top().unwrap().sub(1); 609 610 assert_eq!(*addr, 0); 611 *addr = 1; 612 613 allocator.deallocate_fiber_stack(&stack); 614 } 615 } 616 617 Ok(()) 618 } 619 620 #[cfg(all(unix, target_pointer_width = "64", feature = "async", not(miri)))] 621 #[test] 622 fn test_stack_unzeroed() -> Result<()> { 623 let config = PoolingInstanceAllocatorConfig { 624 max_unused_warm_slots: 0, 625 limits: InstanceLimits { 626 total_stacks: 1, 627 total_memories: 0, 628 total_tables: 0, 629 ..Default::default() 630 }, 631 stack_size: 128, 632 async_stack_zeroing: false, 633 ..PoolingInstanceAllocatorConfig::default() 634 }; 635 let allocator = PoolingInstanceAllocator::new(&config, &Tunables::default_host())?; 636 637 unsafe { 638 for i in 0..255 { 639 let stack = allocator.allocate_fiber_stack()?; 640 641 // The stack pointer is at the top, so decrement it first 642 let addr = stack.top().unwrap().sub(1); 643 644 assert_eq!(*addr, i); 645 *addr = i + 1; 646 647 allocator.deallocate_fiber_stack(&stack); 648 } 649 } 650 651 Ok(()) 652 } 653 } 654