Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support virtual memory usage on windows #274

Merged
merged 20 commits into from
Dec 9, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
937701e
replace raw mmap usage with region crate
Robbepop Dec 8, 2021
51701d3
add windows and macos to GitHub Actions testing CI pipeline
Robbepop Dec 8, 2021
f64a466
simplify GitHub Actions CI clippy pipeline
Robbepop Dec 8, 2021
61db4d7
remove unused errno dependency
Robbepop Dec 8, 2021
d436228
Merge branch 'master' of github.com:paritytech/wasmi into rf-virtual-…
Robbepop Dec 8, 2021
60a3b50
reallocate the virtual memory in ByteBuf::erase
Robbepop Dec 8, 2021
532aab0
try to fix virtual memory allocation problems for GitHub Actions on W…
Robbepop Dec 8, 2021
649cd53
fix typo in GitHub Actions CI file
Robbepop Dec 8, 2021
3014405
fix windows paging for GitHub Actions (trial 1)
Robbepop Dec 8, 2021
9fba5ee
improve safety note
Robbepop Dec 8, 2021
ec342eb
use default values (defaulting to 8GB)
Robbepop Dec 8, 2021
c5b0f59
try to fix Windows GHA
Robbepop Dec 8, 2021
15ff6fc
try again to fix windows CI
Robbepop Dec 9, 2021
c05599e
fallback to Vec-based virtual memory for 32-bit platforms
Robbepop Dec 9, 2021
860f97e
next try at fixing pagefile for windows ...
Robbepop Dec 9, 2021
72037c6
and another try at fixing GHA windows pagefile sizes
Robbepop Dec 9, 2021
c56cd3a
run tests using virtual memory single threaded
Robbepop Dec 9, 2021
965d37d
Merge branch 'master' of github.com:paritytech/wasmi into rf-virtual-…
Robbepop Dec 9, 2021
ce7eed3
remove #[inline] annotations as wished in the code review
Robbepop Dec 9, 2021
1b271a0
remove even more #[inline] annotations as requested by review
Robbepop Dec 9, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@ jobs:
test-args: "--test-threads 1"
runs-on: ${{ matrix.os }}
steps:
- name: Configure Pagefile for Windows
if: matrix.os == 'windows-latest'
uses: al-cheb/configure-pagefile-action@v1.2
with:
minimum-size: 6GB
maximum-size: 32GB
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
Expand Down
10 changes: 6 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@ memory_units = "0.3.0"
libm = "0.2.1"
num-rational = { version = "0.4", default-features = false, features = ["num-bigint"] }
num-traits = { version = "0.2.8", default-features = false }
libc = { version = "0.2.58", optional = true }
errno = { version = "0.2.4", optional = true }
region = { version = "3.0.0", optional = true }
downcast-rs = { version = "1.2.0", default-features = false }

[dev-dependencies]
Expand All @@ -42,9 +41,12 @@ std = [
#
# Note
#
# - This feature is only supported on 64-bit platforms.
# For 32-bit platforms the linear memory will fallback to using the Vec
# based implementation.
# - The default is to fall back is an inefficient vector based implementation.
# - By nature this feature requires `libc` and the Rust standard library.
virtual_memory = ["libc", "std"]
# - By nature this feature requires `region` and the Rust standard library.
virtual_memory = ["region", "std"]

reduced-stack-buffer = [ "parity-wasm/reduced-stack-buffer" ]

Expand Down
212 changes: 76 additions & 136 deletions src/memory/mmap_bytebuf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,181 +5,121 @@
//! memory up to maximum. This might be a problem for systems that don't have a lot of virtual
//! memory (i.e. 32-bit platforms).

use core::ptr::{self, NonNull};
use core::slice;
use region::{Allocation, Protection};

struct Mmap {
/// The pointer that points to the start of the mapping.
///
/// This value doesn't change after creation.
ptr: NonNull<u8>,
/// The length of this mapping.
///
/// Cannot be more than `isize::max_value()`. This value doesn't change after creation.
len: usize,
/// A virtual memory buffer.
struct VirtualMemory {
/// The virtual memory allocation.
allocation: Allocation,
}

impl Mmap {
/// Create a new mmap mapping
impl VirtualMemory {
/// Create a new virtual memory allocation.
///
/// # Note
///
/// Returns `Err` if:
/// - `len` should not exceed `isize::max_value()`
/// - `len` should be greater than 0.
/// - `mmap` returns an error (almost certainly means out of memory).
fn new(len: usize) -> Result<Self, String> {
/// The allocated virtual memory allows for read and write operations.
///
/// # Errors
///
/// - If `len` should not exceed `isize::max_value()`
/// - If `len` should be greater than 0.
/// - If the operating system returns an error upon virtual memory allocation.
pub fn new(len: usize) -> Result<Self, String> {
if len > isize::max_value() as usize {
return Err("`len` should not exceed `isize::max_value()`".into());
}
if len == 0 {
return Err("`len` should be greater than 0".into());
}

let ptr_or_err = unsafe {
// Safety Proof:
// There are not specific safety proofs are required for this call, since the call
// by itself can't invoke any safety problems (however, misusing its result can).
libc::mmap(
// `addr` - let the system to choose the address at which to create the mapping.
ptr::null_mut(),
// the length of the mapping in bytes.
len,
// `prot` - protection flags: READ WRITE !EXECUTE
libc::PROT_READ | libc::PROT_WRITE,
// `flags`
// `MAP_ANON` - mapping is not backed by any file and initial contents are
// initialized to zero.
// `MAP_PRIVATE` - the mapping is private to this process.
libc::MAP_ANON | libc::MAP_PRIVATE,
// `fildes` - a file descriptor. Pass -1 as this is required for some platforms
// when the `MAP_ANON` is passed.
-1,
// `offset` - offset from the file.
0,
)
};

match ptr_or_err {
// With the current parameters, the error can only be returned in case of insufficient
// memory.
//
// If we have `errno` linked in augement the error message with the one that was
// provided by errno.
#[cfg(feature = "errno")]
libc::MAP_FAILED => {
let errno = errno::errno();
Err(format!("mmap returned an error ({}): {}", errno.0, errno))
}
#[cfg(not(feature = "errno"))]
libc::MAP_FAILED => Err("mmap returned an error".into()),
_ => {
let ptr = NonNull::new(ptr_or_err as *mut u8)
.ok_or_else(|| "mmap returned 0".to_string())?;
Ok(Self { ptr, len })
}
}
}

fn as_slice(&self) -> &[u8] {
unsafe {
// Safety Proof:
// - Aliasing guarantees of `self.ptr` are not violated since `self` is the only owner.
// - This pointer was allocated for `self.len` bytes and thus is a valid slice.
// - `self.len` doesn't change throughout the lifetime of `self`.
// - The value is returned valid for the duration of lifetime of `self`.
// `self` cannot be destroyed while the returned slice is alive.
// - `self.ptr` is of `NonNull` type and thus `.as_ptr()` can never return NULL.
// - `self.len` cannot be larger than `isize::max_value()`.
slice::from_raw_parts(self.ptr.as_ptr(), self.len)
}
let allocation =
region::alloc(len, Protection::READ_WRITE).map_err(|error| error.to_string())?;
Ok(Self { allocation })
}

fn as_slice_mut(&mut self) -> &mut [u8] {
unsafe {
// Safety Proof:
// - See the proof for `Self::as_slice`
// - Additionally, it is not possible to obtain two mutable references for `self.ptr`
slice::from_raw_parts_mut(self.ptr.as_ptr(), self.len)
}
/// Returns a shared slice over the bytes of the virtual memory allocation.
pub fn as_slice(&self) -> &[u8] {
// # SAFETY
//
// The operation is safe since we assume that the virtual memory allocation
// has been successful and allocated exactly `self.allocation.len()` bytes.
// Therefore creating a slice with `self.len` elements is valid.
// Aliasing guarantees are not violated since `self` is the only owner
// of the underlying virtual memory allocation.
unsafe { slice::from_raw_parts(self.allocation.as_ptr(), self.allocation.len()) }
}
}

impl Drop for Mmap {
fn drop(&mut self) {
let ret_val = unsafe {
// Safety proof:
// - `self.ptr` was allocated by a call to `mmap`.
// - `self.len` was saved at the same time and it doesn't change throughout the lifetime
// of `self`.
libc::munmap(self.ptr.as_ptr() as *mut libc::c_void, self.len)
};

// There is no reason for `munmap` to fail to deallocate a private annonymous mapping
// allocated by `mmap`.
// However, for the cases when it actually fails prefer to fail, in order to not leak
// and exhaust the virtual memory.
assert_eq!(ret_val, 0, "munmap failed");
/// Returns an exclusive slice over the bytes of the virtual memory allocation.
pub fn as_slice_mut(&mut self) -> &mut [u8] {
// # SAFETY
//
// See safety proof of the `as_slice` method.
// Additionally, it is not possible to obtain two mutable references for the same memory area.
unsafe { slice::from_raw_parts_mut(self.allocation.as_mut_ptr(), self.allocation.len()) }
}
}

/// A virtually allocated byte buffer.
pub struct ByteBuf {
mmap: Option<Mmap>,
/// The underlying virtual memory allocation.
mem: VirtualMemory,
/// The current size of the used parts of the virtual memory allocation.
len: usize,
}

impl ByteBuf {
/// Determines the initial size of the virtual memory allocation.
///
/// # Note
///
/// In this implementation we won't reallocate the virtually allocated
/// buffer and instead simply adjust the `len` field of the `ByteBuf`
/// wrapper in order to efficiently grow the virtual memory.
const ALLOCATION_SIZE: usize = u32::MAX as usize;
Robbepop marked this conversation as resolved.
Show resolved Hide resolved

/// Creates a new byte buffer with the given initial length.
pub fn new(len: usize) -> Result<Self, String> {
let mmap = if len == 0 {
None
} else {
Some(Mmap::new(len)?)
};
Ok(Self { mmap })
if len > isize::max_value() as usize {
return Err("`len` should not exceed `isize::max_value()`".into());
}
let mem = VirtualMemory::new(Self::ALLOCATION_SIZE)?;
Ok(Self { mem, len })
}

/// Reallocates the virtual memory with the new length in bytes.
pub fn realloc(&mut self, new_len: usize) -> Result<(), String> {
let new_mmap = if new_len == 0 {
None
} else {
let mut new_mmap = Mmap::new(new_len)?;
if let Some(cur_mmap) = self.mmap.take() {
let src = cur_mmap.as_slice();
let dst = new_mmap.as_slice_mut();
let amount = src.len().min(dst.len());
dst[..amount].copy_from_slice(&src[..amount]);
}
Some(new_mmap)
};

self.mmap = new_mmap;
// This operation is only actually needed in order to make the
// Vec-based implementation less inefficient. In the case of a
// virtual memory with preallocated 4GB of virtual memory pages
// we only need to adjust the `len` field.
kpp marked this conversation as resolved.
Show resolved Hide resolved
self.len = new_len;
Ok(())
}

/// Returns the current length of the virtual memory.
#[inline]
Robbepop marked this conversation as resolved.
Show resolved Hide resolved
pub fn len(&self) -> usize {
self.mmap.as_ref().map(|m| m.len).unwrap_or(0)
self.len
}

/// Returns a shared slice over the bytes of the virtual memory allocation.
pub fn as_slice(&self) -> &[u8] {
self.mmap.as_ref().map(|m| m.as_slice()).unwrap_or(&[])
&self.mem.as_slice()[..self.len]
}

/// Returns an exclusive slice over the bytes of the virtual memory allocation.
pub fn as_slice_mut(&mut self) -> &mut [u8] {
self.mmap
.as_mut()
.map(|m| m.as_slice_mut())
.unwrap_or(&mut [])
&mut self.mem.as_slice_mut()[..self.len]
}

/// Writes zero to the used bits of the virtual memory.
///
/// # Note
///
/// If possible this API should not exist.
pub fn erase(&mut self) -> Result<(), String> {
let len = self.len();
if len > 0 {
// The order is important.
//
// 1. First we clear, and thus drop, the current mmap if any.
// 2. And then we create a new one.
//
// Otherwise we double the peak memory consumption.
self.mmap = None;
self.mmap = Some(Mmap::new(len)?);
}
self.mem = VirtualMemory::new(Self::ALLOCATION_SIZE)?;
athei marked this conversation as resolved.
Show resolved Hide resolved
Ok(())
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/memory/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ use core::{
};
use parity_wasm::elements::ResizableLimits;

#[cfg(all(unix, feature = "virtual_memory"))]
#[cfg(all(feature = "virtual_memory", target_pointer_width = "64"))]
#[path = "mmap_bytebuf.rs"]
mod bytebuf;

#[cfg(not(all(unix, feature = "virtual_memory")))]
#[cfg(not(all(feature = "virtual_memory", target_pointer_width = "64")))]
#[path = "vec_bytebuf.rs"]
mod bytebuf;

Expand Down