Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
196 changes: 194 additions & 2 deletions crates/polkavm-common/src/program.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::abi::{VM_CODE_ADDRESS_ALIGNMENT, VM_MAXIMUM_CODE_SIZE, VM_MAXIMUM_IMPORT_COUNT, VM_MAXIMUM_JUMP_TABLE_ENTRIES};
use crate::cast::cast;
use crate::utils::ArcBytes;
use crate::varint::{read_simple_varint, read_varint, write_simple_varint, MAX_VARINT_LENGTH};
use core::fmt::Write;
Expand All @@ -8,6 +9,9 @@ use core::ops::Range;
#[repr(transparent)]
pub struct RawReg(u32);

#[cfg(feature = "alloc")]
use crate::abi::MemoryMapBuilder;

impl Eq for RawReg {}
impl PartialEq for RawReg {
fn eq(&self, rhs: &Self) -> bool {
Expand Down Expand Up @@ -444,6 +448,27 @@ impl LookupTable {
}
}

pub const INTERPRETER_CACHE_ENTRY_SIZE: u32 = {
if cfg!(target_pointer_width = "32") {
20
} else if cfg!(target_pointer_width = "64") {
24
} else {
panic!("unsupported target pointer width")
}
};

pub const INTERPRETER_CACHE_RESERVED_ENTRIES: u32 = 10;
pub const INTERPRETER_FLATMAP_ENTRY_SIZE: u32 = 4;

pub fn interpreter_calculate_cache_size(count: usize) -> usize {
count * INTERPRETER_CACHE_ENTRY_SIZE as usize
}

pub fn interpreter_calculate_cache_num_entries(bytes: usize) -> usize {
bytes / INTERPRETER_CACHE_ENTRY_SIZE as usize
}

static TABLE_1: LookupTable = LookupTable::build(1);
static TABLE_2: LookupTable = LookupTable::build(2);

Expand Down Expand Up @@ -737,6 +762,19 @@ mod kani {
let reg2 = reg2.get() as u8;
assert_eq!((reg1, reg2), simple_read_args_regs2(&code));
}

#[kani::proof]
fn verify_interpreter_cache_size() {
let x: usize = kani::any_where(|x| *x <= super::cast(u32::MAX).to_usize());
let bytes: usize = super::interpreter_calculate_cache_size(x);
let calculate_count = super::interpreter_calculate_cache_num_entries(bytes);
assert_eq!(calculate_count, x);

let count = super::interpreter_calculate_cache_num_entries(x);
let calculated_bytes = super::interpreter_calculate_cache_size(count);
assert!(calculated_bytes <= x);
assert!(x - calculated_bytes <= super::interpreter_calculate_cache_size(1));
}
}

/// The lowest level visitor; dispatches directly on opcode numbers.
Expand Down Expand Up @@ -1864,8 +1902,6 @@ impl<'a, 'b, 'c> InstructionFormatter<'a, 'b, 'c> {

impl core::fmt::Display for Formatter {
fn fmt(&self, fmt: &mut core::fmt::Formatter) -> core::fmt::Result {
use crate::cast::cast;

if self.imm == 0 {
write!(fmt, "{}", self.imm)
} else if !self.is_64_bit {
Expand Down Expand Up @@ -4165,6 +4201,28 @@ fn test_instructions_iterator_does_not_emit_unnecessary_invalid_instructions_if_
assert_eq!(i.next(), None);
}

#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub enum EstimateInterpreterMemoryUsageArgs {
UnboundedCache {
instruction_count: u32,
basic_block_count: u32,
page_size: u32,
},
BoundedCache {
instruction_count: u32,
basic_block_count: u32,
max_cache_size_bytes: u32,
max_block_size: u32,
page_size: u32,
},
}

#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub struct ProgramMemoryInfo {
pub baseline_ram_consumption: u32,
pub purgeable_ram_consumption: u32,
}
Comment on lines +4220 to +4224
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Hm.... instead of this struct I think just returning Range<u32> with a minimum and maximum might be better/simpler/easier to understand? (So start of the range would be the baseline minimum, and end would be the maximum total.)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

sure, but explicit field names looks more intuitive to me.

@athei what do you think?


#[derive(Clone, Default)]
#[non_exhaustive]
pub struct ProgramParts {
Expand Down Expand Up @@ -4726,6 +4784,140 @@ impl ProgramBlob {
mutation_depth: 0,
}))
}

#[cfg(feature = "alloc")]
pub(crate) fn calculate_blob_length(&self) -> u64 {
let ProgramBlob {
#[cfg(feature = "unique-id")]
unique_id: _,
is_64_bit: _,
ro_data_size: _,
rw_data_size: _,
stack_size: _,
ro_data,
rw_data,
code,
jump_table,
jump_table_entry_size: _,
bitmask,
import_offsets,
import_symbols,
exports,
debug_strings,
debug_line_program_ranges,
debug_line_programs,
} = self;

let mut ranges = [
ro_data.parent_address_range(),
rw_data.parent_address_range(),
code.parent_address_range(),
jump_table.parent_address_range(),
bitmask.parent_address_range(),
import_offsets.parent_address_range(),
import_symbols.parent_address_range(),
exports.parent_address_range(),
debug_strings.parent_address_range(),
debug_line_program_ranges.parent_address_range(),
debug_line_programs.parent_address_range(),
];

ranges.sort_unstable_by_key(|r| r.start);

let mut blob_length = 0;
let mut last_range = 0..0;
for range in ranges {
if range == last_range {
continue;
}
blob_length += cast(range.len()).to_u64();
last_range = range;
}
blob_length
}

#[cfg(feature = "alloc")]
pub fn estimate_interpreter_memory_usage(&self, args: EstimateInterpreterMemoryUsageArgs) -> Result<ProgramMemoryInfo, &'static str> {
let (page_size, instruction_count, basic_block_count) = match args {
EstimateInterpreterMemoryUsageArgs::UnboundedCache {
page_size,
instruction_count,
basic_block_count,
..
} => (page_size, instruction_count, basic_block_count),
EstimateInterpreterMemoryUsageArgs::BoundedCache {
page_size,
instruction_count,
basic_block_count,
..
} => (page_size, instruction_count, basic_block_count),
};

let cache_entry_count_upper_bound = cast(instruction_count + basic_block_count + INTERPRETER_CACHE_RESERVED_ENTRIES).to_usize();
let cache_size_upper_bound = interpreter_calculate_cache_size(cache_entry_count_upper_bound);

let mut purgeable_ram_consumption = match args {
EstimateInterpreterMemoryUsageArgs::UnboundedCache { .. } => cache_size_upper_bound,
EstimateInterpreterMemoryUsageArgs::BoundedCache {
max_cache_size_bytes,
max_block_size,
..
} => {
let max_cache_size_bytes = cast(max_cache_size_bytes).to_usize();
let cache_entry_count_hard_limit = cast(max_block_size + INTERPRETER_CACHE_RESERVED_ENTRIES).to_usize();
let cache_bytes_hard_limit = interpreter_calculate_cache_size(cache_entry_count_hard_limit);
if cache_bytes_hard_limit > max_cache_size_bytes {
return Err("maximum cache size is too small for the given max block size");
}

max_cache_size_bytes.min(cache_size_upper_bound)
}
};

let code_length = self.code.len();
purgeable_ram_consumption = purgeable_ram_consumption.saturating_add((code_length + 1) * INTERPRETER_FLATMAP_ENTRY_SIZE as usize);

let Ok(purgeable_ram_consumption) = u32::try_from(purgeable_ram_consumption) else {
return Err("estimated interpreter cache size is too large");
};

let memory_map = MemoryMapBuilder::new(page_size)
.ro_data_size(self.ro_data_size)
.rw_data_size(self.rw_data_size)
.stack_size(self.stack_size)
.build()?;

let blob_length = self.calculate_blob_length();
let Ok(baseline_ram_consumption) = u32::try_from(
blob_length
.saturating_add(u64::from(memory_map.ro_data_size()))
.saturating_sub(self.ro_data.len() as u64)
.saturating_add(u64::from(memory_map.rw_data_size()))
.saturating_sub(self.rw_data.len() as u64)
.saturating_add(u64::from(memory_map.stack_size())),
) else {
return Err("calculated baseline RAM consumption is too large");
};

Ok(ProgramMemoryInfo {
baseline_ram_consumption,
purgeable_ram_consumption,
})
}
}

#[cfg(feature = "alloc")]
#[test]
fn test_calculate_blob_length() {
let big_blob = ArcBytes::from(vec![0; 1024]);
let shared_blob = ArcBytes::from(vec![0; 128]);
let parts = ProgramParts {
ro_data: big_blob.subslice(10..20),
rw_data: big_blob.subslice(24..28),
code_and_jump_table: shared_blob.clone(),
..ProgramParts::default()
};
assert_eq!(parts.calculate_blob_length(), 1024 + 128);
}

/// The source location.
Expand Down
23 changes: 14 additions & 9 deletions crates/polkavm-common/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,8 @@ use alloc::{borrow::Cow, sync::Arc, vec::Vec};
#[derive(Clone)]
enum LifetimeObject {
None,
Arc {
_obj: Arc<[u8]>,
},
#[allow(dyn_drop)]
Other {
_obj: Arc<dyn Drop>,
},
Arc { obj: Arc<[u8]> },
Other { obj: Arc<dyn AsRef<[u8]>> },
}

#[derive(Clone)]
Expand Down Expand Up @@ -89,6 +84,16 @@ impl ArcBytes {
lifetime: self.lifetime.clone(),
}
}

#[cfg(feature = "alloc")]
pub(crate) fn parent_address_range(&self) -> Range<usize> {
let slice = match &self.lifetime {
LifetimeObject::None => return 0..0,
LifetimeObject::Arc { obj } => obj.as_ref(),
LifetimeObject::Other { obj } => obj.as_ref().as_ref(),
};
slice.as_ptr() as usize..(slice.as_ptr() as usize + slice.len())
}
}

impl Eq for ArcBytes {}
Expand Down Expand Up @@ -135,7 +140,7 @@ impl From<Vec<u8>> for ArcBytes {
ArcBytes {
pointer: core::ptr::NonNull::new(data.as_ptr().cast_mut()).unwrap(),
length: data.len(),
lifetime: LifetimeObject::Other { _obj: Arc::new(data) },
lifetime: LifetimeObject::Other { obj: Arc::new(data) },
}
}
}
Expand All @@ -146,7 +151,7 @@ impl From<Arc<[u8]>> for ArcBytes {
ArcBytes {
pointer: core::ptr::NonNull::new(data.deref().as_ptr().cast_mut()).unwrap(),
length: data.len(),
lifetime: LifetimeObject::Arc { _obj: data },
lifetime: LifetimeObject::Arc { obj: data },
}
}
}
Expand Down
16 changes: 16 additions & 0 deletions crates/polkavm/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,13 @@ impl<T> IntoResult<T> for T {

pub type RegValue = u64;

#[allow(clippy::exhaustive_structs)]
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub struct SetCacheSizeLimitArgs {
pub max_block_size: u32,
pub max_cache_size_bytes: usize,
}

#[derive(Copy, Clone)]
pub struct RuntimeInstructionSet {
allow_sbrk: bool,
Expand Down Expand Up @@ -1715,4 +1722,13 @@ impl RawInstance {
backend.reset_interpreter_cache();
}
}

/// Set a tight upper limit on the interpreter cache size (in bytes).
pub fn set_interpreter_cache_size_limit(&mut self, cache_info: Option<SetCacheSizeLimitArgs>) -> Result<(), Error> {
#[allow(irrefutable_let_patterns)]
if let InstanceBackend::Interpreted(ref mut backend) = self.backend {
backend.set_interpreter_cache_size_limit(cache_info)?
}
Ok(())
}
}
4 changes: 2 additions & 2 deletions crates/polkavm/src/compiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ const END_BASIC_BLOCK_INVALID: usize = 2;

struct CachePerCompilation {
assembler: Assembler,
program_counter_to_label: FlatMap<Label>,
program_counter_to_label: FlatMap<Label, false>,
gas_metering_stub_offsets: Vec<usize>,
gas_cost_for_basic_block: Vec<u32>,
export_to_label: HashMap<u32, Label>,
Expand Down Expand Up @@ -93,7 +93,7 @@ where
code: &'a [u8],
bitmask: &'a [u8],
asm: Assembler,
program_counter_to_label: FlatMap<Label>,
program_counter_to_label: FlatMap<Label, false>,
step_tracing: bool,
ecall_label: Label,
export_to_label: HashMap<u32, Label>,
Expand Down
Loading