Files
wasmtime/tests/all/wast.rs
Alex Crichton f5041dd362 Implement a setting for reserved dynamic memory growth (#3215)
* Implement a setting for reserved dynamic memory growth

Dynamic memories aren't really that heavily used in Wasmtime right now
because for most 32-bit memories they're classified as "static" which
means they reserve 4gb of address space and never move. Growth of a
static memory is simply making pages accessible, so it's quite fast.

With the memory64 feature, however, this is no longer true since all
memory64 memories are classified as "dynamic" at this time. Previous to
this commit growth of a dynamic memory unconditionally moved the entire
linear memory in the host's address space, always resulting in a new
`Mmap` allocation. This behavior is causing fuzzers to time out when
working with 64-bit memories because incrementally growing a memory by 1
page at a time can incur a quadratic time complexity as bytes are
constantly moved.

This commit implements a scheme where there is now a tunable setting for
memory to be reserved at the end of a dynamic memory to grow into. This
means that dynamic memory growth is ideally amortized as most calls to
`memory.grow` will be able to grow into the pre-reserved space. Some
calls, though, will still need to copy the memory around.

This helps enable a commented out test for 64-bit memories now that it's
fast enough to run in debug mode. This is because the growth of memory
in the test no longer needs to copy 4gb of zeros.

* Test fixes & review comments

* More comments
2021-08-20 10:54:23 -05:00

172 lines
6.1 KiB
Rust

use std::path::Path;
use std::sync::{Condvar, Mutex};
use wasmtime::{
Config, Engine, InstanceAllocationStrategy, InstanceLimits, ModuleLimits,
PoolingAllocationStrategy, Store, Strategy,
};
use wasmtime_wast::WastContext;
include!(concat!(env!("OUT_DIR"), "/wast_testsuite_tests.rs"));
// Each of the tests included from `wast_testsuite_tests` will call this
// function which actually executes the `wast` test suite given the `strategy`
// to compile it.
fn run_wast(wast: &str, strategy: Strategy, pooling: bool) -> anyhow::Result<()> {
let wast = Path::new(wast);
let simd = feature_found(wast, "simd");
let memory64 = feature_found(wast, "memory64");
let multi_memory = feature_found(wast, "multi-memory");
let module_linking = feature_found(wast, "module-linking");
let threads = feature_found(wast, "threads");
let bulk_mem = memory64 || multi_memory || feature_found(wast, "bulk-memory-operations");
// Some simd tests assume support for multiple tables, which are introduced
// by reference types.
let reftypes = simd || feature_found(wast, "reference-types");
// Threads & simd aren't implemented in the old backend, so skip those
// tests.
if (threads || simd) && cfg!(feature = "old-x86-backend") {
return Ok(());
}
let mut cfg = Config::new();
cfg.wasm_simd(simd)
.wasm_bulk_memory(bulk_mem)
.wasm_reference_types(reftypes || module_linking)
.wasm_multi_memory(multi_memory || module_linking)
.wasm_module_linking(module_linking)
.wasm_threads(threads)
.wasm_memory64(memory64)
.strategy(strategy)?
.cranelift_debug_verifier(true);
if feature_found(wast, "canonicalize-nan") {
cfg.cranelift_nan_canonicalization(true);
}
let test_allocates_lots_of_memory = wast.ends_with("more-than-4gb.wast");
// By default we'll allocate huge chunks (6gb) of the address space for each
// linear memory. This is typically fine but when we emulate tests with QEMU
// it turns out that it causes memory usage to balloon massively. Leave a
// knob here so on CI we can cut down the memory usage of QEMU and avoid the
// OOM killer.
//
// Locally testing this out this drops QEMU's memory usage running this
// tests suite from 10GiB to 600MiB. Previously we saw that crossing the
// 10GiB threshold caused our processes to get OOM killed on CI.
if std::env::var("WASMTIME_TEST_NO_HOG_MEMORY").is_ok() {
// The pooling allocator hogs ~6TB of virtual address space for each
// store, so if we don't to hog memory then ignore pooling tests.
if pooling {
return Ok(());
}
// If the test allocates a lot of memory, that's considered "hogging"
// memory, so skip it.
if test_allocates_lots_of_memory {
return Ok(());
}
// Don't use 4gb address space reservations when not hogging memory, and
// also don't reserve lots of memory after dynamic memories for growth
// (makes growth slower).
cfg.static_memory_maximum_size(0);
cfg.dynamic_memory_reserved_for_growth(0);
}
let _pooling_lock = if pooling {
// Some memory64 tests take more than 4gb of resident memory to test,
// but we don't want to configure the pooling allocator to allow that
// (that's a ton of memory to reserve), so we skip those tests.
if test_allocates_lots_of_memory {
return Ok(());
}
// The limits here are crafted such that the wast tests should pass.
// However, these limits may become insufficient in the future as the wast tests change.
// If a wast test fails because of a limit being "exceeded" or if memory/table
// fails to grow, the values here will need to be adjusted.
cfg.allocation_strategy(InstanceAllocationStrategy::Pooling {
strategy: PoolingAllocationStrategy::NextAvailable,
module_limits: ModuleLimits {
imported_memories: 2,
imported_tables: 2,
imported_globals: 11,
memories: 2,
tables: 4,
globals: 11,
memory_pages: 805,
..Default::default()
},
instance_limits: InstanceLimits {
count: 450,
..Default::default()
},
});
Some(lock_pooling())
} else {
None
};
let store = Store::new(&Engine::new(&cfg)?, ());
let mut wast_context = WastContext::new(store);
wast_context.register_spectest()?;
wast_context.run_file(wast)?;
Ok(())
}
fn feature_found(path: &Path, name: &str) -> bool {
path.iter().any(|part| match part.to_str() {
Some(s) => s.contains(name),
None => false,
})
}
// The pooling tests make about 6TB of address space reservation which means
// that we shouldn't let too many of them run concurrently at once. On
// high-cpu-count systems (e.g. 80 threads) this leads to mmap failures because
// presumably too much of the address space has been reserved with our limits
// specified above. By keeping the number of active pooling-related tests to a
// specified maximum we can put a cap on the virtual address space reservations
// made.
fn lock_pooling() -> impl Drop {
const MAX_CONCURRENT_POOLING: u32 = 8;
lazy_static::lazy_static! {
static ref ACTIVE: MyState = MyState::default();
}
#[derive(Default)]
struct MyState {
lock: Mutex<u32>,
waiters: Condvar,
}
impl MyState {
fn lock(&self) -> impl Drop + '_ {
let state = self.lock.lock().unwrap();
let mut state = self
.waiters
.wait_while(state, |cnt| *cnt >= MAX_CONCURRENT_POOLING)
.unwrap();
*state += 1;
LockGuard { state: self }
}
}
struct LockGuard<'a> {
state: &'a MyState,
}
impl Drop for LockGuard<'_> {
fn drop(&mut self) {
*self.state.lock.lock().unwrap() -= 1;
self.state.waiters.notify_one();
}
}
ACTIVE.lock()
}