Skip to content

Commit becdee5

Browse files
authored
Add PoolingAllocatorMetrics (#11490)
This exposes some basic runtime metrics derived from the internal state of a `PoolingInstanceAllocator`. Two new atomics were added to PoolingInstanceAllocator: `live_memories` and `live_tables`. While these counts could be derived from existing state it would require acquiring mutexes on some inner state.
1 parent 25b5a56 commit becdee5

File tree

6 files changed

+191
-29
lines changed

6 files changed

+191
-29
lines changed

crates/wasmtime/src/engine.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -720,6 +720,13 @@ impl Engine {
720720
crate::runtime::vm::tls_eager_initialize();
721721
}
722722

723+
/// Returns a [`PoolingAllocatorMetrics`] if this engine was configured with
724+
/// [`InstanceAllocationStrategy::Pooling`].
725+
#[cfg(feature = "pooling-allocator")]
726+
pub fn pooling_allocator_metrics(&self) -> Option<crate::vm::PoolingAllocatorMetrics> {
727+
crate::runtime::vm::PoolingAllocatorMetrics::new(self)
728+
}
729+
723730
pub(crate) fn allocator(&self) -> &dyn crate::runtime::vm::InstanceAllocator {
724731
self.inner.allocator.as_ref()
725732
}

crates/wasmtime/src/runtime.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ pub use values::*;
9999
pub(crate) use uninhabited::*;
100100

101101
#[cfg(feature = "pooling-allocator")]
102-
pub use vm::PoolConcurrencyLimitError;
102+
pub use vm::{PoolConcurrencyLimitError, PoolingAllocatorMetrics};
103103

104104
#[cfg(feature = "profiling")]
105105
mod profiling;

crates/wasmtime/src/runtime/vm.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ pub use crate::runtime::vm::instance::{
105105
};
106106
#[cfg(feature = "pooling-allocator")]
107107
pub use crate::runtime::vm::instance::{
108-
InstanceLimits, PoolConcurrencyLimitError, PoolingInstanceAllocator,
108+
InstanceLimits, PoolConcurrencyLimitError, PoolingAllocatorMetrics, PoolingInstanceAllocator,
109109
PoolingInstanceAllocatorConfig,
110110
};
111111
pub use crate::runtime::vm::interpreter::*;

crates/wasmtime/src/runtime/vm/instance/allocator.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ pub use self::on_demand::OnDemandInstanceAllocator;
3030
mod pooling;
3131
#[cfg(feature = "pooling-allocator")]
3232
pub use self::pooling::{
33-
InstanceLimits, PoolConcurrencyLimitError, PoolingInstanceAllocator,
33+
InstanceLimits, PoolConcurrencyLimitError, PoolingAllocatorMetrics, PoolingInstanceAllocator,
3434
PoolingInstanceAllocatorConfig,
3535
};
3636

@@ -286,6 +286,12 @@ pub unsafe trait InstanceAllocator: Send + Sync {
286286

287287
/// Allow access to memory regions protected by any protection key.
288288
fn allow_all_pkeys(&self);
289+
290+
/// Returns `Some(&PoolingInstanceAllocator)` if this is one.
291+
#[cfg(feature = "pooling-allocator")]
292+
fn as_pooling(&self) -> Option<&PoolingInstanceAllocator> {
293+
None
294+
}
289295
}
290296

291297
impl dyn InstanceAllocator + '_ {

crates/wasmtime/src/runtime/vm/instance/allocator/pooling.rs

Lines changed: 60 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
mod decommit_queue;
2222
mod index_allocator;
2323
mod memory_pool;
24+
mod metrics;
2425
mod table_pool;
2526

2627
#[cfg(feature = "gc")]
@@ -54,6 +55,7 @@ use crate::runtime::vm::{
5455
mpk::{self, ProtectionKey, ProtectionMask},
5556
sys::vm::PageMap,
5657
};
58+
use core::sync::atomic::AtomicUsize;
5759
use std::borrow::Cow;
5860
use std::fmt::Display;
5961
use std::sync::{Mutex, MutexGuard};
@@ -65,6 +67,8 @@ use wasmtime_environ::{
6567
DefinedMemoryIndex, DefinedTableIndex, HostPtr, Module, Tunables, VMOffsets,
6668
};
6769

70+
pub use self::metrics::PoolingAllocatorMetrics;
71+
6872
#[cfg(feature = "gc")]
6973
use super::GcHeapAllocationIndex;
7074
#[cfg(feature = "gc")]
@@ -305,8 +309,12 @@ pub struct PoolingInstanceAllocator {
305309
live_component_instances: AtomicU64,
306310

307311
decommit_queue: Mutex<DecommitQueue>,
312+
308313
memories: MemoryPool,
314+
live_memories: AtomicUsize,
315+
309316
tables: TablePool,
317+
live_tables: AtomicUsize,
310318

311319
#[cfg(feature = "gc")]
312320
gc_heaps: GcHeapPool,
@@ -335,6 +343,8 @@ impl Drop for PoolingInstanceAllocator {
335343

336344
debug_assert_eq!(self.live_component_instances.load(Ordering::Acquire), 0);
337345
debug_assert_eq!(self.live_core_instances.load(Ordering::Acquire), 0);
346+
debug_assert_eq!(self.live_memories.load(Ordering::Acquire), 0);
347+
debug_assert_eq!(self.live_tables.load(Ordering::Acquire), 0);
338348

339349
debug_assert!(self.memories.is_empty());
340350
debug_assert!(self.tables.is_empty());
@@ -357,7 +367,9 @@ impl PoolingInstanceAllocator {
357367
live_core_instances: AtomicU64::new(0),
358368
decommit_queue: Mutex::new(DecommitQueue::default()),
359369
memories: MemoryPool::new(config, tunables)?,
370+
live_memories: AtomicUsize::new(0),
360371
tables: TablePool::new(config)?,
372+
live_tables: AtomicUsize::new(0),
361373
#[cfg(feature = "gc")]
362374
gc_heaps: GcHeapPool::new(config)?,
363375
#[cfg(feature = "async")]
@@ -652,23 +664,29 @@ unsafe impl InstanceAllocator for PoolingInstanceAllocator {
652664
ty: &wasmtime_environ::Memory,
653665
memory_index: Option<DefinedMemoryIndex>,
654666
) -> Result<(MemoryAllocationIndex, Memory)> {
655-
// FIXME(rust-lang/rust#145127) this should ideally use a version of
656-
// `with_flush_and_retry` but adapted for async closures instead of only
657-
// sync closures. Right now that won't compile though so this is the
658-
// manually expanded version of the method.
659-
let e = match self.memories.allocate(request, ty, memory_index).await {
660-
Ok(result) => return Ok(result),
661-
Err(e) => e,
662-
};
667+
async {
668+
// FIXME(rust-lang/rust#145127) this should ideally use a version of
669+
// `with_flush_and_retry` but adapted for async closures instead of only
670+
// sync closures. Right now that won't compile though so this is the
671+
// manually expanded version of the method.
672+
let e = match self.memories.allocate(request, ty, memory_index).await {
673+
Ok(result) => return Ok(result),
674+
Err(e) => e,
675+
};
663676

664-
if e.is::<PoolConcurrencyLimitError>() {
665-
let queue = self.decommit_queue.lock().unwrap();
666-
if self.flush_decommit_queue(queue) {
667-
return self.memories.allocate(request, ty, memory_index).await;
677+
if e.is::<PoolConcurrencyLimitError>() {
678+
let queue = self.decommit_queue.lock().unwrap();
679+
if self.flush_decommit_queue(queue) {
680+
return self.memories.allocate(request, ty, memory_index).await;
681+
}
668682
}
669-
}
670683

671-
Err(e)
684+
Err(e)
685+
}
686+
.await
687+
.inspect(|_| {
688+
self.live_memories.fetch_add(1, Ordering::Relaxed);
689+
})
672690
}
673691

674692
unsafe fn deallocate_memory(
@@ -677,6 +695,9 @@ unsafe impl InstanceAllocator for PoolingInstanceAllocator {
677695
allocation_index: MemoryAllocationIndex,
678696
memory: Memory,
679697
) {
698+
let prev = self.live_memories.fetch_sub(1, Ordering::Relaxed);
699+
debug_assert!(prev > 0);
700+
680701
// Reset the image slot. If there is any error clearing the
681702
// image, just drop it here, and let the drop handler for the
682703
// slot unmap in a way that retains the address space
@@ -712,21 +733,27 @@ unsafe impl InstanceAllocator for PoolingInstanceAllocator {
712733
ty: &wasmtime_environ::Table,
713734
_table_index: DefinedTableIndex,
714735
) -> Result<(super::TableAllocationIndex, Table)> {
715-
// FIXME: see `allocate_memory` above for comments about duplication
716-
// with `with_flush_and_retry`.
717-
let e = match self.tables.allocate(request, ty).await {
718-
Ok(result) => return Ok(result),
719-
Err(e) => e,
720-
};
736+
async {
737+
// FIXME: see `allocate_memory` above for comments about duplication
738+
// with `with_flush_and_retry`.
739+
let e = match self.tables.allocate(request, ty).await {
740+
Ok(result) => return Ok(result),
741+
Err(e) => e,
742+
};
721743

722-
if e.is::<PoolConcurrencyLimitError>() {
723-
let queue = self.decommit_queue.lock().unwrap();
724-
if self.flush_decommit_queue(queue) {
725-
return self.tables.allocate(request, ty).await;
744+
if e.is::<PoolConcurrencyLimitError>() {
745+
let queue = self.decommit_queue.lock().unwrap();
746+
if self.flush_decommit_queue(queue) {
747+
return self.tables.allocate(request, ty).await;
748+
}
726749
}
727-
}
728750

729-
Err(e)
751+
Err(e)
752+
}
753+
.await
754+
.inspect(|_| {
755+
self.live_tables.fetch_add(1, Ordering::Relaxed);
756+
})
730757
}
731758

732759
unsafe fn deallocate_table(
@@ -735,6 +762,9 @@ unsafe impl InstanceAllocator for PoolingInstanceAllocator {
735762
allocation_index: TableAllocationIndex,
736763
mut table: Table,
737764
) {
765+
let prev = self.live_tables.fetch_sub(1, Ordering::Relaxed);
766+
debug_assert!(prev > 0);
767+
738768
let mut queue = DecommitQueue::default();
739769
// SAFETY: This table is no longer in use by the allocator when this
740770
// method is called and additionally all image ranges are pushed with
@@ -816,6 +846,10 @@ unsafe impl InstanceAllocator for PoolingInstanceAllocator {
816846
) -> (MemoryAllocationIndex, Memory) {
817847
self.gc_heaps.deallocate(allocation_index, gc_heap)
818848
}
849+
850+
fn as_pooling(&self) -> Option<&PoolingInstanceAllocator> {
851+
Some(self)
852+
}
819853
}
820854

821855
#[cfg(test)]
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
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 {
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.
23+
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.
28+
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.
35+
pub fn memories(&self) -> usize {
36+
self.allocator().live_memories.load(Ordering::Relaxed)
37+
}
38+
39+
/// Returns the number of WebAssembly tables currently allocated.
40+
pub fn tables(&self) -> usize {
41+
self.allocator().live_tables.load(Ordering::Relaxed)
42+
}
43+
44+
fn allocator(&self) -> &PoolingInstanceAllocator {
45+
self.engine
46+
.allocator()
47+
.as_pooling()
48+
.expect("engine should have pooling allocator")
49+
}
50+
}
51+
52+
#[cfg(test)]
53+
mod tests {
54+
use crate::{
55+
Config, InstanceAllocationStrategy, Store,
56+
component::{Component, Linker},
57+
};
58+
59+
use super::*;
60+
61+
// A component with 1 core instance, 1 memory, 1 table
62+
const TEST_COMPONENT: &[u8] = b"
63+
(component
64+
(core module $m
65+
(memory 1)
66+
(table 1 funcref)
67+
)
68+
(core instance (instantiate (module $m)))
69+
)
70+
";
71+
72+
#[test]
73+
#[cfg_attr(miri, ignore)]
74+
fn smoke_test() {
75+
// Start with nothing
76+
let engine =
77+
Engine::new(&Config::new().allocation_strategy(InstanceAllocationStrategy::pooling()))
78+
.unwrap();
79+
let metrics = engine.pooling_allocator_metrics().unwrap();
80+
81+
assert_eq!(metrics.core_instances(), 0);
82+
assert_eq!(metrics.component_instances(), 0);
83+
assert_eq!(metrics.memories(), 0);
84+
assert_eq!(metrics.tables(), 0);
85+
86+
// Instantiate one of each
87+
let mut store = Store::new(&engine, ());
88+
let component = Component::new(&engine, TEST_COMPONENT).unwrap();
89+
let linker = Linker::new(&engine);
90+
let instance = linker.instantiate(&mut store, &component).unwrap();
91+
92+
assert_eq!(metrics.core_instances(), 1);
93+
assert_eq!(metrics.component_instances(), 1);
94+
assert_eq!(metrics.memories(), 1);
95+
assert_eq!(metrics.tables(), 1);
96+
97+
// Back to nothing
98+
let _ = (instance, store);
99+
100+
assert_eq!(metrics.core_instances(), 0);
101+
assert_eq!(metrics.component_instances(), 0);
102+
assert_eq!(metrics.memories(), 0);
103+
assert_eq!(metrics.tables(), 0);
104+
}
105+
106+
#[test]
107+
fn test_non_pooling_allocator() {
108+
let engine =
109+
Engine::new(&Config::new().allocation_strategy(InstanceAllocationStrategy::OnDemand))
110+
.unwrap();
111+
112+
let maybe_metrics = engine.pooling_allocator_metrics();
113+
assert!(maybe_metrics.is_none());
114+
}
115+
}

0 commit comments

Comments
 (0)