mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-06-09 20:11:09 +00:00
Refactor and document allocator (#4855)
* Clarify code a bit. * Move code around. * Introduce `Order`. * Introduce `Link` structure. * Get rid of ptr_offset This is beneficial since ptr_offset is essentially makes us handle two different address spaces, global (i.e. `mem`) and heap local and without it things are becoming simpler. * Rename PREFIX_SIZE to HEADER_SIZE. This will come in the next commits. * Introduce a separate `Memory` trait. This is not necessary, but will come in handy for the upcoming changes. * Rename `ptr` to `header_ptr` where makes sense. * Introduce a `Header` type. * Make `bump` dumber. This allows us to pull `HEADER_SIZE` to see that we actually allocate `order.size() + HEADER_SIZE`. * Clean up. * Introduce a freelists struct. * Update documentation. * Make Sized requirement optional to make the PR truly back-compatible. * Apply suggestions from code review Co-Authored-By: Gavin Wood <gavin@parity.io> * Apply suggestions from code review Co-Authored-By: Bastian Köcher <bkchr@users.noreply.github.com> Co-authored-by: Gavin Wood <github@gavwood.com> Co-authored-by: Bastian Köcher <bkchr@users.noreply.github.com>
This commit is contained in:
@@ -16,42 +16,40 @@
|
||||
|
||||
//! This module implements a freeing-bump allocator.
|
||||
//!
|
||||
//! The algorithm is as follows:
|
||||
//! We store `N` linked list heads, where `N` is the total number of sizes
|
||||
//! of allocations to support. A simple set is powers of two from 8 bytes
|
||||
//! to 16,777,216 bytes (2^3 - 2^24 inclusive), resulting in `N = 22`:
|
||||
//! The heap is a continuous linear memory and chunks are allocated using a bump allocator.
|
||||
//!
|
||||
//! ```ignore
|
||||
//! let mut heads [u64; N] = [0; N];
|
||||
//! fn size(n: u64) -> u64 { 8 << n }
|
||||
//! let mut bumper = 0;
|
||||
//! fn bump(n: u64) -> u64 { let res = bumper; bumper += n; res }
|
||||
//! +-------------+-------------------------------------------------+
|
||||
//! | <allocated> | <unallocated> |
|
||||
//! +-------------+-------------------------------------------------+
|
||||
//! ^
|
||||
//! |_ bumper
|
||||
//! ```
|
||||
//!
|
||||
//! We assume there is a slab of heap to be allocated:
|
||||
//! Only allocations with sizes of power of two can be allocated. If the incoming request has a non
|
||||
//! power of two size it is increased to the nearest power of two. The power of two of size is
|
||||
//! referred as **an order**.
|
||||
//!
|
||||
//! ```ignore
|
||||
//! let mut heap = [0u8; HEAP_SIZE];
|
||||
//! ```
|
||||
//! Each allocation has a header immediately preceding to it. The header is always 8 bytes and can
|
||||
//! be of two types: free and occupied.
|
||||
//!
|
||||
//! Whenever we allocate, we select the lowest linked list item size that
|
||||
//! will fit the allocation (i.e. the next highest power of two).
|
||||
//! We then check to see if the linked list is empty. If empty, we use
|
||||
//! the bump allocator to get the allocation with an extra 8 bytes
|
||||
//! preceding it. We initialise those preceding 8 bytes to identify the
|
||||
//! list to which it belongs. If it is not empty, we unlink the first item from
|
||||
//! the linked list and then reset the 8 preceding bytes so they now record
|
||||
//! the identity of the linked list.
|
||||
//! For implementing freeing we maintain a linked lists for each order. The maximum supported
|
||||
//! allocation size is capped, therefore the number of orders and thus the linked lists is as well
|
||||
//! limited.
|
||||
//!
|
||||
//! To deallocate we use the preceding 8 bytes of the allocation to knit
|
||||
//! back the allocation into the linked list from the head.
|
||||
//! When the allocater serves an allocation request it first checks the linked list for the respective
|
||||
//! order. If it doesn't have any free chunks, the allocator requests memory from the bump allocator.
|
||||
//! In any case the order is stored in the header of the allocation.
|
||||
//!
|
||||
//! Upon deallocation we get the order of the allocation from its header and then add that
|
||||
//! allocation to the linked list for the respective order.
|
||||
|
||||
use crate::Error;
|
||||
use sp_std::{convert::{TryFrom, TryInto}, ops::Range};
|
||||
use sp_std::{convert::{TryFrom, TryInto}, ops::{Range, Index, IndexMut}};
|
||||
use sp_wasm_interface::{Pointer, WordSize};
|
||||
|
||||
// The pointers need to be aligned to 8 bytes. This is because the
|
||||
// maximum value type handled by wasm32 is u64.
|
||||
/// The minimal alignment guaranteed by this allocator. The alignment of 8 is choosen because it is
|
||||
/// the alignment guaranteed by wasm32.
|
||||
const ALIGNMENT: u32 = 8;
|
||||
|
||||
// The pointer returned by `allocate()` needs to fulfill the alignment
|
||||
@@ -65,17 +63,7 @@ const MIN_POSSIBLE_ALLOCATION: u32 = 8;
|
||||
|
||||
// Each pointer is prefixed with 8 bytes, which identify the list index
|
||||
// to which it belongs.
|
||||
const PREFIX_SIZE: u32 = 8;
|
||||
|
||||
/// An implementation of freeing bump allocator.
|
||||
///
|
||||
/// Refer to the module-level documentation for further details.
|
||||
pub struct FreeingBumpHeapAllocator {
|
||||
bumper: u32,
|
||||
heads: [u32; N],
|
||||
ptr_offset: u32,
|
||||
total_size: u32,
|
||||
}
|
||||
const HEADER_SIZE: u32 = 8;
|
||||
|
||||
/// Create an allocator error.
|
||||
fn error(msg: &'static str) -> Error {
|
||||
@@ -93,6 +81,219 @@ macro_rules! trace {
|
||||
}
|
||||
}
|
||||
|
||||
/// The exponent for the power of two sized block adjusted to the minimum size.
|
||||
///
|
||||
/// This way, if `MIN_POSSIBLE_ALLOCATION == 8`, we would get:
|
||||
///
|
||||
/// power_of_two_size | order
|
||||
/// 8 | 0
|
||||
/// 16 | 1
|
||||
/// 32 | 2
|
||||
/// 64 | 3
|
||||
///
|
||||
/// and so on.
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
|
||||
struct Order(u32);
|
||||
|
||||
impl Order {
|
||||
/// Create `Order` object from a raw order.
|
||||
///
|
||||
/// Returns `Err` if it is greater than the maximum supported order.
|
||||
fn from_raw(order: u32) -> Result<Self, Error> {
|
||||
if order < N as u32 {
|
||||
Ok(Self(order))
|
||||
} else {
|
||||
Err(error("invalid order"))
|
||||
}
|
||||
}
|
||||
|
||||
/// Compute the order by the given size
|
||||
///
|
||||
/// The size is clamped, so that the following holds:
|
||||
///
|
||||
/// `MIN_POSSIBLE_ALLOCATION <= size <= MAX_POSSIBLE_ALLOCATION`
|
||||
fn from_size(size: u32) -> Result<Self, Error> {
|
||||
let clamped_size = if size > MAX_POSSIBLE_ALLOCATION {
|
||||
return Err(Error::RequestedAllocationTooLarge);
|
||||
} else if size < MIN_POSSIBLE_ALLOCATION {
|
||||
MIN_POSSIBLE_ALLOCATION
|
||||
} else {
|
||||
size
|
||||
};
|
||||
|
||||
// Round the clamped size to the next power of two.
|
||||
//
|
||||
// It returns the unchanged value if the value is already a power of two.
|
||||
let power_of_two_size = clamped_size.next_power_of_two();
|
||||
|
||||
// Compute the number of trailing zeroes to get the order. We adjust it by the number of
|
||||
// trailing zeroes in the minimum possible allocation.
|
||||
let order = power_of_two_size.trailing_zeros() - MIN_POSSIBLE_ALLOCATION.trailing_zeros();
|
||||
|
||||
Ok(Self(order))
|
||||
}
|
||||
|
||||
/// Returns the corresponding size for this order.
|
||||
///
|
||||
/// Note that it is always a power of two.
|
||||
fn size(&self) -> u32 {
|
||||
MIN_POSSIBLE_ALLOCATION << self.0
|
||||
}
|
||||
|
||||
/// Extract the order as `u32`.
|
||||
fn into_raw(self) -> u32 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// A marker for denoting the end of the linked list.
|
||||
const EMPTY_MARKER: u32 = u32::max_value();
|
||||
|
||||
/// A link between headers in the free list.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum Link {
|
||||
/// Null, denotes that there is no next element.
|
||||
Null,
|
||||
/// Link to the next element represented as a pointer to the a header.
|
||||
Ptr(u32),
|
||||
}
|
||||
|
||||
impl Link {
|
||||
/// Creates a link from raw value.
|
||||
fn from_raw(raw: u32) -> Self {
|
||||
if raw != EMPTY_MARKER {
|
||||
Self::Ptr(raw)
|
||||
} else {
|
||||
Self::Null
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts this link into a raw u32.
|
||||
fn into_raw(self) -> u32 {
|
||||
match self {
|
||||
Self::Null => EMPTY_MARKER,
|
||||
Self::Ptr(ptr) => ptr,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A header of an allocation.
|
||||
///
|
||||
/// The header is encoded in memory as follows.
|
||||
///
|
||||
/// ## Free header
|
||||
///
|
||||
/// ```ignore
|
||||
/// 64 32 0
|
||||
// +--------------+-------------------+
|
||||
/// | 0 | next element link |
|
||||
/// +--------------+-------------------+
|
||||
/// ```
|
||||
///
|
||||
/// ## Occupied header
|
||||
///
|
||||
/// ```ignore
|
||||
/// 64 32 0
|
||||
// +--------------+-------------------+
|
||||
/// | 1 | order |
|
||||
/// +--------------+-------------------+
|
||||
/// ```
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
enum Header {
|
||||
/// A free header contains a link to the next element to form a free linked list.
|
||||
Free(Link),
|
||||
/// An occupied header has attached order to know in which free list we should put the
|
||||
/// allocation upon deallocation.
|
||||
Occupied(Order),
|
||||
}
|
||||
|
||||
impl Header {
|
||||
fn read_from<M: Memory + ?Sized>(memory: &M, header_ptr: u32) -> Result<Self, Error> {
|
||||
let raw_header = memory.read_le_u64(header_ptr)?;
|
||||
|
||||
// Check if the header represents an occupied or free allocation and extract the header data
|
||||
// by trimming (and discarding) the high bits.
|
||||
let occupied = raw_header & 0x00000001_00000000 != 0;
|
||||
let header_data = raw_header as u32;
|
||||
|
||||
Ok(if occupied {
|
||||
Self::Occupied(Order::from_raw(header_data)?)
|
||||
} else {
|
||||
Self::Free(Link::from_raw(header_data))
|
||||
})
|
||||
}
|
||||
|
||||
/// Write out this header to memory.
|
||||
fn write_into<M: Memory + ?Sized>(&self, memory: &mut M, header_ptr: u32) -> Result<(), Error> {
|
||||
let (header_data, occupied_mask) = match *self {
|
||||
Self::Occupied(order) => (order.into_raw(), 0x00000001_00000000),
|
||||
Self::Free(link) => (link.into_raw(), 0x00000000_00000000),
|
||||
};
|
||||
let raw_header = header_data as u64 | occupied_mask;
|
||||
memory.write_le_u64(header_ptr, raw_header)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns the order of the allocation if this is an occupied header.
|
||||
fn into_occupied(self) -> Option<Order> {
|
||||
match self {
|
||||
Self::Occupied(order) => Some(order),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the link to the next element in the free list if this is a free header.
|
||||
fn into_free(self) -> Option<Link> {
|
||||
match self {
|
||||
Self::Free(link) => Some(link),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This struct represents a collection of intrusive linked lists for each order.
|
||||
struct FreeLists {
|
||||
heads: [Link; N],
|
||||
}
|
||||
|
||||
impl FreeLists {
|
||||
/// Creates the free empty lists.
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
heads: [Link::Null; N]
|
||||
}
|
||||
}
|
||||
|
||||
/// Replaces a given link for the specified order and returns the old one.
|
||||
fn replace(&mut self, order: Order, new: Link) -> Link {
|
||||
let prev = self[order];
|
||||
self[order] = new;
|
||||
prev
|
||||
}
|
||||
}
|
||||
|
||||
impl Index<Order> for FreeLists {
|
||||
type Output = Link;
|
||||
fn index(&self, index: Order) -> &Link {
|
||||
&self.heads[index.0 as usize]
|
||||
}
|
||||
}
|
||||
|
||||
impl IndexMut<Order> for FreeLists {
|
||||
fn index_mut(&mut self, index: Order) -> &mut Link {
|
||||
&mut self.heads[index.0 as usize]
|
||||
}
|
||||
}
|
||||
|
||||
/// An implementation of freeing bump allocator.
|
||||
///
|
||||
/// Refer to the module-level documentation for further details.
|
||||
pub struct FreeingBumpHeapAllocator {
|
||||
bumper: u32,
|
||||
free_lists: FreeLists,
|
||||
total_size: u32,
|
||||
}
|
||||
|
||||
impl FreeingBumpHeapAllocator {
|
||||
/// Creates a new allocation heap which follows a freeing-bump strategy.
|
||||
/// The maximum size which can be allocated at once is 16 MiB.
|
||||
@@ -101,13 +302,11 @@ impl FreeingBumpHeapAllocator {
|
||||
///
|
||||
/// - `heap_base` - the offset from the beginning of the linear memory where the heap starts.
|
||||
pub fn new(heap_base: u32) -> Self {
|
||||
// ptr_offset is the next alignment boundary on or after heap_base.
|
||||
let ptr_offset = (heap_base + ALIGNMENT - 1) / ALIGNMENT * ALIGNMENT;
|
||||
let aligned_heap_base = (heap_base + ALIGNMENT - 1) / ALIGNMENT * ALIGNMENT;
|
||||
|
||||
FreeingBumpHeapAllocator {
|
||||
bumper: 0,
|
||||
heads: [u32::max_value(); N],
|
||||
ptr_offset,
|
||||
bumper: aligned_heap_base,
|
||||
free_lists: FreeLists::new(),
|
||||
total_size: 0,
|
||||
}
|
||||
}
|
||||
@@ -122,46 +321,42 @@ impl FreeingBumpHeapAllocator {
|
||||
///
|
||||
/// - `mem` - a slice representing the linear memory on which this allocator operates.
|
||||
/// - `size` - size in bytes of the allocation request
|
||||
pub fn allocate(&mut self, mem: &mut [u8], size: WordSize) -> Result<Pointer<u8>, Error> {
|
||||
let mem_size = u32::try_from(mem.len())
|
||||
.expect("size of Wasm linear memory is <2^32; qed");
|
||||
let max_heap_size = mem_size - self.ptr_offset;
|
||||
pub fn allocate<M: Memory + ?Sized>(
|
||||
&mut self,
|
||||
mem: &mut M,
|
||||
size: WordSize,
|
||||
) -> Result<Pointer<u8>, Error> {
|
||||
let order = Order::from_size(size)?;
|
||||
|
||||
if size > MAX_POSSIBLE_ALLOCATION {
|
||||
return Err(Error::RequestedAllocationTooLarge);
|
||||
}
|
||||
let header_ptr: u32 = match self.free_lists[order] {
|
||||
Link::Ptr(header_ptr) => {
|
||||
assert!(
|
||||
header_ptr + order.size() + HEADER_SIZE <= mem.size(),
|
||||
"Pointer is looked up in list of free entries, into which
|
||||
only valid values are inserted; qed"
|
||||
);
|
||||
|
||||
let size = size.max(MIN_POSSIBLE_ALLOCATION);
|
||||
let item_size = size.next_power_of_two();
|
||||
if item_size + PREFIX_SIZE + self.total_size > max_heap_size {
|
||||
return Err(Error::AllocatorOutOfSpace);
|
||||
}
|
||||
// Remove this header from the free list.
|
||||
let next_free = Header::read_from(mem, header_ptr)?
|
||||
.into_free()
|
||||
.ok_or_else(|| error("free list points to a occupied header"))?;
|
||||
self.free_lists[order] = next_free;
|
||||
|
||||
let list_index = (item_size.trailing_zeros() - 3) as usize;
|
||||
let ptr: u32 = if self.heads[list_index] != u32::max_value() {
|
||||
// Something from the free list
|
||||
let ptr = self.heads[list_index];
|
||||
assert!(
|
||||
ptr + item_size + PREFIX_SIZE <= max_heap_size,
|
||||
"Pointer is looked up in list of free entries, into which
|
||||
only valid values are inserted; qed"
|
||||
);
|
||||
|
||||
self.heads[list_index] = self.get_heap_u64(mem, ptr)?
|
||||
.try_into()
|
||||
.map_err(|_| error("read invalid free list pointer"))?;
|
||||
ptr
|
||||
} else {
|
||||
// Nothing to be freed. Bump.
|
||||
self.bump(item_size, max_heap_size)?
|
||||
header_ptr
|
||||
}
|
||||
Link::Null => {
|
||||
// Corresponding free list is empty. Allocate a new item.
|
||||
self.bump(order.size() + HEADER_SIZE, mem.size())?
|
||||
}
|
||||
};
|
||||
|
||||
self.set_heap_u64(mem, ptr, list_index as u64)?;
|
||||
// Write the order in the occupied header.
|
||||
Header::Occupied(order).write_into(mem, header_ptr)?;
|
||||
|
||||
self.total_size = self.total_size + item_size + PREFIX_SIZE;
|
||||
self.total_size += order.size() + HEADER_SIZE;
|
||||
trace!("Heap size is {} bytes after allocation", self.total_size);
|
||||
|
||||
Ok(Pointer::new(self.ptr_offset + ptr + PREFIX_SIZE))
|
||||
Ok(Pointer::new(header_ptr + HEADER_SIZE))
|
||||
}
|
||||
|
||||
/// Deallocates the space which was allocated for a pointer.
|
||||
@@ -170,80 +365,83 @@ impl FreeingBumpHeapAllocator {
|
||||
///
|
||||
/// - `mem` - a slice representing the linear memory on which this allocator operates.
|
||||
/// - `ptr` - pointer to the allocated chunk
|
||||
pub fn deallocate(&mut self, mem: &mut [u8], ptr: Pointer<u8>) -> Result<(), Error> {
|
||||
let ptr = u32::from(ptr) - self.ptr_offset;
|
||||
let ptr = ptr.checked_sub(PREFIX_SIZE).ok_or_else(||
|
||||
error("Invalid pointer for deallocation")
|
||||
)?;
|
||||
pub fn deallocate<M: Memory + ?Sized>(&mut self, mem: &mut M, ptr: Pointer<u8>) -> Result<(), Error> {
|
||||
let header_ptr = u32::from(ptr)
|
||||
.checked_sub(HEADER_SIZE)
|
||||
.ok_or_else(|| error("Invalid pointer for deallocation"))?;
|
||||
|
||||
let list_index: usize = self.get_heap_u64(mem, ptr)?
|
||||
.try_into()
|
||||
.map_err(|_| error("read invalid list index"))?;
|
||||
if list_index > self.heads.len() {
|
||||
return Err(error("read invalid list index"));
|
||||
}
|
||||
self.set_heap_u64(mem, ptr, self.heads[list_index] as u64)?;
|
||||
self.heads[list_index] = ptr;
|
||||
let order = Header::read_from(mem, header_ptr)?
|
||||
.into_occupied()
|
||||
.ok_or_else(|| error("the allocation points to an empty header"))?;
|
||||
|
||||
let item_size = Self::get_item_size_from_index(list_index);
|
||||
self.total_size = self.total_size.checked_sub(item_size as u32 + PREFIX_SIZE)
|
||||
// Update the just freed header and knit it back to the free list.
|
||||
let prev_head = self.free_lists.replace(order, Link::Ptr(header_ptr));
|
||||
Header::Free(prev_head).write_into(mem, header_ptr)?;
|
||||
|
||||
// Do the total_size book keeping.
|
||||
self.total_size = self
|
||||
.total_size
|
||||
.checked_sub(order.size() + HEADER_SIZE)
|
||||
.ok_or_else(|| error("Unable to subtract from total heap size without overflow"))?;
|
||||
trace!("Heap size is {} bytes after deallocation", self.total_size);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Increases the `bumper` by `item_size + PREFIX_SIZE`.
|
||||
/// Increases the `bumper` by `size`.
|
||||
///
|
||||
/// Returns the `bumper` from before the increase.
|
||||
/// Returns an `Error::AllocatorOutOfSpace` if the operation
|
||||
/// would exhaust the heap.
|
||||
fn bump(&mut self, item_size: u32, max_heap_size: u32) -> Result<u32, Error> {
|
||||
if self.bumper + PREFIX_SIZE + item_size > max_heap_size {
|
||||
fn bump(&mut self, size: u32, heap_end: u32) -> Result<u32, Error> {
|
||||
if self.bumper + size > heap_end {
|
||||
return Err(Error::AllocatorOutOfSpace);
|
||||
}
|
||||
|
||||
let res = self.bumper;
|
||||
self.bumper += item_size + PREFIX_SIZE;
|
||||
self.bumper += size;
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
fn get_item_size_from_index(index: usize) -> usize {
|
||||
// we shift 1 by three places, since the first possible item size is 8
|
||||
1 << 3 << index
|
||||
}
|
||||
/// A trait for abstraction of accesses to linear memory.
|
||||
pub trait Memory {
|
||||
/// Read a u64 from the heap in LE form. Used to read heap allocation prefixes.
|
||||
fn read_le_u64(&self, ptr: u32) -> Result<u64, Error>;
|
||||
/// Write a u64 to the heap in LE form. Used to write heap allocation prefixes.
|
||||
fn write_le_u64(&mut self, ptr: u32, val: u64) -> Result<(), Error>;
|
||||
/// Returns the full size of the memory.
|
||||
fn size(&self) -> u32;
|
||||
}
|
||||
|
||||
// Read a u64 from the heap in LE form. Used to read heap allocation prefixes.
|
||||
fn get_heap_u64(&self, heap: &[u8], offset: u32) -> Result<u64, Error> {
|
||||
let range = self.heap_range(offset, 8, heap.len())
|
||||
.ok_or_else(|| error("read out of heap bounds"))?;
|
||||
let bytes = heap[range].try_into()
|
||||
impl Memory for [u8] {
|
||||
fn read_le_u64(&self, ptr: u32) -> Result<u64, Error> {
|
||||
let range =
|
||||
heap_range(ptr, 8, self.len()).ok_or_else(|| error("read out of heap bounds"))?;
|
||||
let bytes = self[range]
|
||||
.try_into()
|
||||
.expect("[u8] slice of length 8 must be convertible to [u8; 8]");
|
||||
Ok(u64::from_le_bytes(bytes))
|
||||
}
|
||||
|
||||
// Write a u64 to the heap in LE form. Used to write heap allocation prefixes.
|
||||
fn set_heap_u64(&self, heap: &mut [u8], offset: u32, val: u64) -> Result<(), Error> {
|
||||
let range = self.heap_range(offset, 8, heap.len())
|
||||
.ok_or_else(|| error("write out of heap bounds"))?;
|
||||
fn write_le_u64(&mut self, ptr: u32, val: u64) -> Result<(), Error> {
|
||||
let range =
|
||||
heap_range(ptr, 8, self.len()).ok_or_else(|| error("write out of heap bounds"))?;
|
||||
let bytes = val.to_le_bytes();
|
||||
&mut heap[range].copy_from_slice(&bytes[..]);
|
||||
&mut self[range].copy_from_slice(&bytes[..]);
|
||||
Ok(())
|
||||
}
|
||||
fn size(&self) -> u32 {
|
||||
u32::try_from(self.len()).expect("size of Wasm linear memory is <2^32; qed")
|
||||
}
|
||||
}
|
||||
|
||||
fn heap_range(&self, offset: u32, length: u32, heap_len: usize) -> Option<Range<usize>> {
|
||||
let start = offset
|
||||
.checked_add(self.ptr_offset)?
|
||||
as usize;
|
||||
let end = offset
|
||||
.checked_add(self.ptr_offset)?
|
||||
.checked_add(length)?
|
||||
as usize;
|
||||
if end <= heap_len {
|
||||
Some(start..end)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
fn heap_range(offset: u32, length: u32, heap_len: usize) -> Option<Range<usize>> {
|
||||
let start = offset as usize;
|
||||
let end = offset.checked_add(length)? as usize;
|
||||
if end <= heap_len {
|
||||
Some(start..end)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
@@ -268,8 +466,8 @@ mod tests {
|
||||
let ptr = heap.allocate(&mut mem[..], 1).unwrap();
|
||||
|
||||
// then
|
||||
// returned pointer must start right after `PREFIX_SIZE`
|
||||
assert_eq!(ptr, to_pointer(PREFIX_SIZE));
|
||||
// returned pointer must start right after `HEADER_SIZE`
|
||||
assert_eq!(ptr, to_pointer(HEADER_SIZE));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -300,14 +498,14 @@ mod tests {
|
||||
|
||||
// then
|
||||
// a prefix of 8 bytes is prepended to each pointer
|
||||
assert_eq!(ptr1, to_pointer(PREFIX_SIZE));
|
||||
assert_eq!(ptr1, to_pointer(HEADER_SIZE));
|
||||
|
||||
// the prefix of 8 bytes + the content of ptr1 padded to the lowest possible
|
||||
// item size of 8 bytes + the prefix of ptr1
|
||||
assert_eq!(ptr2, to_pointer(24));
|
||||
|
||||
// ptr2 + its content of 16 bytes + the prefix of 8 bytes
|
||||
assert_eq!(ptr3, to_pointer(24 + 16 + PREFIX_SIZE));
|
||||
assert_eq!(ptr3, to_pointer(24 + 16 + HEADER_SIZE));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -317,7 +515,7 @@ mod tests {
|
||||
let mut heap = FreeingBumpHeapAllocator::new(0);
|
||||
let ptr1 = heap.allocate(&mut mem[..], 1).unwrap();
|
||||
// the prefix of 8 bytes is prepended to the pointer
|
||||
assert_eq!(ptr1, to_pointer(PREFIX_SIZE));
|
||||
assert_eq!(ptr1, to_pointer(HEADER_SIZE));
|
||||
|
||||
let ptr2 = heap.allocate(&mut mem[..], 1).unwrap();
|
||||
// the prefix of 8 bytes + the content of ptr 1 is prepended to the pointer
|
||||
@@ -329,7 +527,7 @@ mod tests {
|
||||
// then
|
||||
// then the heads table should contain a pointer to the
|
||||
// prefix of ptr2 in the leftmost entry
|
||||
assert_eq!(heap.heads[0], u32::from(ptr2) - PREFIX_SIZE);
|
||||
assert_eq!(heap.free_lists.heads[0], Link::Ptr(u32::from(ptr2) - HEADER_SIZE));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -341,13 +539,13 @@ mod tests {
|
||||
|
||||
let ptr1 = heap.allocate(&mut mem[..], 1).unwrap();
|
||||
// the prefix of 8 bytes is prepended to the pointer
|
||||
assert_eq!(ptr1, to_pointer(padded_offset + PREFIX_SIZE));
|
||||
assert_eq!(ptr1, to_pointer(padded_offset + HEADER_SIZE));
|
||||
|
||||
let ptr2 = heap.allocate(&mut mem[..], 9).unwrap();
|
||||
// the padded_offset + the previously allocated ptr (8 bytes prefix +
|
||||
// 8 bytes content) + the prefix of 8 bytes which is prepended to the
|
||||
// current pointer
|
||||
assert_eq!(ptr2, to_pointer(padded_offset + 16 + PREFIX_SIZE));
|
||||
assert_eq!(ptr2, to_pointer(padded_offset + 16 + HEADER_SIZE));
|
||||
|
||||
// when
|
||||
heap.deallocate(&mut mem[..], ptr2).unwrap();
|
||||
@@ -355,8 +553,8 @@ mod tests {
|
||||
|
||||
// then
|
||||
// should have re-allocated
|
||||
assert_eq!(ptr3, to_pointer(padded_offset + 16 + PREFIX_SIZE));
|
||||
assert_eq!(heap.heads, [u32::max_value(); N]);
|
||||
assert_eq!(ptr3, to_pointer(padded_offset + 16 + HEADER_SIZE));
|
||||
assert_eq!(heap.free_lists.heads, [Link::Null; N]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -375,12 +573,12 @@ mod tests {
|
||||
heap.deallocate(&mut mem[..], ptr3).unwrap();
|
||||
|
||||
// then
|
||||
assert_eq!(heap.heads[0], u32::from(ptr3) - PREFIX_SIZE);
|
||||
assert_eq!(heap.free_lists.heads[0], Link::Ptr(u32::from(ptr3) - HEADER_SIZE));
|
||||
|
||||
let ptr4 = heap.allocate(&mut mem[..], 8).unwrap();
|
||||
assert_eq!(ptr4, ptr3);
|
||||
|
||||
assert_eq!(heap.heads[0], u32::from(ptr2) - PREFIX_SIZE);
|
||||
assert_eq!(heap.free_lists.heads[0], Link::Ptr(u32::from(ptr2) - HEADER_SIZE));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -404,8 +602,8 @@ mod tests {
|
||||
// given
|
||||
let mut mem = [0u8; PAGE_SIZE as usize];
|
||||
let mut heap = FreeingBumpHeapAllocator::new(0);
|
||||
let ptr1 = heap.allocate(&mut mem[..], (PAGE_SIZE / 2) - PREFIX_SIZE).unwrap();
|
||||
assert_eq!(ptr1, to_pointer(PREFIX_SIZE));
|
||||
let ptr1 = heap.allocate(&mut mem[..], (PAGE_SIZE / 2) - HEADER_SIZE).unwrap();
|
||||
assert_eq!(ptr1, to_pointer(HEADER_SIZE));
|
||||
|
||||
// when
|
||||
let ptr2 = heap.allocate(&mut mem[..], PAGE_SIZE / 2);
|
||||
@@ -428,7 +626,7 @@ mod tests {
|
||||
let ptr = heap.allocate(&mut mem[..], MAX_POSSIBLE_ALLOCATION).unwrap();
|
||||
|
||||
// then
|
||||
assert_eq!(ptr, to_pointer(PREFIX_SIZE));
|
||||
assert_eq!(ptr, to_pointer(HEADER_SIZE));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -454,7 +652,7 @@ mod tests {
|
||||
let mut heap = FreeingBumpHeapAllocator::new(0);
|
||||
|
||||
let ptr1 = heap.allocate(&mut mem[..], 32).unwrap();
|
||||
assert_eq!(ptr1, to_pointer(PREFIX_SIZE));
|
||||
assert_eq!(ptr1, to_pointer(HEADER_SIZE));
|
||||
heap.deallocate(&mut mem[..], ptr1).expect("failed freeing ptr1");
|
||||
assert_eq!(heap.total_size, 0);
|
||||
assert_eq!(heap.bumper, 40);
|
||||
@@ -466,7 +664,7 @@ mod tests {
|
||||
assert_eq!(heap.bumper, 64);
|
||||
|
||||
// when
|
||||
// the `bumper` value is equal to `max_heap_size` here and any
|
||||
// the `bumper` value is equal to `size` here and any
|
||||
// further allocation which would increment the bumper must fail.
|
||||
// we try to allocate 8 bytes here, which will increment the
|
||||
// bumper since no 8 byte item has been allocated+freed before.
|
||||
@@ -490,7 +688,7 @@ mod tests {
|
||||
heap.allocate(&mut mem[..], 9).unwrap();
|
||||
|
||||
// then
|
||||
assert_eq!(heap.total_size, PREFIX_SIZE + 16);
|
||||
assert_eq!(heap.total_size, HEADER_SIZE + 16);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -501,7 +699,7 @@ mod tests {
|
||||
|
||||
// when
|
||||
let ptr = heap.allocate(&mut mem[..], 42).unwrap();
|
||||
assert_eq!(ptr, to_pointer(16 + PREFIX_SIZE));
|
||||
assert_eq!(ptr, to_pointer(16 + HEADER_SIZE));
|
||||
heap.deallocate(&mut mem[..], ptr).unwrap();
|
||||
|
||||
// then
|
||||
@@ -528,23 +726,22 @@ mod tests {
|
||||
fn should_read_and_write_u64_correctly() {
|
||||
// given
|
||||
let mut mem = [0u8; PAGE_SIZE as usize];
|
||||
let heap = FreeingBumpHeapAllocator::new(16);
|
||||
|
||||
// when
|
||||
heap.set_heap_u64(&mut mem[..], 40, 4480113).unwrap();
|
||||
Memory::write_le_u64(mem.as_mut(), 40, 4480113).unwrap();
|
||||
|
||||
// then
|
||||
let value = heap.get_heap_u64(&mut mem[..], 40).unwrap();
|
||||
let value = Memory::read_le_u64(mem.as_mut(), 40).unwrap();
|
||||
assert_eq!(value, 4480113);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_get_item_size_from_index() {
|
||||
fn should_get_item_size_from_order() {
|
||||
// given
|
||||
let index = 0;
|
||||
let raw_order = 0;
|
||||
|
||||
// when
|
||||
let item_size = FreeingBumpHeapAllocator::get_item_size_from_index(index);
|
||||
let item_size = Order::from_raw(raw_order).unwrap().size();
|
||||
|
||||
// then
|
||||
assert_eq!(item_size, 8);
|
||||
@@ -553,10 +750,10 @@ mod tests {
|
||||
#[test]
|
||||
fn should_get_max_item_size_from_index() {
|
||||
// given
|
||||
let index = 21;
|
||||
let raw_order = 21;
|
||||
|
||||
// when
|
||||
let item_size = FreeingBumpHeapAllocator::get_item_size_from_index(index);
|
||||
let item_size = Order::from_raw(raw_order).unwrap().size();
|
||||
|
||||
// then
|
||||
assert_eq!(item_size as u32, MAX_POSSIBLE_ALLOCATION);
|
||||
@@ -568,10 +765,27 @@ mod tests {
|
||||
let mut heap = FreeingBumpHeapAllocator::new(0);
|
||||
|
||||
// Allocate and free some pointers
|
||||
let ptrs = (0..4).map(|_| heap.allocate(&mut mem, 8).unwrap()).collect::<Vec<_>>();
|
||||
ptrs.into_iter().for_each(|ptr| heap.deallocate(&mut mem, ptr).unwrap());
|
||||
let ptrs = (0..4).map(|_| heap.allocate(&mut mem[..], 8).unwrap()).collect::<Vec<_>>();
|
||||
ptrs.into_iter().for_each(|ptr| heap.deallocate(&mut mem[..], ptr).unwrap());
|
||||
|
||||
// Second time we should be able to allocate all of them again.
|
||||
let _ = (0..4).map(|_| heap.allocate(&mut mem, 8).unwrap()).collect::<Vec<_>>();
|
||||
let _ = (0..4).map(|_| heap.allocate(&mut mem[..], 8).unwrap()).collect::<Vec<_>>();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn header_read_write() {
|
||||
let roundtrip = |header: Header| {
|
||||
let mut memory = [0u8; 32];
|
||||
header.write_into(memory.as_mut(), 0).unwrap();
|
||||
|
||||
let read_header = Header::read_from(memory.as_mut(), 0).unwrap();
|
||||
assert_eq!(header, read_header);
|
||||
};
|
||||
|
||||
roundtrip(Header::Occupied(Order(0)));
|
||||
roundtrip(Header::Occupied(Order(1)));
|
||||
roundtrip(Header::Free(Link::Null));
|
||||
roundtrip(Header::Free(Link::Ptr(0)));
|
||||
roundtrip(Header::Free(Link::Ptr(4)));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user