xref: /wasmtime-44.0.1/examples/mpk.rs (revision f9f8a4df)
1 //! This example demonstrates:
2 //! - how to enable memory protection keys (MPK) in a Wasmtime embedding (see
3 //!   [`build_engine`])
4 //! - the expected memory compression from using MPK: it will probe the system
5 //!   by creating larger and larger memory pools until system memory is
6 //!   exhausted (see [`probe_engine_size`]). Then, it prints a comparison of the
7 //!   memory used in both the MPK enabled and MPK disabled configurations.
8 //!
9 //! You can execute this example with:
10 //!
11 //! ```console
12 //! $ cargo run --example mpk
13 //! ```
14 //!
15 //! Append `-- --help` for details about the configuring the memory size of the
16 //! pool. Also, to inspect interesting configuration values used for
17 //! constructing the pool, turn on logging:
18 //!
19 //! ```console
20 //! $ RUST_LOG=debug cargo run --example mpk -- --memory-size 512MiB
21 //! ```
22 //!
23 //! Note that MPK support is limited to x86 Linux systems. OS limits on the
24 //! number of virtual memory areas (VMAs) can significantly restrict the total
25 //! number MPK-striped memory slots; each MPK-protected slot ends up using a new
26 //! VMA entry. On Linux, one can raise this limit:
27 //!
28 //! ```console
29 //! $ sysctl vm.max_map_count
30 //! 65530
31 //! $ sysctl vm.max_map_count=$LARGER_LIMIT
32 //! ```
33 
34 use anyhow::{anyhow, Result};
35 use bytesize::ByteSize;
36 use clap::Parser;
37 use log::{info, warn};
38 use std::str::FromStr;
39 use wasmtime::*;
40 
41 fn main() -> Result<()> {
42     env_logger::init();
43     let args = Args::parse();
44     info!("{:?}", args);
45 
46     let without_mpk = probe_engine_size(&args, MpkEnabled::Disable)?;
47     println!("without MPK:\t{}", without_mpk.to_string());
48 
49     if PoolingAllocationConfig::are_memory_protection_keys_available() {
50         let with_mpk = probe_engine_size(&args, MpkEnabled::Enable)?;
51         println!("with MPK:\t{}", with_mpk.to_string());
52         println!(
53             "\t\t{}x more slots per reserved memory",
54             with_mpk.compare(&without_mpk)
55         );
56     } else {
57         println!("with MPK:\tunavailable\t\tunavailable");
58     }
59 
60     Ok(())
61 }
62 
63 #[derive(Debug, Parser)]
64 #[command(author, version, about, long_about = None)]
65 struct Args {
66     /// The maximum number of bytes for each WebAssembly linear memory in the
67     /// pool.
68     #[arg(long, default_value = "128MiB", value_parser = parse_byte_size)]
69     memory_size: u64,
70 
71     /// The maximum number of bytes a memory is considered static; see
72     /// `Config::static_memory_maximum_size` for more details and the default
73     /// value if unset.
74     #[arg(long, value_parser = parse_byte_size)]
75     static_memory_maximum_size: Option<u64>,
76 
77     /// The size in bytes of the guard region to expect between static memory
78     /// slots; see [`Config::static_memory_guard_size`] for more details and the
79     /// default value if unset.
80     #[arg(long, value_parser = parse_byte_size)]
81     static_memory_guard_size: Option<u64>,
82 }
83 
84 /// Parse a human-readable byte size--e.g., "512 MiB"--into the correct number
85 /// of bytes.
86 fn parse_byte_size(value: &str) -> Result<u64> {
87     let size = ByteSize::from_str(value).map_err(|e| anyhow!(e))?;
88     Ok(size.as_u64())
89 }
90 
91 /// Find the engine with the largest number of memories we can create on this
92 /// machine.
93 fn probe_engine_size(args: &Args, mpk: MpkEnabled) -> Result<Pool> {
94     let mut search = ExponentialSearch::new();
95     let mut mapped_bytes = 0;
96     while !search.done() {
97         match build_engine(&args, search.next(), mpk) {
98             Ok(rb) => {
99                 // TODO: assert!(rb >= mapped_bytes);
100                 mapped_bytes = rb;
101                 search.record(true)
102             }
103             Err(e) => {
104                 warn!("failed engine allocation, continuing search: {:?}", e);
105                 search.record(false)
106             }
107         }
108     }
109     Ok(Pool {
110         num_memories: search.next(),
111         mapped_bytes,
112     })
113 }
114 
115 #[derive(Debug)]
116 #[allow(dead_code)]
117 struct Pool {
118     num_memories: u32,
119     mapped_bytes: usize,
120 }
121 impl Pool {
122     /// Print a human-readable, tab-separated description of this structure.
123     fn to_string(&self) -> String {
124         let human_size = ByteSize::b(self.mapped_bytes as u64).to_string_as(true);
125         format!(
126             "{} memory slots\t{} reserved",
127             self.num_memories, human_size
128         )
129     }
130     /// Return the number of times more memory slots in `self` than `other`
131     /// after normalizing by the mapped bytes sizes. Rounds to three decimal
132     /// places arbitrarily; no significance intended.
133     fn compare(&self, other: &Pool) -> f64 {
134         let size_ratio = other.mapped_bytes as f64 / self.mapped_bytes as f64;
135         let slots_ratio = self.num_memories as f64 / other.num_memories as f64;
136         let times_more_efficient = slots_ratio * size_ratio;
137         (times_more_efficient * 1000.0).round() / 1000.0
138     }
139 }
140 
141 /// Exponentially increase the `next` value until the attempts fail, then
142 /// perform a binary search to find the maximum attempted value that still
143 /// succeeds.
144 #[derive(Debug)]
145 struct ExponentialSearch {
146     /// Determines if we are in the growth phase.
147     growing: bool,
148     /// The last successful value tried; this is the algorithm's lower bound.
149     last: u32,
150     /// The next value to try; this is the algorithm's upper bound.
151     next: u32,
152 }
153 impl ExponentialSearch {
154     fn new() -> Self {
155         Self {
156             growing: true,
157             last: 0,
158             next: 1,
159         }
160     }
161     fn next(&self) -> u32 {
162         self.next
163     }
164     fn record(&mut self, success: bool) {
165         if !success {
166             self.growing = false
167         }
168         let diff = if self.growing {
169             (self.next - self.last) * 2
170         } else {
171             (self.next - self.last + 1) / 2
172         };
173         if success {
174             self.last = self.next;
175             self.next = self.next + diff;
176         } else {
177             self.next = self.next - diff;
178         }
179     }
180     fn done(&self) -> bool {
181         self.last == self.next
182     }
183 }
184 
185 /// Build a pool-allocated engine with `num_memories` slots.
186 fn build_engine(args: &Args, num_memories: u32, enable_mpk: MpkEnabled) -> Result<usize> {
187     // Configure the memory pool.
188     let mut pool = PoolingAllocationConfig::default();
189     let memory_pages = args.memory_size / u64::from(wasmtime_environ::WASM_PAGE_SIZE);
190     pool.memory_pages(memory_pages);
191     pool.total_memories(num_memories)
192         .memory_protection_keys(enable_mpk);
193 
194     // Configure the engine itself.
195     let mut config = Config::new();
196     if let Some(static_memory_maximum_size) = args.static_memory_maximum_size {
197         config.static_memory_maximum_size(static_memory_maximum_size);
198     }
199     if let Some(static_memory_guard_size) = args.static_memory_guard_size {
200         config.static_memory_guard_size(static_memory_guard_size);
201     }
202     config.allocation_strategy(InstanceAllocationStrategy::Pooling(pool));
203 
204     // Measure memory use before and after the engine is built.
205     let mapped_bytes_before = num_bytes_mapped()?;
206     let engine = Engine::new(&config)?;
207     let mapped_bytes_after = num_bytes_mapped()?;
208 
209     // Ensure we actually use the engine somehow.
210     engine.increment_epoch();
211 
212     let mapped_bytes = mapped_bytes_after - mapped_bytes_before;
213     info!(
214         "{}-slot pool ({:?}): {} bytes mapped",
215         num_memories, enable_mpk, mapped_bytes
216     );
217     Ok(mapped_bytes)
218 }
219 
220 /// Add up the sizes of all the mapped virtual memory regions for the current
221 /// Linux process.
222 ///
223 /// This manually parses `/proc/self/maps` to avoid a rather-large `proc-maps`
224 /// dependency. We do expect this example to be Linux-specific anyways. For
225 /// reference, lines of that file look like:
226 ///
227 /// ```text
228 /// 5652d4418000-5652d441a000 r--p 00000000 00:23 84629427 /usr/bin/...
229 /// ```
230 ///
231 /// We parse the start and end addresses: <start>-<end> [ignore the rest].
232 #[cfg(target_os = "linux")]
233 fn num_bytes_mapped() -> Result<usize> {
234     use std::fs::File;
235     use std::io::{BufRead, BufReader};
236 
237     let file = File::open("/proc/self/maps")?;
238     let reader = BufReader::new(file);
239     let mut total = 0;
240     for line in reader.lines() {
241         let line = line?;
242         let range = line
243             .split_whitespace()
244             .next()
245             .ok_or(anyhow!("parse failure: expected whitespace"))?;
246         let mut addresses = range.split("-");
247         let start = addresses
248             .next()
249             .ok_or(anyhow!("parse failure: expected dash-separated address"))?;
250         let start = usize::from_str_radix(start, 16)?;
251         let end = addresses
252             .next()
253             .ok_or(anyhow!("parse failure: expected dash-separated address"))?;
254         let end = usize::from_str_radix(end, 16)?;
255 
256         total += end - start;
257     }
258     Ok(total)
259 }
260 
261 #[cfg(not(target_os = "linux"))]
262 fn num_bytes_mapped() -> Result<usize> {
263     anyhow::bail!("this example can only read virtual memory maps on Linux")
264 }
265