xref: /wasmtime-44.0.1/examples/mpk.rs (revision dfc4358d)
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;
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: usize,
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     pool.max_memory_size(args.memory_size);
190     pool.total_memories(num_memories)
191         .memory_protection_keys(enable_mpk);
192 
193     // Configure the engine itself.
194     let mut config = Config::new();
195     if let Some(static_memory_maximum_size) = args.static_memory_maximum_size {
196         config.static_memory_maximum_size(static_memory_maximum_size);
197     }
198     if let Some(static_memory_guard_size) = args.static_memory_guard_size {
199         config.static_memory_guard_size(static_memory_guard_size);
200     }
201     config.allocation_strategy(InstanceAllocationStrategy::Pooling(pool));
202 
203     // Measure memory use before and after the engine is built.
204     let mapped_bytes_before = num_bytes_mapped()?;
205     let engine = Engine::new(&config)?;
206     let mapped_bytes_after = num_bytes_mapped()?;
207 
208     // Ensure we actually use the engine somehow.
209     engine.increment_epoch();
210 
211     let mapped_bytes = mapped_bytes_after - mapped_bytes_before;
212     info!(
213         "{}-slot pool ({:?}): {} bytes mapped",
214         num_memories, enable_mpk, mapped_bytes
215     );
216     Ok(mapped_bytes)
217 }
218 
219 /// Add up the sizes of all the mapped virtual memory regions for the current
220 /// Linux process.
221 ///
222 /// This manually parses `/proc/self/maps` to avoid a rather-large `proc-maps`
223 /// dependency. We do expect this example to be Linux-specific anyways. For
224 /// reference, lines of that file look like:
225 ///
226 /// ```text
227 /// 5652d4418000-5652d441a000 r--p 00000000 00:23 84629427 /usr/bin/...
228 /// ```
229 ///
230 /// We parse the start and end addresses: <start>-<end> [ignore the rest].
231 #[cfg(target_os = "linux")]
232 fn num_bytes_mapped() -> Result<usize> {
233     use std::fs::File;
234     use std::io::{BufRead, BufReader};
235 
236     let file = File::open("/proc/self/maps")?;
237     let reader = BufReader::new(file);
238     let mut total = 0;
239     for line in reader.lines() {
240         let line = line?;
241         let range = line
242             .split_whitespace()
243             .next()
244             .ok_or(anyhow!("parse failure: expected whitespace"))?;
245         let mut addresses = range.split("-");
246         let start = addresses
247             .next()
248             .ok_or(anyhow!("parse failure: expected dash-separated address"))?;
249         let start = usize::from_str_radix(start, 16)?;
250         let end = addresses
251             .next()
252             .ok_or(anyhow!("parse failure: expected dash-separated address"))?;
253         let end = usize::from_str_radix(end, 16)?;
254 
255         total += end - start;
256     }
257     Ok(total)
258 }
259 
260 #[cfg(not(target_os = "linux"))]
261 fn num_bytes_mapped() -> Result<usize> {
262     anyhow::bail!("this example can only read virtual memory maps on Linux")
263 }
264