Skip to content

Commit

Permalink
Support virtual memory usage on windows (#274)
Browse files Browse the repository at this point in the history
* replace raw mmap usage with region crate

By doing so we add windows as a platform with support for virtual memory.

* add windows and macos to GitHub Actions testing CI pipeline

* simplify GitHub Actions CI clippy pipeline

* remove unused errno dependency

* reallocate the virtual memory in ByteBuf::erase

This is basically what the code before the PR has been doing.

* try to fix virtual memory allocation problems for GitHub Actions on Windows

* fix typo in GitHub Actions CI file

* fix windows paging for GitHub Actions (trial 1)

* improve safety note

* use default values (defaulting to 8GB)

* try to fix Windows GHA

* try again to fix windows CI

* fallback to Vec-based virtual memory for 32-bit platforms

* next try at fixing pagefile for windows ...

* and another try at fixing GHA windows pagefile sizes

* run tests using virtual memory single threaded

This is yet another attempt at fixing the windows GHA CI

* remove #[inline] annotations as wished in the code review

* remove even more #[inline] annotations as requested by review
  • Loading branch information
Robbepop authored Dec 9, 2021
1 parent 060c503 commit cd69711
Show file tree
Hide file tree
Showing 4 changed files with 90 additions and 142 deletions.
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;

/// 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.
self.len = new_len;
Ok(())
}

/// Returns the current length of the virtual memory.
#[inline]
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)?;
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

0 comments on commit cd69711

Please sign in to comment.