use anyhow::anyhow;
use bytesize::ByteSize;
use clap::Parser;
use log::{info, warn};
use std::str::FromStr;
use wasmtime::*;
fn main() -> Result<()> {
env_logger::init();
let args = Args::parse();
info!("{args:?}");
let without_mpk = probe_engine_size(&args, Enabled::No)?;
println!("without MPK:\t{}", without_mpk.to_string());
if PoolingAllocationConfig::are_memory_protection_keys_available() {
let with_mpk = probe_engine_size(&args, Enabled::Yes)?;
println!("with MPK:\t{}", with_mpk.to_string());
println!(
"\t\t{}x more slots per reserved memory",
with_mpk.compare(&without_mpk)
);
} else {
println!("with MPK:\tunavailable\t\tunavailable");
}
Ok(())
}
#[derive(Debug, Parser)]
#[command(author, version, about, long_about = None)]
struct Args {
#[arg(long, default_value = "128MiB", value_parser = parse_byte_size)]
memory_size: u64,
#[arg(long, value_parser = parse_byte_size)]
memory_reservation: Option<u64>,
#[arg(long, value_parser = parse_byte_size)]
memory_guard_size: Option<u64>,
}
fn parse_byte_size(value: &str) -> Result<u64> {
let size = ByteSize::from_str(value).map_err(|e| anyhow!(e))?;
Ok(size.as_u64())
}
fn probe_engine_size(args: &Args, mpk: Enabled) -> Result<Pool> {
let mut search = ExponentialSearch::new();
let mut mapped_bytes = 0;
while !search.done() {
match build_engine(&args, search.next(), mpk) {
Ok(rb) => {
mapped_bytes = rb;
search.record(true)
}
Err(e) => {
warn!("failed engine allocation, continuing search: {e:?}");
search.record(false)
}
}
}
Ok(Pool {
num_memories: search.next(),
mapped_bytes,
})
}
#[derive(Debug)]
struct Pool {
num_memories: u32,
mapped_bytes: usize,
}
impl Pool {
fn to_string(&self) -> String {
let human_size = ByteSize::b(self.mapped_bytes as u64).display().si();
format!(
"{} memory slots\t{} reserved",
self.num_memories, human_size
)
}
fn compare(&self, other: &Pool) -> f64 {
let size_ratio = other.mapped_bytes as f64 / self.mapped_bytes as f64;
let slots_ratio = self.num_memories as f64 / other.num_memories as f64;
let times_more_efficient = slots_ratio * size_ratio;
(times_more_efficient * 1000.0).round() / 1000.0
}
}
#[derive(Debug)]
struct ExponentialSearch {
growing: bool,
last: u32,
next: u32,
}
impl ExponentialSearch {
fn new() -> Self {
Self {
growing: true,
last: 0,
next: 1,
}
}
fn next(&self) -> u32 {
self.next
}
fn record(&mut self, success: bool) {
if !success {
self.growing = false
}
let diff = if self.growing {
(self.next - self.last) * 2
} else {
(self.next - self.last + 1) / 2
};
if success {
self.last = self.next;
self.next = self.next + diff;
} else {
self.next = self.next - diff;
}
}
fn done(&self) -> bool {
self.last == self.next
}
}
fn build_engine(args: &Args, num_memories: u32, enable_mpk: Enabled) -> Result<usize> {
let mut pool = PoolingAllocationConfig::default();
let max_memory_size =
usize::try_from(args.memory_size).expect("memory size should fit in `usize`");
pool.max_memory_size(max_memory_size)
.total_memories(num_memories)
.memory_protection_keys(enable_mpk);
let mut config = Config::new();
if let Some(memory_reservation) = args.memory_reservation {
config.memory_reservation(memory_reservation);
}
if let Some(memory_guard_size) = args.memory_guard_size {
config.memory_guard_size(memory_guard_size);
}
config.allocation_strategy(InstanceAllocationStrategy::Pooling(pool));
let mapped_bytes_before = num_bytes_mapped()?;
let engine = Engine::new(&config)?;
let mapped_bytes_after = num_bytes_mapped()?;
engine.increment_epoch();
let mapped_bytes = mapped_bytes_after - mapped_bytes_before;
info!("{num_memories}-slot pool ({enable_mpk:?}): {mapped_bytes} bytes mapped");
Ok(mapped_bytes)
}
#[cfg(target_os = "linux")]
fn num_bytes_mapped() -> Result<usize> {
use std::fs::File;
use std::io::{BufRead, BufReader};
let file = File::open("/proc/self/maps")?;
let reader = BufReader::new(file);
let mut total = 0;
for line in reader.lines() {
let line = line?;
let range = line
.split_whitespace()
.next()
.ok_or(anyhow!("parse failure: expected whitespace"))?;
let mut addresses = range.split("-");
let start = addresses
.next()
.ok_or(anyhow!("parse failure: expected dash-separated address"))?;
let start = usize::from_str_radix(start, 16)?;
let end = addresses
.next()
.ok_or(anyhow!("parse failure: expected dash-separated address"))?;
let end = usize::from_str_radix(end, 16)?;
total += end - start;
}
Ok(total)
}
#[cfg(not(target_os = "linux"))]
fn num_bytes_mapped() -> Result<usize> {
anyhow::bail!("this example can only read virtual memory maps on Linux")
}