Skip to content
This repository was archived by the owner on Mar 24, 2022. It is now read-only.

Commit e890e0a

Browse files
authored
Fixes #452: Introduce a random slot allocation strategy to Regions. (#496)
1 parent f81349a commit e890e0a

File tree

7 files changed

+264
-44
lines changed

7 files changed

+264
-44
lines changed

Cargo.lock

Lines changed: 5 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lucet-runtime/lucet-runtime-internals/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ memoffset = "0.5.3"
2424
nix = "0.17"
2525
num-derive = "0.3.0"
2626
num-traits = "0.2"
27+
rand = "0.7"
2728
raw-cpuid = "6.0.0"
2829
thiserror = "1.0.4"
2930
tracing = "0.1.12"

lucet-runtime/lucet-runtime-internals/src/alloc/mod.rs

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ use crate::region::RegionInternal;
44
use libc::c_void;
55
use lucet_module::GlobalValue;
66
use nix::unistd::{sysconf, SysconfVar};
7-
use std::sync::{Arc, Once, Weak};
7+
use rand::{thread_rng, Rng, RngCore};
8+
use std::sync::{Arc, Mutex, Once, Weak};
89

910
pub const HOST_PAGE_SIZE_EXPECTED: usize = 4096;
1011
static mut HOST_PAGE_SIZE: usize = 0;
@@ -94,6 +95,45 @@ impl Slot {
9495
}
9596
}
9697

98+
/// The strategy by which a `Region` selects an allocation to back an `Instance`.
99+
pub enum AllocStrategy {
100+
/// Allocate from the next slot available.
101+
Linear,
102+
/// Allocate randomly from the set of available slots.
103+
Random,
104+
/// Allocate randomly from the set of available slots using the
105+
/// supplied random number generator.
106+
///
107+
/// This strategy is used to create reproducible behavior for testing.
108+
CustomRandom(Arc<Mutex<dyn RngCore>>),
109+
}
110+
111+
impl AllocStrategy {
112+
/// For a given `AllocStrategy`, use the number of free_slots and
113+
/// capacity to determine the next slot to allocate for an
114+
/// `Instance`.
115+
pub fn next(&mut self, free_slots: usize, capacity: usize) -> Result<usize, Error> {
116+
if free_slots == 0 {
117+
return Err(Error::RegionFull(capacity));
118+
}
119+
match self {
120+
AllocStrategy::Linear => Ok(free_slots - 1),
121+
AllocStrategy::Random => {
122+
// Instantiate a random number generator and get a
123+
// random slot index.
124+
let mut rng = thread_rng();
125+
Ok(rng.gen_range(0, free_slots))
126+
}
127+
AllocStrategy::CustomRandom(custom_rng) => {
128+
// Get a random slot index using the supplied random
129+
// number generator.
130+
let mut rng = custom_rng.lock().unwrap();
131+
Ok(rng.gen_range(0, free_slots))
132+
}
133+
}
134+
}
135+
}
136+
97137
/// The structure that manages the allocations backing an `Instance`.
98138
///
99139
/// `Alloc`s are not to be created directly, but rather are created by `Region`s during instance

lucet-runtime/lucet-runtime-internals/src/alloc/tests.rs

Lines changed: 171 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22
macro_rules! alloc_tests {
33
( $TestRegion:path ) => {
44
use libc::c_void;
5-
use std::sync::Arc;
5+
use rand::rngs::StdRng;
6+
use rand::{thread_rng, Rng, SeedableRng};
7+
use std::sync::{Arc, Mutex};
68
use $TestRegion as TestRegion;
7-
use $crate::alloc::{host_page_size, Limits, MINSIGSTKSZ};
9+
use $crate::alloc::{host_page_size, AllocStrategy, Limits, MINSIGSTKSZ};
810
use $crate::context::{Context, ContextHandle};
911
use $crate::error::Error;
1012
use $crate::instance::InstanceInternal;
@@ -774,6 +776,173 @@ macro_rules! alloc_tests {
774776
assert_eq!(region.used_slots(), 0);
775777
}
776778

779+
/// This test exercises the AllocStrategy::Random. In this scenario,
780+
/// the Region has a single slot which is "randomly" allocated and then dropped.
781+
#[test]
782+
fn slot_counts_work_with_random_alloc() {
783+
let module = MockModuleBuilder::new()
784+
.with_heap_spec(ONE_PAGE_HEAP)
785+
.build();
786+
let region = TestRegion::create(1, &LIMITS).expect("region created");
787+
assert_eq!(region.capacity(), 1);
788+
assert_eq!(region.free_slots(), 1);
789+
assert_eq!(region.used_slots(), 0);
790+
791+
let inst = region
792+
.new_instance_builder(module.clone())
793+
.with_alloc_strategy(AllocStrategy::Random)
794+
.build()
795+
.expect("new_instance succeeds");
796+
assert_eq!(region.capacity(), 1);
797+
assert_eq!(region.free_slots(), 0);
798+
assert_eq!(region.used_slots(), 1);
799+
800+
drop(inst);
801+
assert_eq!(region.capacity(), 1);
802+
assert_eq!(region.free_slots(), 1);
803+
assert_eq!(region.used_slots(), 0);
804+
}
805+
806+
/// This test exercises the AllocStrategy::CustomRandom. In this scenario,
807+
/// the Region has 10 slots which are randomly allocated up to capacity
808+
/// and then dropped. The test is executed 100 times to exercise the
809+
/// random nature of the allocation strategy.
810+
#[test]
811+
fn slot_counts_work_with_custom_random_alloc() {
812+
let mut master_rng = thread_rng();
813+
let seed: u64 = master_rng.gen();
814+
eprintln!(
815+
"Seeding slot_counts_work_with_custom_random_alloc() with {}",
816+
seed
817+
);
818+
819+
let rng: StdRng = SeedableRng::seed_from_u64(seed);
820+
let shared_rng = Arc::new(Mutex::new(rng));
821+
822+
for _ in 0..100 {
823+
let mut inst_vec = Vec::new();
824+
let module = MockModuleBuilder::new()
825+
.with_heap_spec(ONE_PAGE_HEAP)
826+
.build();
827+
let total_slots = 10;
828+
let region = TestRegion::create(total_slots, &LIMITS).expect("region created");
829+
assert_eq!(region.capacity(), total_slots);
830+
assert_eq!(region.free_slots(), 10);
831+
assert_eq!(region.used_slots(), 0);
832+
833+
// Randomly allocate all of the slots in the region.
834+
for i in 1..=total_slots {
835+
let inst = region
836+
.new_instance_builder(module.clone())
837+
.with_alloc_strategy(AllocStrategy::CustomRandom(shared_rng.clone()))
838+
.build()
839+
.expect("new_instance succeeds");
840+
841+
assert_eq!(region.capacity(), total_slots);
842+
assert_eq!(region.free_slots(), total_slots - i);
843+
assert_eq!(region.used_slots(), i);
844+
inst_vec.push(inst);
845+
}
846+
847+
// It's not possible to allocate just one more. Try
848+
// it and affirm that the error is handled gracefully.
849+
let wont_inst = region
850+
.new_instance_builder(module.clone())
851+
.with_alloc_strategy(AllocStrategy::CustomRandom(shared_rng.clone()))
852+
.build();
853+
assert!(wont_inst.is_err());
854+
855+
// Drop all of the slots in the region.
856+
for i in 1..=total_slots {
857+
drop(inst_vec.pop());
858+
assert_eq!(region.capacity(), total_slots);
859+
assert_eq!(region.free_slots(), total_slots - (total_slots - i));
860+
assert_eq!(region.used_slots(), total_slots - i);
861+
}
862+
863+
// Allocate just one more to make sure the drops took place
864+
// and the Region has capacity again.
865+
region
866+
.new_instance_builder(module.clone())
867+
.with_alloc_strategy(AllocStrategy::CustomRandom(shared_rng.clone()))
868+
.build()
869+
.expect("new_instance succeeds");
870+
}
871+
}
872+
873+
/// This test exercises a mixed AllocStrategy. In this scenario,
874+
/// the Region has 10 slots which are randomly and linearly allocated
875+
/// up to capacity and then dropped. The test is executed 100 times to
876+
/// exercise the random nature of the allocation strategy.
877+
#[test]
878+
fn slot_counts_work_with_mixed_alloc() {
879+
let mut master_rng = thread_rng();
880+
let seed: u64 = master_rng.gen();
881+
eprintln!("Seeding slot_counts_work_with_mixed_alloc() with {}", seed);
882+
883+
let rng: StdRng = SeedableRng::seed_from_u64(seed);
884+
let shared_rng = Arc::new(Mutex::new(rng));
885+
886+
for _ in 0..100 {
887+
let mut inst_vec = Vec::new();
888+
let module = MockModuleBuilder::new()
889+
.with_heap_spec(ONE_PAGE_HEAP)
890+
.build();
891+
let total_slots = 10;
892+
let region = TestRegion::create(total_slots, &LIMITS).expect("region created");
893+
assert_eq!(region.capacity(), total_slots);
894+
assert_eq!(region.free_slots(), 10);
895+
assert_eq!(region.used_slots(), 0);
896+
897+
// Allocate all of the slots in the region.
898+
for i in 1..=total_slots {
899+
let inst;
900+
if i % 2 == 0 {
901+
inst = region
902+
.new_instance_builder(module.clone())
903+
.with_alloc_strategy(AllocStrategy::CustomRandom(shared_rng.clone()))
904+
.build()
905+
.expect("new_instance succeeds");
906+
} else {
907+
inst = region
908+
.new_instance_builder(module.clone())
909+
.with_alloc_strategy(AllocStrategy::Linear)
910+
.build()
911+
.expect("new_instance succeeds");
912+
}
913+
914+
assert_eq!(region.capacity(), total_slots);
915+
assert_eq!(region.free_slots(), total_slots - i);
916+
assert_eq!(region.used_slots(), i);
917+
inst_vec.push(inst);
918+
}
919+
920+
// It's not possible to allocate just one more. Try
921+
// it and affirm that the error is handled gracefully.
922+
let wont_inst = region
923+
.new_instance_builder(module.clone())
924+
.with_alloc_strategy(AllocStrategy::CustomRandom(shared_rng.clone()))
925+
.build();
926+
assert!(wont_inst.is_err());
927+
928+
// Drop all of the slots in the region.
929+
for i in 1..=total_slots {
930+
drop(inst_vec.pop());
931+
assert_eq!(region.capacity(), total_slots);
932+
assert_eq!(region.free_slots(), total_slots - (total_slots - i));
933+
assert_eq!(region.used_slots(), total_slots - i);
934+
}
935+
936+
// Allocate just one more to make sure the drops took place
937+
// and the Region has capacity again.
938+
region
939+
.new_instance_builder(module.clone())
940+
.with_alloc_strategy(AllocStrategy::Linear)
941+
.build()
942+
.expect("new_instance succeeds");
943+
}
944+
}
945+
777946
fn do_nothing_module() -> Arc<dyn Module> {
778947
extern "C" fn do_nothing(_vmctx: *mut lucet_vmctx) -> () {}
779948

lucet-runtime/lucet-runtime-internals/src/module.rs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,18 @@ pub trait ModuleInternal: Send + Sync {
107107
///
108108
/// Returns a `Result<(), Error>` rather than a boolean in order to provide a richer accounting
109109
/// of what may be invalid.
110-
fn validate_runtime_spec(&self, limits: &Limits) -> Result<(), Error> {
110+
fn validate_runtime_spec(
111+
&self,
112+
limits: &Limits,
113+
instance_heap_limit: usize,
114+
) -> Result<(), Error> {
115+
if instance_heap_limit > limits.heap_memory_size {
116+
return Err(Error::InvalidArgument(
117+
"heap memory size requested for instance is larger than slot allows",
118+
));
119+
}
120+
let heap_memory_size = std::cmp::min(limits.heap_memory_size, instance_heap_limit);
121+
111122
// Modules without heap specs will not access the heap
112123
if let Some(heap) = self.heap_spec() {
113124
// Assure that the total reserved + guard regions fit in the address space.
@@ -128,7 +139,7 @@ pub trait ModuleInternal: Send + Sync {
128139
bail_limits_exceeded!("heap spec reserved and guard size: {:?}", heap);
129140
}
130141

131-
if heap.initial_size as usize > limits.heap_memory_size {
142+
if heap.initial_size as usize > heap_memory_size {
132143
bail_limits_exceeded!("heap spec initial size: {:?}", heap);
133144
}
134145

lucet-runtime/lucet-runtime-internals/src/region.rs

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
pub mod mmap;
22

3-
use crate::alloc::{Alloc, Limits, Slot};
3+
use crate::alloc::{Alloc, AllocStrategy, Limits, Slot};
44
use crate::embed_ctx::CtxMap;
55
use crate::error::Error;
66
use crate::instance::InstanceHandle;
@@ -53,6 +53,7 @@ pub trait RegionInternal: Send + Sync {
5353
module: Arc<dyn Module>,
5454
embed_ctx: CtxMap,
5555
heap_memory_size_limit: usize,
56+
alloc_strategy: AllocStrategy,
5657
) -> Result<InstanceHandle, Error>;
5758

5859
/// Unmaps the heap, stack, and globals of an `Alloc`, while retaining the virtual address
@@ -90,6 +91,7 @@ pub struct InstanceBuilder<'a> {
9091
module: Arc<dyn Module>,
9192
embed_ctx: CtxMap,
9293
heap_memory_size_limit: usize,
94+
alloc_strategy: AllocStrategy,
9395
}
9496

9597
impl<'a> InstanceBuilder<'a> {
@@ -99,9 +101,21 @@ impl<'a> InstanceBuilder<'a> {
99101
module,
100102
embed_ctx: CtxMap::default(),
101103
heap_memory_size_limit: region.get_limits().heap_memory_size,
104+
alloc_strategy: AllocStrategy::Linear,
102105
}
103106
}
104107

108+
/// Allocate the instance using the supplied `AllocStrategy`.
109+
///
110+
/// This call is optional. The default allocation strategy for
111+
/// Regions is Linear, which allocates the instance using next available
112+
/// alloc. If a different strategy is desired, choose from those
113+
/// available in `AllocStrategy`.
114+
pub fn with_alloc_strategy(mut self, alloc_strategy: AllocStrategy) -> Self {
115+
self.alloc_strategy = alloc_strategy;
116+
self
117+
}
118+
105119
/// Add a smaller, custom limit for the heap memory size to the built instance.
106120
///
107121
/// This call is optional. Attempts to build a new instance fail if the
@@ -122,7 +136,11 @@ impl<'a> InstanceBuilder<'a> {
122136

123137
/// Build the instance.
124138
pub fn build(self) -> Result<InstanceHandle, Error> {
125-
self.region
126-
.new_instance_with(self.module, self.embed_ctx, self.heap_memory_size_limit)
139+
self.region.new_instance_with(
140+
self.module,
141+
self.embed_ctx,
142+
self.heap_memory_size_limit,
143+
self.alloc_strategy,
144+
)
127145
}
128146
}

0 commit comments

Comments
 (0)