Skip to content
This repository has been archived by the owner on Nov 15, 2023. It is now read-only.

Prevent double allocation of the payload when calling sp_io::storage::get #11523

Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
e5e94b1
Expose allocation stats in `FreeingBumpHeapAllocator`
koute May 25, 2022
94b191f
Return allocation stats when calling into the runtime
koute May 25, 2022
339ad2a
Bump `parity-scale-codec` to 3.1.3 (fork)
koute May 25, 2022
b78d9e3
Prevent double allocation of the payload when calling `sp_io::storage…
koute May 25, 2022
709fc78
Fix tests
koute Jun 3, 2022
6a1d839
Merge remote-tracking branch 'origin/master' into master_more_efficie…
koute Jun 3, 2022
68bd3d4
Remove unnecessary `mut`
koute Jun 3, 2022
29fe671
Enable the `bytes` feature for `parity-scale-codec` in `sp-runtime-in…
koute Jun 3, 2022
c062a9b
Update client/allocator/src/freeing_bump.rs
koute Jun 10, 2022
3cf7ce1
Bump `parity-scale-codec` to 3.1.3
koute Jun 10, 2022
f16ffca
Merge remote-tracking branch 'origin/master' into master_more_efficie…
koute Jun 10, 2022
476899c
Merge remote-tracking branch 'origin/master' into master_more_efficie…
koute Jun 20, 2022
8a2f8bf
Merge remote-tracking branch 'origin/master' into master_more_efficie…
koute Jun 20, 2022
fca70a7
Merge remote-tracking branch 'origin/master' into master_more_efficie…
koute Jun 27, 2022
155bb67
Merge remote-tracking branch 'origin/master' into master_more_efficie…
koute Jul 25, 2022
38c89e9
Merge remote-tracking branch 'origin/master' into master_more_efficie…
koute Jul 26, 2022
6cbd8d4
Merge remote-tracking branch 'origin/master' into master_more_efficie…
koute Jul 26, 2022
5690236
Merge remote-tracking branch 'origin/master' into master_more_efficie…
koute Jul 27, 2022
d333e24
Merge remote-tracking branch 'origin/master' into master_more_efficie…
koute Jul 28, 2022
9318154
Fix some of the UI tests
koute Jul 29, 2022
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
14 changes: 8 additions & 6 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -303,3 +303,6 @@ inherits = "release"
lto = "fat"
# https://doc.rust-lang.org/rustc/codegen-options/index.html#codegen-units
codegen-units = 1

[patch.crates-io]
parity-scale-codec = { git = "https://github.com/koute/parity-scale-codec.git", branch = "master_bytes_deserialization" }
97 changes: 56 additions & 41 deletions client/allocator/src/freeing_bump.rs
Original file line number Diff line number Diff line change
Expand Up @@ -314,27 +314,50 @@ impl IndexMut<Order> for FreeLists {
}
}

/// Memory allocation stats gathered during the lifetime of the allocator.
#[derive(Clone, Debug, Default)]
#[non_exhaustive]
pub struct AllocationStats {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should also track the maximum single allocation that ever happened. Maybe we can directly track this as percentage of the maximum allowed allocation?

(order.size() + HEADER_SIZE) / MAX_POSSIBLE_ALLOCATION

Copy link
Contributor Author

@koute koute Jun 10, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, we could do that, although:

  1. personally I'd probably just track its value directly instead of a percentage (e.g. imagine we hook this data to Prometheus/Grafana and we'll change the maximum possible value in the future - we can then have a situation where some nodes can have vastly different maximum allocation sizes depending on which version of the code they're running, and those will be reported exactly the same in the stats; I'd rather just add a field with the max_possible_allocation to this struct and the percentage can be later calculated when displaying this data)

  2. there are also some other metrics I'd like to add (e.g. how many allocations were made), but I simply omitted those since this is not yet the allocation stats PR (as I've said in the other comment this is just a preliminary implementation that isn't supposed to be complete), so I'd suggest we do this in a future PR (although if you want I can add this right now; its utility will be limited though so I'm not sure there's much point?)

/// The current number of bytes allocated.
///
/// This represents how many bytes are allocated *right now*.
pub bytes_allocated: u32,

/// The peak number of bytes ever allocated.
///
/// This is the maximum the `bytes_allocated` ever reached.
pub bytes_allocated_peak: u32,

/// The sum of every allocation ever made.
///
/// This increases every time a new allocation is made.
pub bytes_allocated_sum: u32,

/// The amount of address space (in bytes) used by the allocator.
///
/// This is calculated as the difference between the allocator's bumper
/// and the heap base.
///
/// Currently the bumper's only ever incremented, so this is simultaneously
/// the current value as well as the peak value.
pub address_space_used: u32,
}

/// An implementation of freeing bump allocator.
///
/// Refer to the module-level documentation for further details.
pub struct FreeingBumpHeapAllocator {
original_heap_base: u32,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit (feel free to ignore)

I know that currently this allocator is only used by wasmi and wasmtime which are currently using wasm32 modules.

But the allocator itself looks like it is designed to be generic, maybe makes sense to port all these u32 types to usize?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm... well, I don't think that would be worth it currently. It'd just make the whole thing more complicated for no real gain, and besides, this allocator was designed to be used with WASM if you read some of the comments (although yes, you're right, you could just use it as-is for something else).

If we ever decide to support wasm64-unknown-unknown in the very far future then we'll have to do something with this allocator, but I don't think that day will come anytime soon.

bumper: u32,
free_lists: FreeLists,
total_size: u32,
poisoned: bool,
max_total_size: u32,
max_bumper: u32,
last_observed_memory_size: u32,
stats: AllocationStats,
}

impl Drop for FreeingBumpHeapAllocator {
fn drop(&mut self) {
log::debug!(
target: LOG_TARGET,
"allocator being destroyed, max_total_size {}, max_bumper {}",
self.max_total_size,
self.max_bumper,
)
log::debug!(target: LOG_TARGET, "allocator dropped: {:?}", self.stats)
}
}

Expand All @@ -348,13 +371,12 @@ impl FreeingBumpHeapAllocator {
let aligned_heap_base = (heap_base + ALIGNMENT - 1) / ALIGNMENT * ALIGNMENT;

FreeingBumpHeapAllocator {
original_heap_base: aligned_heap_base,
bumper: aligned_heap_base,
free_lists: FreeLists::new(),
total_size: 0,
poisoned: false,
max_total_size: 0,
max_bumper: aligned_heap_base,
last_observed_memory_size: 0,
stats: AllocationStats::default(),
}
}

Expand Down Expand Up @@ -412,22 +434,13 @@ impl FreeingBumpHeapAllocator {
// Write the order in the occupied header.
Header::Occupied(order).write_into(mem, header_ptr)?;

self.total_size += order.size() + HEADER_SIZE;

log::trace!(
target: LOG_TARGET,
"after allocation, total_size = {}, bumper = {}.",
self.total_size,
self.bumper,
);
self.stats.bytes_allocated += order.size() + HEADER_SIZE;
self.stats.bytes_allocated_sum += order.size() + HEADER_SIZE;
self.stats.bytes_allocated_peak =
std::cmp::max(self.stats.bytes_allocated_peak, self.stats.bytes_allocated);
self.stats.address_space_used = self.bumper - self.original_heap_base;

// update trackers if needed.
if self.total_size > self.max_total_size {
self.max_total_size = self.total_size;
}
if self.bumper > self.max_bumper {
self.max_bumper = self.bumper;
}
log::trace!(target: LOG_TARGET, "after allocation: {:?}", self.stats);

bomb.disarm();
Ok(Pointer::new(header_ptr + HEADER_SIZE))
Expand Down Expand Up @@ -469,21 +482,23 @@ impl FreeingBumpHeapAllocator {
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
self.stats.bytes_allocated = self
.stats
.bytes_allocated
.checked_sub(order.size() + HEADER_SIZE)
.ok_or_else(|| error("Unable to subtract from total heap size without overflow"))?;
log::trace!(
"after deallocation, total_size = {}, bumper = {}.",
self.total_size,
self.bumper,
);
.ok_or_else(|| error("underflow of the currently allocated bytes count"))?;

log::trace!("after deallocation: {:?}", self.stats,);
koute marked this conversation as resolved.
Show resolved Hide resolved

bomb.disarm();
Ok(())
}

/// Returns the allocation stats for this allocator.
pub fn stats(&self) -> AllocationStats {
self.stats.clone()
}

/// Increases the `bumper` by `size`.
///
/// Returns the `bumper` from before the increase. Returns an `Error::AllocatorOutOfSpace` if
Expand Down Expand Up @@ -791,13 +806,13 @@ mod tests {
let ptr1 = heap.allocate(&mut mem[..], 32).unwrap();
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.stats.bytes_allocated, 0);
assert_eq!(heap.bumper, 40);

let ptr2 = heap.allocate(&mut mem[..], 16).unwrap();
assert_eq!(ptr2, to_pointer(48));
heap.deallocate(&mut mem[..], ptr2).expect("failed freeing ptr2");
assert_eq!(heap.total_size, 0);
assert_eq!(heap.stats.bytes_allocated, 0);
assert_eq!(heap.bumper, 64);

// when
Expand Down Expand Up @@ -825,7 +840,7 @@ mod tests {
heap.allocate(&mut mem[..], 9).unwrap();

// then
assert_eq!(heap.total_size, HEADER_SIZE + 16);
assert_eq!(heap.stats.bytes_allocated, HEADER_SIZE + 16);
}

#[test]
Expand All @@ -840,7 +855,7 @@ mod tests {
heap.deallocate(&mut mem[..], ptr).unwrap();

// then
assert_eq!(heap.total_size, 0);
assert_eq!(heap.stats.bytes_allocated, 0);
}

#[test]
Expand All @@ -856,7 +871,7 @@ mod tests {
}

// then
assert_eq!(heap.total_size, 0);
assert_eq!(heap.stats.bytes_allocated, 0);
}

#[test]
Expand Down
2 changes: 1 addition & 1 deletion client/allocator/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,4 @@ mod error;
mod freeing_bump;

pub use error::Error;
pub use freeing_bump::FreeingBumpHeapAllocator;
pub use freeing_bump::{AllocationStats, FreeingBumpHeapAllocator};
17 changes: 16 additions & 1 deletion client/executor/common/src/wasm_runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
use crate::error::Error;
use sp_wasm_interface::Value;

pub use sc_allocator::AllocationStats;

/// A method to be used to find the entrypoint when calling into the runtime
///
/// Contains variants on how to resolve wasm function that will be invoked.
Expand Down Expand Up @@ -78,7 +80,20 @@ pub trait WasmInstance: Send {
/// Before execution, instance is reset.
///
/// Returns the encoded result on success.
fn call(&mut self, method: InvokeMethod, data: &[u8]) -> Result<Vec<u8>, Error>;
fn call(&mut self, method: InvokeMethod, data: &[u8]) -> Result<Vec<u8>, Error> {
self.call_with_allocation_stats(method, data).0
}

/// Call a method on this WASM instance.
///
/// Before execution, instance is reset.
///
/// Returns the encoded result on success.
fn call_with_allocation_stats(
&mut self,
method: InvokeMethod,
data: &[u8],
) -> (Result<Vec<u8>, Error>, Option<AllocationStats>);

/// Call an exported method on this WASM instance.
///
Expand Down
49 changes: 47 additions & 2 deletions client/executor/src/native_executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ use std::{
use codec::{Decode, Encode};
use sc_executor_common::{
runtime_blob::RuntimeBlob,
wasm_runtime::{InvokeMethod, WasmInstance, WasmModule},
wasm_runtime::{AllocationStats, InvokeMethod, WasmInstance, WasmModule},
};
use sp_core::{
traits::{CodeExecutor, Externalities, RuntimeCode, RuntimeSpawn, RuntimeSpawnExt},
Expand Down Expand Up @@ -219,6 +219,47 @@ where
allow_missing_host_functions: bool,
export_name: &str,
call_data: &[u8],
) -> std::result::Result<Vec<u8>, Error> {
self.uncached_call_impl(
runtime_blob,
ext,
allow_missing_host_functions,
export_name,
call_data,
&mut None,
)
}

/// Same as `uncached_call`, except it also returns allocation statistics.
#[doc(hidden)] // We use this function in tests.
pub fn uncached_call_with_allocation_stats(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not always return the stats?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you asking why it's returning an Option? Or why isn't there just a single function instead of two where first variant returning the stats and the second not returning them?

(I assume that it's the latter, but I'll explain anyway for other people who might be reading.)

It is an Option because in theory there might not be any stats to return if we couldn't call into the runtime at all (e.g. because the passed export_name is missing). Otherwise we essentially always want to return those stats: we obviously want to do it when it returns an Ok, but we also want to do it when it returns an Err (e.g. the runtime panicks due to running out of memory).

So why have two functions? Mostly to reduce churn in this PR. This is not the allocation stats PR; I just preemptively added this so that I can write a reasonable test for the issue I'm fixing here. I intend to flesh out the allocation stats (and probably rethink the interface) in a future PR.

&self,
runtime_blob: RuntimeBlob,
ext: &mut dyn Externalities,
allow_missing_host_functions: bool,
export_name: &str,
call_data: &[u8],
) -> (std::result::Result<Vec<u8>, Error>, Option<AllocationStats>) {
let mut allocation_stats = None;
let result = self.uncached_call_impl(
runtime_blob,
ext,
allow_missing_host_functions,
export_name,
call_data,
&mut allocation_stats,
);
(result, allocation_stats)
}

fn uncached_call_impl(
&self,
runtime_blob: RuntimeBlob,
ext: &mut dyn Externalities,
allow_missing_host_functions: bool,
export_name: &str,
call_data: &[u8],
allocation_stats_out: &mut Option<AllocationStats>,
) -> std::result::Result<Vec<u8>, Error> {
let module = crate::wasm_runtime::create_wasm_runtime_with_code::<H>(
self.method,
Expand All @@ -235,10 +276,14 @@ where
let mut instance = AssertUnwindSafe(instance);
let mut ext = AssertUnwindSafe(ext);
let module = AssertUnwindSafe(module);
let mut allocation_stats_out = AssertUnwindSafe(allocation_stats_out);

with_externalities_safe(&mut **ext, move || {
preregister_builtin_ext(module.clone());
instance.call_export(export_name, call_data)
let (result, allocation_stats) =
instance.call_with_allocation_stats(export_name.into(), call_data);
**allocation_stats_out = allocation_stats;
result
})
.and_then(|r| r)
}
Expand Down
Loading