Skip to main content

wasmtime/runtime/vm/instance/allocator/pooling/
metrics.rs

1use core::sync::atomic::Ordering;
2
3use 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)]
11pub struct PoolingAllocatorMetrics {
12    engine: Engine,
13}
14
15impl 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    /// Returns the number of WebAssembly stacks currently allocated.
45    #[cfg(feature = "async")]
46    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")]
52    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.
62    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.
69    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.
79    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.
86    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")]
97    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")]
108    pub fn unused_stack_bytes_resident(&self) -> Option<usize> {
109        self.allocator().stacks.unused_bytes_resident()
110    }
111
112    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)]
121mod 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
143    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    /// A module whose memory carries data, so an incorrect reset shows up as
156    /// wrong bytes rather than only as a metric.
157    const TEST_MEMORY_MODULE: &str = r#"
158        (module
159            (memory (export "memory") 1)
160            (data (i32.const 0) "init")
161        )
162    "#;
163
164    /// This test verifies both that memory is freed, and that instantiation
165    /// still works correctly afterward.
166    #[test]
167    #[cfg_attr(miri, ignore)]
168    fn release_idle_pool_memory_frees_unused_slots() -> Result<()> {
169        let mut pool = small_pool_config();
170        pool.linear_memory_keep_resident(1 << 16);
171        let engine = Engine::new(&Config::new().allocation_strategy(pool))?;
172        let metrics = engine.pooling_allocator_metrics().unwrap();
173        let module = Module::new(&engine, TEST_MEMORY_MODULE)?;
174
175        // Dirty a slot and give it back: the pool keeps part of it resident.
176        {
177            let mut store = Store::new(&engine, ());
178            let instance = crate::Instance::new(&mut store, &module, &[])?;
179            let memory = instance.get_memory(&mut store, "memory").unwrap();
180            memory.data_mut(&mut store)[4..8].copy_from_slice(b"dirt");
181        }
182
183        // On platforms whose decommit resets to zero (`DecommitBehavior::Zero`)
184        // a freed slot keeps nothing resident, so there is nothing to release
185        // and only the contents check below applies.
186        let resident = metrics.unused_memory_bytes_resident();
187        if resident > 0 {
188            let released = engine.release_idle_pool_memory();
189            assert!(
190                released > 0,
191                "{resident} bytes were resident and none were released"
192            );
193            assert_eq!(
194                metrics.unused_memory_bytes_resident(),
195                0,
196                "a released slot still reports resident bytes"
197            );
198            // The slot is allocatable again, not lost.
199            assert_eq!(metrics.unused_warm_memories(), 1);
200        }
201
202        // The slot still reads as its module defines it: the released region
203        // is restored by the mapping, and the previous instance's write is
204        // gone because the slot was reset when it was freed.
205        let mut store = Store::new(&engine, ());
206        let instance = crate::Instance::new(&mut store, &module, &[])?;
207        let memory = instance.get_memory(&mut store, "memory").unwrap();
208        assert_eq!(&memory.data(&store)[..8], b"init\0\0\0\0");
209
210        // Releasing with nothing unused is a no-op rather than an error.
211        assert_eq!(engine.release_idle_pool_memory(), 0);
212        Ok(())
213    }
214
215    #[test]
216    #[cfg_attr(miri, ignore)]
217    fn smoke_test() {
218        // Start with nothing
219        let engine = Engine::new(&Config::new().allocation_strategy(small_pool_config())).unwrap();
220        let metrics = engine.pooling_allocator_metrics().unwrap();
221
222        assert_eq!(metrics.core_instances(), 0);
223        assert_eq!(metrics.component_instances(), 0);
224        assert_eq!(metrics.memories(), 0);
225        assert_eq!(metrics.tables(), 0);
226
227        // Instantiate one of each
228        let mut store = Store::new(&engine, ());
229        let component = Component::new(&engine, TEST_COMPONENT).unwrap();
230        let linker = Linker::new(&engine);
231        let instance = linker.instantiate(&mut store, &component).unwrap();
232
233        assert_eq!(metrics.core_instances(), 1);
234        assert_eq!(metrics.component_instances(), 1);
235        assert_eq!(metrics.memories(), 1);
236        assert_eq!(metrics.tables(), 1);
237
238        // Back to nothing
239        let _ = (instance, store);
240
241        assert_eq!(metrics.core_instances(), 0);
242        assert_eq!(metrics.component_instances(), 0);
243        assert_eq!(metrics.memories(), 0);
244        assert_eq!(metrics.tables(), 0);
245    }
246
247    #[test]
248    fn test_non_pooling_allocator() {
249        let engine =
250            Engine::new(&Config::new().allocation_strategy(InstanceAllocationStrategy::OnDemand))
251                .unwrap();
252
253        let maybe_metrics = engine.pooling_allocator_metrics();
254        assert!(maybe_metrics.is_none());
255    }
256
257    #[test]
258    #[cfg_attr(any(miri, not(target_os = "linux")), ignore)]
259    fn unused_memories_tables_and_more() -> Result<()> {
260        let mut pool = small_pool_config();
261        pool.linear_memory_keep_resident(65536);
262        pool.table_keep_resident(65536);
263        pool.pagemap_scan(Enabled::Auto);
264        let mut config = Config::new();
265        config.allocation_strategy(pool);
266        let engine = Engine::new(&config)?;
267
268        let metrics = engine.pooling_allocator_metrics().unwrap();
269        let host_page_size = crate::vm::host_page_size();
270
271        assert_eq!(metrics.memories(), 0);
272        assert_eq!(metrics.core_instances(), 0);
273        assert_eq!(metrics.component_instances(), 0);
274        assert_eq!(metrics.memories(), 0);
275        assert_eq!(metrics.tables(), 0);
276        assert_eq!(metrics.unused_warm_memories(), 0);
277        assert_eq!(metrics.unused_memory_bytes_resident(), 0);
278        assert_eq!(metrics.unused_warm_tables(), 0);
279        assert_eq!(metrics.unused_table_bytes_resident(), 0);
280
281        let m1 = Module::new(
282            &engine,
283            r#"
284            (module (memory (export "m") 1) (table 1 funcref))
285        "#,
286        )?;
287
288        let mut store = Store::new(&engine, ());
289        crate::Instance::new(&mut store, &m1, &[])?;
290        assert_eq!(metrics.memories(), 1);
291        assert_eq!(metrics.tables(), 1);
292        assert_eq!(metrics.core_instances(), 1);
293        assert_eq!(metrics.component_instances(), 0);
294        drop(store);
295
296        assert_eq!(metrics.memories(), 0);
297        assert_eq!(metrics.tables(), 0);
298        assert_eq!(metrics.core_instances(), 0);
299        assert_eq!(metrics.unused_warm_memories(), 1);
300        assert_eq!(metrics.unused_warm_tables(), 1);
301        if PoolingAllocationConfig::is_pagemap_scan_available() {
302            assert_eq!(metrics.unused_memory_bytes_resident(), 0);
303            assert_eq!(metrics.unused_table_bytes_resident(), host_page_size);
304        } else {
305            assert_eq!(metrics.unused_memory_bytes_resident(), 65536);
306            assert_eq!(metrics.unused_table_bytes_resident(), host_page_size);
307        }
308
309        let mut store = Store::new(&engine, ());
310        let i = crate::Instance::new(&mut store, &m1, &[])?;
311        assert_eq!(metrics.memories(), 1);
312        assert_eq!(metrics.tables(), 1);
313        assert_eq!(metrics.core_instances(), 1);
314        assert_eq!(metrics.component_instances(), 0);
315        assert_eq!(metrics.unused_warm_memories(), 0);
316        assert_eq!(metrics.unused_warm_tables(), 0);
317        assert_eq!(metrics.unused_memory_bytes_resident(), 0);
318        assert_eq!(metrics.unused_table_bytes_resident(), 0);
319        let m = i.get_memory(&mut store, "m").unwrap();
320        m.data_mut(&mut store)[0] = 1;
321        m.grow(&mut store, 1)?;
322        drop(store);
323
324        assert_eq!(metrics.memories(), 0);
325        assert_eq!(metrics.tables(), 0);
326        assert_eq!(metrics.core_instances(), 0);
327        assert_eq!(metrics.unused_warm_memories(), 1);
328        assert_eq!(metrics.unused_warm_tables(), 1);
329        if PoolingAllocationConfig::is_pagemap_scan_available() {
330            assert_eq!(metrics.unused_memory_bytes_resident(), host_page_size);
331            assert_eq!(metrics.unused_table_bytes_resident(), host_page_size);
332        } else {
333            assert_eq!(metrics.unused_memory_bytes_resident(), 65536);
334            assert_eq!(metrics.unused_table_bytes_resident(), host_page_size);
335        }
336
337        let stores = (0..10)
338            .map(|_| {
339                let mut store = Store::new(&engine, ());
340                crate::Instance::new(&mut store, &m1, &[]).unwrap();
341                store
342            })
343            .collect::<Vec<_>>();
344
345        assert_eq!(metrics.memories(), 10);
346        assert_eq!(metrics.tables(), 10);
347        assert_eq!(metrics.core_instances(), 10);
348        assert_eq!(metrics.unused_warm_memories(), 0);
349        assert_eq!(metrics.unused_warm_tables(), 0);
350        assert_eq!(metrics.unused_memory_bytes_resident(), 0);
351        assert_eq!(metrics.unused_table_bytes_resident(), 0);
352
353        drop(stores);
354
355        assert_eq!(metrics.memories(), 00);
356        assert_eq!(metrics.tables(), 00);
357        assert_eq!(metrics.core_instances(), 00);
358        assert_eq!(metrics.unused_warm_memories(), 10);
359        assert_eq!(metrics.unused_warm_tables(), 10);
360        if PoolingAllocationConfig::is_pagemap_scan_available() {
361            assert_eq!(metrics.unused_memory_bytes_resident(), host_page_size);
362            assert_eq!(metrics.unused_table_bytes_resident(), 10 * host_page_size);
363        } else {
364            assert_eq!(metrics.unused_memory_bytes_resident(), 10 * 65536);
365            assert_eq!(metrics.unused_table_bytes_resident(), 10 * host_page_size);
366        }
367
368        Ok(())
369    }
370
371    #[test]
372    #[cfg_attr(miri, ignore)]
373    fn gc_heaps() -> Result<()> {
374        let pool = small_pool_config();
375        let mut config = Config::new();
376        config.allocation_strategy(pool);
377        let engine = Engine::new(&config)?;
378
379        let metrics = engine.pooling_allocator_metrics().unwrap();
380
381        assert_eq!(metrics.gc_heaps(), 0);
382        let mut store = Store::new(&engine, ());
383        crate::ExternRef::new(&mut store, ())?;
384        assert_eq!(metrics.gc_heaps(), 1);
385        drop(store);
386        assert_eq!(metrics.gc_heaps(), 0);
387
388        Ok(())
389    }
390
391    #[tokio::test]
392    #[cfg_attr(miri, ignore)]
393    async fn stacks() -> Result<()> {
394        let pool = small_pool_config();
395        let mut config = Config::new();
396        config.allocation_strategy(pool);
397        let engine = Engine::new(&config)?;
398
399        let metrics = engine.pooling_allocator_metrics().unwrap();
400
401        assert_eq!(metrics.stacks(), 0);
402        assert_eq!(metrics.unused_warm_stacks(), 0);
403        let mut store = Store::new(&engine, ());
404
405        crate::Func::wrap(&mut store, || {})
406            .call_async(&mut store, &[], &mut [])
407            .await?;
408        assert_eq!(metrics.stacks(), 1);
409        drop(store);
410        assert_eq!(metrics.stacks(), 0);
411        assert_eq!(metrics.unused_stack_bytes_resident(), None);
412        if StackPool::enabled() {
413            assert_eq!(metrics.unused_warm_stacks(), 1);
414        } else {
415            assert_eq!(metrics.unused_warm_stacks(), 0);
416        }
417
418        Ok(())
419    }
420}