Skip to content

Commit

Permalink
Document budgeting and limiting (#637)
Browse files Browse the repository at this point in the history
  • Loading branch information
udoprog authored Oct 1, 2023
1 parent 626d0a9 commit 6e24f08
Show file tree
Hide file tree
Showing 16 changed files with 241 additions and 150 deletions.
5 changes: 3 additions & 2 deletions .github/workflows/site.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,14 @@ jobs:
- run: cargo run --bin rune -- doc --output target/site/docs
env:
RUST_LOG: rune=info
- run: mdbook build -d ../target/site/book book
- uses: dtolnay/rust-toolchain@nightly
- run: cargo +nightly doc -p rune --all-features --target-dir target/site/api
- run: cargo +nightly doc -p rune --all-features
env:
RUST_LOG: rune=info
RUSTFLAGS: --cfg docsrs
RUSTDOCFLAGS: --cfg docsrs
- run: mdbook build -d ../target/site/book book
- run: mv target/doc target/site/api
- uses: peaceiris/actions-gh-pages@v3
with:
deploy_key: ${{ secrets.ACTIONS_DEPLOY_KEY }}
Expand Down
32 changes: 32 additions & 0 deletions crates/rune-alloc/src/callable.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
//! A trait used for types which can be called.
//!
//! This trait allows for memory [`limits`] and [`budgets`] to be combined.
//!
//! [`limits`]: crate::limit
//! [`budgets`]: ../../runtime/budget/index.html
/// A trait used for types which can be called.
///
/// This trait allows for memory [`limits`] and [`budgets`] to be combined.
///
/// [`limits`]: crate::limit
/// [`budgets`]: ../../runtime/budget/index.html
pub trait Callable {
/// Output of the callable.
type Output;

/// Call and consume the callable.
fn call(self) -> Self::Output;
}

/// Blanket implementation for closures.
impl<T, O> Callable for T
where
T: FnOnce() -> O,
{
type Output = O;

fn call(self) -> Self::Output {
self()
}
}
6 changes: 6 additions & 0 deletions crates/rune-alloc/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@
#![no_std]
// TODO: get rid of this once we've evaluated what we want to have public.
#![allow(dead_code)]
#![deny(rustdoc::broken_intra_doc_links)]
#![deny(rustdoc::private_doc_tests)]
#![cfg_attr(rune_nightly, feature(rustdoc_missing_doc_code_examples))]
#![cfg_attr(rune_nightly, deny(rustdoc::missing_doc_code_examples))]
#![cfg_attr(rune_nightly, feature(core_intrinsics))]
#![cfg_attr(rune_nightly, feature(dropck_eyepatch))]
#![cfg_attr(rune_nightly, feature(min_specialization))]
Expand Down Expand Up @@ -135,6 +139,8 @@ pub(crate) mod ptr;
#[doc(hidden)]
pub mod slice;

pub mod callable;

pub mod prelude {
//! Prelude for common traits used in combination with this crate which
//! matches the behavior of the std prelude.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,29 @@
//! Memory limits for Rune.
//!
//! This module contains methods which allows for limiting the memory use of the
//! virtual machine to abide by the specified budget.
//! virtual machine to abide by the specified memory limit.
//!
//! By default memory limits are disabled, but can be enabled by wrapping your
//! function call or future in [with].
//!
//! # Limitations
//!
//! Limiting is plugged in at the [Rust allocator level], and does not account
//! for allocator overhead. Allocator overhead comes about because an allocator
//! needs to use some extra system memory to perform internal bookkeeping.
//! Usually this should not be an issue, because the allocator overhead should
//! be a fragment of memory use. But the exact details would depend on the
//! [global allocator] used.
//!
//! As an example, see the [implementation notes for jemalloc].
//!
//! [implementation notes for jemalloc]:
//! http://jemalloc.net/jemalloc.3.html#implementation_notes
//! [Rust allocator level]: https://doc.rust-lang.org/alloc/alloc/index.html
//! [global allocator]:
//! https://doc.rust-lang.org/alloc/alloc/trait.GlobalAlloc.html
#[cfg_attr(feature = "std", path = "limit/std.rs")]
#[cfg_attr(feature = "std", path = "std.rs")]
mod no_std;

use core::future::Future;
Expand All @@ -15,17 +32,32 @@ use core::task::{Context, Poll};

use pin_project::pin_project;

use crate::callable::Callable;

/// Something being budgeted.
///
/// See [`with`].
#[pin_project]
pub struct Memory<T> {
/// The current budget.
budget: usize,
/// The current limit.
memory: usize,
/// The thing being budgeted.
#[pin]
value: T,
}

/// Wrap the given value with a memory limit.
/// Wrap the given value with a memory limit. Using a value of [`usize::MAX`]
/// effectively disables the memory limit.
///
/// The following things can be wrapped:
/// * A [`FnOnce`] closure, like `with(|| println!("Hello World")).call()`.
/// * A [`Future`], like `with(async { /* async work */ }).await`;
///
/// It's also possible to wrap other wrappers which implement [`Callable`].
///
/// See the [module level documentation] for more details.
///
/// [module level documentation]: crate::limit
///
/// # Examples
///
Expand All @@ -48,7 +80,7 @@ pub struct Memory<T> {
/// # Ok::<_, rune::alloc::Error>(())
/// ```
///
/// Overloading the limit. Note that this happens because while the vector is
/// Breaching the limit. Note that this happens because while the vector is
/// growing it might both over-allocate, and hold onto two allocations
/// simultaneously.
///
Expand All @@ -68,8 +100,8 @@ pub struct Memory<T> {
///
/// assert!(f.call().is_err());
/// ```
pub fn with<T>(budget: usize, value: T) -> Memory<T> {
Memory { budget, value }
pub fn with<T>(memory: usize, value: T) -> Memory<T> {
Memory { memory, value }
}

/// Get remaining memory that may be allocated.
Expand Down Expand Up @@ -122,9 +154,9 @@ impl Drop for MemoryGuard {
}
}

impl<T, O> Memory<T>
impl<T> Memory<T>
where
T: FnOnce() -> O,
T: Callable,
{
/// Call the wrapped function, replacing the current budget and restoring it
/// once the function call completes.
Expand Down Expand Up @@ -193,9 +225,21 @@ where
/// assert_eq!(limit::get(), usize::MAX);
/// # Ok::<_, rune::alloc::Error>(())
/// ```
pub fn call(self) -> O {
let _guard = MemoryGuard(self::no_std::rune_memory_replace(self.budget));
(self.value)()
pub fn call(self) -> T::Output {
Callable::call(self)
}
}

impl<T> Callable for Memory<T>
where
T: Callable,
{
type Output = T::Output;

#[inline]
fn call(self) -> Self::Output {
let _guard = MemoryGuard(self::no_std::rune_memory_replace(self.memory));
self.value.call()
}
}

Expand Down Expand Up @@ -281,9 +325,9 @@ where
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.project();

let _guard = MemoryGuard(self::no_std::rune_memory_replace(*this.budget));
let _guard = MemoryGuard(self::no_std::rune_memory_replace(*this.memory));
let poll = this.value.poll(cx);
*this.budget = self::no_std::rune_memory_get();
*this.memory = self::no_std::rune_memory_get();
poll
}
}
71 changes: 37 additions & 34 deletions crates/rune-alloc/src/slice.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
pub(crate) use self::iter::{RawIter, RawIterMut};
pub use self::iter::{RawIter, RawIterMut};
pub(crate) mod iter;

use crate::alloc::{Allocator, Global};
Expand Down Expand Up @@ -136,46 +136,49 @@ pub(crate) mod hack {
where
T: TryClone,
{
#[inline]
fn to_vec<A: Allocator>(s: &[Self], alloc: A) -> Result<Vec<Self, A>, Error> {
struct DropGuard<'a, T, A: Allocator> {
vec: &'a mut Vec<T, A>,
num_init: usize,
}
impl<'a, T, A: Allocator> Drop for DropGuard<'a, T, A> {
#[inline]
fn drop(&mut self) {
// SAFETY:
// items were marked initialized in the loop below
unsafe {
self.vec.set_len(self.num_init);
default_fn! {
#[inline]
fn to_vec<A: Allocator>(s: &[Self], alloc: A) -> Result<Vec<Self, A>, Error> {
struct DropGuard<'a, T, A: Allocator> {
vec: &'a mut Vec<T, A>,
num_init: usize,
}

impl<'a, T, A: Allocator> Drop for DropGuard<'a, T, A> {
#[inline]
fn drop(&mut self) {
// SAFETY:
// items were marked initialized in the loop below
unsafe {
self.vec.set_len(self.num_init);
}
}
}
let mut vec = Vec::try_with_capacity_in(s.len(), alloc)?;
let mut guard = DropGuard {
vec: &mut vec,
num_init: 0,
};
let slots = guard.vec.spare_capacity_mut();
// .take(slots.len()) is necessary for LLVM to remove bounds checks
// and has better codegen than zip.
for (i, b) in s.iter().enumerate().take(slots.len()) {
guard.num_init = i;
slots[i].write(b.try_clone()?);
}
core::mem::forget(guard);
// SAFETY:
// the vec was allocated and initialized above to at least this length.
unsafe {
vec.set_len(s.len());
}
Ok(vec)
}
let mut vec = Vec::try_with_capacity_in(s.len(), alloc)?;
let mut guard = DropGuard {
vec: &mut vec,
num_init: 0,
};
let slots = guard.vec.spare_capacity_mut();
// .take(slots.len()) is necessary for LLVM to remove bounds checks
// and has better codegen than zip.
for (i, b) in s.iter().enumerate().take(slots.len()) {
guard.num_init = i;
slots[i].write(b.try_clone()?);
}
core::mem::forget(guard);
// SAFETY:
// the vec was allocated and initialized above to at least this length.
unsafe {
vec.set_len(s.len());
}
Ok(vec)
}
}

#[cfg(rune_nightly)]
impl<T: TryCopy> ConvertVec for T {
impl<T: crate::clone::TryCopy> ConvertVec for T {
#[inline]
fn to_vec<A: Allocator>(s: &[Self], alloc: A) -> Result<Vec<Self, A>, Error> {
let mut v = Vec::try_with_capacity_in(s.len(), alloc)?;
Expand Down
28 changes: 1 addition & 27 deletions crates/rune-alloc/src/string/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -535,24 +535,6 @@ impl<A: Allocator> PartialEq for FromUtf8Error<A> {

impl<A: Allocator> Eq for FromUtf8Error<A> {}

/// A possible error value when converting a `String` from a UTF-16 byte slice.
///
/// This type is the error type for the [`from_utf16`] method on [`String`].
///
/// [`from_utf16`]: String::from_utf16
///
/// # Examples
///
/// ```
/// // 𝄞mu<invalid>ic
/// let v = &[0xD834, 0xDD1E, 0x006d, 0x0075,
/// 0xD800, 0x0069, 0x0063];
///
/// assert!(String::from_utf16(v).is_err());
/// ```
#[derive(Debug)]
pub(crate) struct FromUtf16Error(());

impl<A: Allocator> String<A> {
/// Creates a new empty `String`.
///
Expand Down Expand Up @@ -1087,7 +1069,7 @@ impl<A: Allocator> String<A> {
/// ```
#[inline]
#[must_use]
pub(crate) fn as_bytes(&self) -> &[u8] {
pub fn as_bytes(&self) -> &[u8] {
&self.vec
}

Expand Down Expand Up @@ -1790,16 +1772,8 @@ impl<A: Allocator> fmt::Display for FromUtf8Error<A> {
}
}

impl fmt::Display for FromUtf16Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt("invalid utf-16: lone surrogate found", f)
}
}

#[cfg(feature = "std")]
impl<A: Allocator> std::error::Error for FromUtf8Error<A> {}
#[cfg(feature = "std")]
impl std::error::Error for FromUtf16Error {}

impl<A: Allocator + Clone> TryClone for String<A> {
fn try_clone(&self) -> Result<Self, Error> {
Expand Down
41 changes: 1 addition & 40 deletions crates/rune-alloc/src/vec/into_iter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,47 +82,8 @@ impl<T, A: Allocator> IntoIter<T, A> {
ptr::slice_from_raw_parts_mut(self.ptr as *mut T, self.len())
}

/// Drops remaining elements and relinquishes the backing allocation. This
/// method guarantees it won't panic before relinquishing the backing
/// allocation.
///
/// This is roughly equivalent to the following, but more efficient
///
/// ```
/// use rune::alloc::Vec;
///
/// # #[cfg(not(miri))]
/// # fn main() -> Result<(), rune_alloc::Error> {
/// # let mut into_iter = Vec::<u8>::try_with_capacity(10)?.into_iter();
/// let mut into_iter = std::mem::replace(&mut into_iter, Vec::new().into_iter());
/// (&mut into_iter).for_each(drop);
/// std::mem::forget(into_iter);
/// # Ok(())
/// # }
/// # #[cfg(miri)] fn main() {}
/// ```
///
/// This method is used by in-place iteration, refer to the vec::in_place_collect
/// documentation for an overview.
pub(super) fn forget_allocation_drop_remaining(&mut self) {
let remaining = self.as_raw_mut_slice();

// overwrite the individual fields instead of creating a new
// struct and then overwriting &mut self.
// this creates less assembly
self.cap = 0;
self.buf = unsafe { NonNull::new_unchecked(RawVec::NEW.ptr()) };
self.ptr = self.buf.as_ptr();
self.end = self.buf.as_ptr();

// Dropping the remaining elements can panic, so this needs to be
// done only after updating the other fields.
unsafe {
ptr::drop_in_place(remaining);
}
}

/// Forgets to Drop the remaining elements while still allowing the backing allocation to be freed.
#[cfg(rune_nightly)]
pub(crate) fn forget_remaining_elements(&mut self) {
// For th ZST case, it is crucial that we mutate `end` here, not `ptr`.
// `ptr` must stay aligned, while `end` may be unaligned.
Expand Down
Loading

0 comments on commit 6e24f08

Please sign in to comment.