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